最近看了几篇关于数据库事物的文章,很受启发。对于复杂的业务系统,通常需要操作各种不同的数据源,为了保证数据的统一性和完整性,必须解决数据库的事物问题。对于简单的单数据源的事物,利用JDBC的事物控制就可以完成,跨数据源多库的事物需要利用JTA的API完成。
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、 事物问题的控制
一般事物控制有两种情况,一种是声明式的事物,如EJB、Spring的AOP等;另外一种是采用直接编码控制事物。声明式的事物一般是采用AOP设置代理类,最终和编码的方式在本质上是一致的。这里我们主要讨论编码式的事物控制。在实际的开发中,可能会跨数据库处理事物,需要利用JTA控制,这里先考虑简单的单个数据源的事物控制,主要利用JDBC实现。
对于Web应用的事物控制,核心的问题是事物应该被确定在那个层面上进行。一个最常见的解决方案是统一在Service层处理,比如Spring整合Hibernate时利用AOP在Service层控制事物。事物控制在Service层遇到的问题是,当Controller层需要处理一个复杂的业务时,可能会调用ServiceA和ServiceB,这时,两个Service的事物没有办法保证一致。简单的解决办法是多写一个ServiceC类,在ServiceC中完成复杂的业务功能。并且,在ServiceC中,是不能调用ServiceA或者ServiceB的,否则会产生事物的嵌套。当业务越来越复杂,代码的利用率会降低。有人提出的解决方案是将Service分类,分为A、B、C三大类。
A类为简单的操作类Service,即不需要调用DAO函数,不需要操作数据库,这类Service可以在名称后面加上后缀区分,如AccountHelper。这类Service就是简单的辅助对象,可以在任何地方调用。
B类为需要操作DAO函数的,我们直接命名为AccountTransaction。这类Service直接提供给Controller调用。这类Service需要复杂打开和关闭Conn,即简单的理解为conn.begin()和conn.commit()就位于这类Service的开始和结束,其他的Service对象不能嵌套的调用他们。
C类为需要调用DAO函数的,但是他们不能直接被Controller调用,他们不负责事物的begin和commit。他们只能被B类调用,由B类负责事物控制。
当Service被分解为三大类后,你可以使用Spring的AOP功能配置他们的事物控制代理了。这种方案可以很好的解决事物问题。但是,这种方案的可操作性还是有问题,面对换了一拨又一拨的开发人员,代码的规范和质量不能很好的保证,那么只能实现一种更加简单可行的方案了。
当一个Web请求被响应,Servlet会产生一个新的线程,我们可以利用这种唯一的ThreadId作为唯一键标记一个Connection,所有基于该请求的操作都利用该Conn完成,事物的控制全部交给该Conn。即我们将事物控制从Service层提到Controller层。我们对Struts的配置文件做一点变通,对于每个Action加入一个关于事物控制的属性。当不涉及数据库操作的请求,我们设置为事物无关的,设计数据库操作的请求设为事物控制的,稍稍减少一下无数据库操作对事物控制的浪费。考虑下一个需要数据库操作的Action被请求后,后台是如何运作的。
首先,取得当前的ThreadId,在连接池中找到一个空闲的Conn,然后把它放置到InUsed池内,同时begin一个事物。Service类不需要什么特殊的修改,不管多少次的嵌套,不管操作多少次的DAO,当需要数据库操作时,我们利用当前的ThreadId在InUsed池内取得Conn。经历了N多操作后,在Action负责转向之前,我们利用当前的ThreadId得到了Conn,我们进行commit。或者在任何一个Exception,我们rollback。当然有一点小小的补充,当类似批处理的请求被提交,我们需要完成大量的工作,可能需要分批次的提交事物。比如,我们要做一个百万数据导入另外一个表中,我们不能因为9999999条失败了而回滚了整个事物。我们可以提供一些额外的API,完成个性化的事物控制,将大事物分解为小的事物,分批提交。
上面的文字,完成了对一个简单的JDBC事物控制的解决方案。对于复杂的业务系统,牺牲部分性能,将事物交给Controller层处理,会比Service层处理得到更加好的开发效率和事物保证。且不论开发人员的素质问题,在利用Spring的AOP功能时,同样会N多AOP代理的性能消耗。而Controller控制事物,开发人员可以完全不予理会事物操作,不会有嵌套事物,不需要命名规则。