沉睡森林@漂在北京

本处文章除注明“转载”外均为原创,转载请注明出处。

  BlogJava :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理 ::
  152 随笔 :: 4 文章 :: 114 评论 :: 0 Trackbacks


最近看了几篇关于数据库事物的文章,很受启发。对于复杂的业务系统,通常需要操作各种不同的数据源,为了保证数据的统一性和完整性,必须解决数据库的事物问题。对于简单的单数据源的事物,利用JDBC的事物控制就可以完成,跨数据源多库的事物需要利用JTAAPI完成。

1、  问题描述

涉及Web的应用,通常针对每次请求,Servlet容器产生一个新的线程。常用的DAO模式,对于每次dao操作都会获得一个数据库连接,操作完成后销毁数据库连接资源。一个最原始的DAO模式代码如下:

publicvoid delete(Long id) throws AccountException {

           Connection conn =null;

           Statement stmt = null;

           ResultSet rs = null;

           try {

                 conn =DbUtil.openConn();

                 stmt = conn.createStatement();

                 stmt.executeQuery("DELETE  FROM TABLE_NAME");

//              while(rs.next()){

                      //...

//              }

                

           } catch (Exception e) {

                 e.printStackTrace();

                  //...

           }finally{

                 DbUtil.close(conn, stmt, rs);

           }

}

     

这种模式的问题很多,主要有以下几点:

l        每次调用都会初始化数据库连接资源,使用完毕后清除资源。对于数据库资源的创建和销毁都会消耗大量的性能。

l        使用了JDBC默认的数据库事物提交方式,调用即提交数据库事物。如果在该DAO函数里面完成多个操作的话,无法保证事务性。

l        WEB层或者SERVICE层调用DAO方法时,如果一次需要调用多个DAO,那么会多次初始化和销毁数据库资源,事物也无法保证。

总结上面的问题,可以发现,其实这种常用的DAO模式根本无法满足企业系统开发的需要,无法保证系统的性能和数据的完整性。那么,怎么样的DAO设计才可以满足企业开发的需要?

2、  性能问题的解决

对于上面的示例,性能的优化主要在于数据库资源的重用。考虑数据库连接这种重量级的资源的宝贵性,我们可以建立一个缓存池,对连接进行池化操作。Pool的概念在JAVA EE的设计中经常遇到,连接池的使用可以避免大量不需要的创建和销毁产生的性能压力。

基于上面的分析,我们可以设计一个连接池,设置最大连接数量,在系统启动时初始化连接池。当然,我们可以分别设计一个使用中的连接池和一个空闲的连接池,存放不同状态的连接。当一个连接被请求获取,那么它进入InUse状态,当请求操作完成,该连接进入Idlesse状态。当然,Idlesse状态的连接被请求获取后会进入InUse状态。

3、  事物问题的控制

一般事物控制有两种情况,一种是声明式的事物,如EJBSpringAOP等;另外一种是采用直接编码控制事物。声明式的事物一般是采用AOP设置代理类,最终和编码的方式在本质上是一致的。这里我们主要讨论编码式的事物控制。在实际的开发中,可能会跨数据库处理事物,需要利用JTA控制,这里先考虑简单的单个数据源的事物控制,主要利用JDBC实现。

对于Web应用的事物控制,核心的问题是事物应该被确定在那个层面上进行。一个最常见的解决方案是统一在Service层处理,比如Spring整合Hibernate时利用AOPService层控制事物。事物控制在Service层遇到的问题是,当Controller层需要处理一个复杂的业务时,可能会调用ServiceAServiceB,这时,两个Service的事物没有办法保证一致。简单的解决办法是多写一个ServiceC类,在ServiceC中完成复杂的业务功能。并且,在ServiceC中,是不能调用ServiceA或者ServiceB的,否则会产生事物的嵌套。当业务越来越复杂,代码的利用率会降低。有人提出的解决方案是将Service分类,分为ABC三大类。

A类为简单的操作类Service,即不需要调用DAO函数,不需要操作数据库,这类Service可以在名称后面加上后缀区分,如AccountHelper。这类Service就是简单的辅助对象,可以在任何地方调用。

B类为需要操作DAO函数的,我们直接命名为AccountTransaction。这类Service直接提供给Controller调用。这类Service需要复杂打开和关闭Conn,即简单的理解为conn.begin()conn.commit()就位于这类Service的开始和结束,其他的Service对象不能嵌套的调用他们。

C类为需要调用DAO函数的,但是他们不能直接被Controller调用,他们不负责事物的begincommit。他们只能被B类调用,由B类负责事物控制。

Service被分解为三大类后,你可以使用SpringAOP功能配置他们的事物控制代理了。这种方案可以很好的解决事物问题。但是,这种方案的可操作性还是有问题,面对换了一拨又一拨的开发人员,代码的规范和质量不能很好的保证,那么只能实现一种更加简单可行的方案了。

当一个Web请求被响应,Servlet会产生一个新的线程,我们可以利用这种唯一的ThreadId作为唯一键标记一个Connection,所有基于该请求的操作都利用该Conn完成,事物的控制全部交给该Conn。即我们将事物控制从Service层提到Controller层。我们对Struts的配置文件做一点变通,对于每个Action加入一个关于事物控制的属性。当不涉及数据库操作的请求,我们设置为事物无关的,设计数据库操作的请求设为事物控制的,稍稍减少一下无数据库操作对事物控制的浪费。考虑下一个需要数据库操作的Action被请求后,后台是如何运作的。

首先,取得当前的ThreadId,在连接池中找到一个空闲的Conn,然后把它放置到InUsed池内,同时begin一个事物。Service类不需要什么特殊的修改,不管多少次的嵌套,不管操作多少次的DAO,当需要数据库操作时,我们利用当前的ThreadIdInUsed池内取得Conn。经历了N多操作后,在Action负责转向之前,我们利用当前的ThreadId得到了Conn,我们进行commit。或者在任何一个Exception,我们rollback。当然有一点小小的补充,当类似批处理的请求被提交,我们需要完成大量的工作,可能需要分批次的提交事物。比如,我们要做一个百万数据导入另外一个表中,我们不能因为9999999条失败了而回滚了整个事物。我们可以提供一些额外的API,完成个性化的事物控制,将大事物分解为小的事物,分批提交。

上面的文字,完成了对一个简单的JDBC事物控制的解决方案。对于复杂的业务系统,牺牲部分性能,将事物交给Controller层处理,会比Service层处理得到更加好的开发效率和事物保证。且不论开发人员的素质问题,在利用SpringAOP功能时,同样会NAOP代理的性能消耗。而Controller控制事物,开发人员可以完全不予理会事物操作,不会有嵌套事物,不需要命名规则。

 

posted on 2009-03-07 13:34 王总兵 阅读(1352) 评论(0)  编辑  收藏

只有注册用户登录后才能发表评论。


网站导航: