Transaction 那点事儿(一)
这篇博文已经“难产”好几天了,压力还是有些大的,因为 Transaction(事务管理)的问题,争论一直就没有停止过。由于个人能力真的非常有限,花了好多功夫去学习,总算基本上解决了问题,所以这才第一时间就拿出来与网友们共享,也听听大家的想法。
提示:对 Transaction 不太理解的朋友们,可阅读这篇博文《Transaction 那点事儿》。
现在就开始吧!
请看下面这一段代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@Bean public class ProductServiceImpl extends BaseService implements ProductService { ... @Override public boolean createProduct(Map<String, Object> productFieldMap) { String sql = SQLHelper.getSQL( "insert.product" ); Object[] params = { productFieldMap.get( "productTypeId" ), productFieldMap.get( "productName" ), productFieldMap.get( "productCode" ), productFieldMap.get( "price" ), productFieldMap.get( "description" ) }; int rows = DBHelper.update(sql, params); return rows == 1 ; } } |
我们先不去考虑 createProduct() 方法中那段不够优雅的代码,总之这一坨 shi 就是为了完成一个 insert 语句的,后续我会将其简化。
除此以外,大家可能已经看出一些问题。没有事务管理!
如果执行过程中抛出了一个异常,事务无法回滚。这个案例仅仅是一条 SQL 语句,如果是多条呢?前面的执行成功了,就最后一条执行失败,那应该是整个事务都要回滚,前面做的都不算数才对。
为了实现这个目标,我山寨了 Spring 的做法,它有一个 @Transactional 注解,可以标注在方法上,那么被标注的方法就是具备事务特性了,还可以设置事务传播方式与隔离级别等功能,确实够强大的,完全取代了以前的 XML 配置方式。
于是我也做了一个 @Transaction 注解(注意:我这里是事务的名词,Spring 用的是形容词),代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
@Bean public class ProductServiceImpl extends BaseService implements ProductService { ... @Override @Transaction public boolean createProduct(Map<String, Object> productFieldMap) { String sql = SQLHelper.getSQL( "insert.product" ); Object[] params = { productFieldMap.get( "productTypeId" ), productFieldMap.get( "productName" ), productFieldMap.get( "productCode" ), productFieldMap.get( "price" ), productFieldMap.get( "description" ) }; int rows = DBHelper.update(sql, params); if ( true ) { throw new RuntimeException( "Insert log failure!" ); // 故意抛出异常,让事务回滚 } return rows == 1 ; } } |
在执行 DBHelper.update() 方法以后,我故意抛出了一个 RuntimeException,我想看看事务能否回滚,也就是那条 insert 语句没有生效。
做了一个单元测试,测了一把,果然报错了,product 表里也没有插入任何数据。
看来事务管理功能的确生效了,那么,我是如何实现 @Transaction 这个注解所具有的功能?请接着往下看,下面的才是精华所在。
一开始我修改了 DBHelper 的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
|
public class DBHelper { private static final BasicDataSource ds = new BasicDataSource(); private static final QueryRunner runner = new QueryRunner(ds); // 定义一个局部线程变量(使每个线程都拥有自己的连接) private static ThreadLocal<Connection> connContainer = new ThreadLocal<Connection>(); static { System.out.println( "Init DBHelper..." ); // 初始化数据源 ds.setDriverClassName(ConfigHelper.getStringProperty( "jdbc.driver" )); ds.setUrl(ConfigHelper.getStringProperty( "jdbc.url" )); ds.setUsername(ConfigHelper.getStringProperty( "jdbc.username" )); ds.setPassword(ConfigHelper.getStringProperty( "jdbc.password" )); ds.setMaxActive(ConfigHelper.getNumberProperty( "jdbc.max.active" )); ds.setMaxIdle(ConfigHelper.getNumberProperty( "jdbc.max.idle" )); } // 获取数据源 public static DataSource getDataSource() { return ds; } // 开启事务 public static void beginTransaction() { Connection conn = connContainer.get(); if (conn == null ) { try { conn = ds.getConnection(); conn.setAutoCommit( false ); } catch (Exception e) { e.printStackTrace(); } finally { connContainer.set(conn); } } } // 提交事务 public static void commitTransaction() { Connection conn = connContainer.get(); if (conn != null ) { try { conn.commit(); conn.close(); } catch (Exception e) { e.printStackTrace(); } finally { connContainer.remove(); } } } // 回滚事务 public static void rollbackTransaction() { Connection conn = connContainer.get(); if (conn != null ) { try { conn.rollback(); conn.close(); } catch (Exception e) { e.printStackTrace(); } finally { connContainer.remove(); } } } ... // 执行更新(包括 UPDATE、INSERT、DELETE) public static int update(String sql, Object... params) { // 若当前线程中存在连接,则传入(用于事务处理),否则将从数据源中获取连接 Connection conn = connContainer.get(); return DBUtil.update(runner, conn, sql, params); } } |
首先,我将 Connection 放到 ThreadLocal 容器中了,这样每个线程之间对 Connection 的访问就是隔离的了(不会共享),保证了线程安全。
然后,我增加了几个关于事务的方法,例如:beginTransaction()、commitTransaction()、rollbackTransaction(),这三个方法中的代码非常重要,一定要细看!我就不解释了。
最后,我修改了 update() 方法,先从 ThreadLocal 中拿出 Connection,然后传入到 DBUtil.update() 方法中。注意:有可能从 ThreadLocal 中根本拿不到 Connection,因为此时的 Connection 是从 DataSource 中获取的(这是非事务的情况),只要执行了 beginTransaction() 方法,就会从 DataSource 中获取一个 Connection,然后将事务自动提交功能关闭,最后往 ThreadLocal 中放入一个 Connection。
提示:对 ThreadLocal 不太理解的朋友们,可阅读这篇博文《ThreadLocal 那点事儿》。
那问题来了,DBUtil 又是如何处理事务的呢?我对 DBUtil 是这样修改的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
public class DBUtil { ... // 更新(包括 UPDATE、INSERT、DELETE,返回受影响的行数) public static int update(QueryRunner runner, Connection conn, String sql, Object... params) { int result = 0 ; try { if (conn != null ) { result = runner.update(conn, sql, params); } else { result = runner.update(sql, params); } } catch (SQLException e) { e.printStackTrace(); } return result; } } |
这里,我首先对传入进来的 Connection 对象进行判断:
若不为空(事务情况),调用 runner.update(conn, sql, params) 方法,将 conn 传递到 QueryRunner 中,也就是说,完全交给 Apache Commons DbUtils 来处理事务了,因为此时的 conn 是动过手脚的(在 beginTransaction() 方法中,做了 conn.setAutoCommit(false) 操作)。
若为空(非事务情况),调用 runner.update(sql, params) 方法,此时没有将 conn 传递到 QueryRunner 中,也就是说,Connection 由 Apache Commons DbUtils 从 DataSource 中获取,无需考虑事务问题,或者说,事务是自动提交的。
我想到这里,我已经解释清楚了。但还有必要再做一下总结:
获取 Connection 分两种情况,若自动从 DataSource 中获取,则为非事务情况;反之,从关闭 Connection 自动提交功能后,强制传入 Connection 时,则为事务情况。因为传递过去的是同一个 Connection,那么 Apache Commons DbUtils 是不会自动从 DataSource 中获取 Connection 了。
好了,地基终于建设完毕,剩下的就是什么时候调用那些 xxxTransaction() 方法呢?又是在哪里调用的呢?
最简单又最直接的方式莫过于此:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
@Bean public class ProductServiceImpl extends BaseService implements ProductService { ... public boolean createProduct(Map<String, Object> productFieldMap) { int rows = 0 ; try { // 开启事务 DBHelper.beginTransaction(); String sql = SQLHelper.getSQL( "insert.product" ); Object[] params = { productFieldMap.get( "productTypeId" ), productFieldMap.get( "productName" ), productFieldMap.get( "productCode" ), productFieldMap.get( "price" ), productFieldMap.get( "description" ) }; rows = DBHelper.update(sql, params); } catch (Exception e) { // 回滚事务 DBHelper.rollbackTransaction(); e.printStackTrace(); throw new RuntimeException(); } finally { // 提交事务 DBHelper.commitTransaction(); } return rows == 1 ; } } |
但这样写,总感觉太累赘,以后凡是需要考虑事务问题的,都要用一个 try...catch...finally 语句来处理,还要手工调用那些 DBHelper.xxxTransaction() 方法。对于开发人员而言,简直这就像噩梦!
这里就要用到一点设计模式了,我选择了“Proxy 模式”,就是“代理模式”,说准确一点应该是“动态代理模式”。
提示:对 Proxy 不太理解的朋友,可阅读这篇博文《Proxy 那点事儿》。
我想把一头一尾的代码都放在 Proxy 中,这里仅保留最核心的逻辑。代理类会自动拦截到 Service 类中所有的方法,先判断该方法是否带有 @Transaction 注解,如果有的话,就开启事务,然后调用方法,最后提交事务,遇到异常还要回滚事务。若没有 @Transaction 注解呢?什么都不做,直接调用目标方法即可。
这就是我的思路,下面看看这个动态代理类是如何实现的吧:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
public class TransactionProxy implements MethodInterceptor { private static TransactionProxy instance = new TransactionProxy(); private TransactionProxy() { } public static TransactionProxy getInstance() { return instance; } @SuppressWarnings ( "unchecked" ) public <T> T getProxy(Class<T> cls) { return (T) Enhancer.create(cls, this ); } @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { Object result; if (method.isAnnotationPresent(Transaction. class )) { try { // 开启事务 DBHelper.beginTransaction(); // 执行操作 method.setAccessible( true ); result = proxy.invokeSuper(obj, args); // 提交事务 DBHelper.commitTransaction(); } catch (Exception e) { // 回滚事务 DBHelper.rollbackTransaction(); e.printStackTrace(); throw new RuntimeException(); } } else { result = proxy.invokeSuper(obj, args); } return result; } } |
我选用的是 CGLib 类库实现的动态代理,因为我认为它比 JDK 提供的动态代理更为强大一些,它可以代理没有接口的类,而 JDK 的动态代理是有限制的,目标类必须实现接口才能被代理。
在这个 TransactionProxy 类中还用到了“Singleton 模式”,作用是提高一些性能,同时也简化了 API 调用方式。
下面是最重要的地方了,如何才能将这些具有事务的 Service 类加入 IoC 容器呢?这样在 Action 中注入的 Service 就不再是普通的实现类了,而是通过 CGLib 动态生成的实现类(可以在 IDE 中打个断点看看就知道)。
好了,看看负责 IoC 容器的 BeanHelper吧,我又是如何修改的呢?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
public class BeanHelper { // Bean 类 => Bean 实例 private static final Map<Class<?>, Object> beanMap = new HashMap<Class<?>, Object>(); static { System.out.println( "Init BeanHelper..." ); try { // 获取并遍历所有的 Bean(带有 @Bean 注解的类) List<Class<?>> beanClassList = ClassHelper.getClassListByAnnotation(Bean. class ); for (Class<?> beanClass : beanClassList) { // 创建 Bean 实例 Object beanInstance; if (BaseService. class .isAssignableFrom(beanClass)) { // 若为 Service 类,则获取动态代理实例(可以使用 CGLib 动态代理,不能使用 JDK 动态代理,因为初始化 Bean 字段时会报错) beanInstance = TransactionProxy.getInstance().getProxy(beanClass); } else { // 否则通过反射创建实例 beanInstance = beanClass.newInstance(); } // 将 Bean 实例放入 Bean Map 中(键为 Bean 类,值为 Bean 实例) beanMap.put(beanClass, beanInstance); } // 遍历 Bean Map for (Map.Entry<Class<?>, Object> beanEntry : beanMap.entrySet()) { ... } } catch (Exception e) { e.printStackTrace(); } } ... } |
在遍历 beanClassList 时,判断当前的 beanClass 是否继承于 BaseService?如果是,那么就创建动态代理实例给 beanInstance;否则,就像以前一样,通过反射来创建 beanInstance。
改动量还不算太大,动态代理就会初始化到相应的 Bean 对象上了。
到此为止,事务管理实现原理已全部结束。当然问题还有很多,比如:我没有考虑事务隔离级别、事务传播行为、事务超时、只读事务等问题,甚至还有更复杂的 JTA 事务。
但我个人认为,事务管理功能实用就行了,标注了 @Transaction 注解的方法就有事务,没有标注就没有事务,很简单。没必要真的做得和 Spring 事务管理器那样完备,比如:支持 7 种事务传播行为。那有人就会提到,为什么不提供“嵌套事务”和“JTA 事务”呢?我想说的是,追求是无止境的,即便是 Spring 也有它的不足之处。关键是对框架的定位要看准,该框架仅用于开发中、小规模的 Java Web 应用系统,那么这类复杂的事务处理情况又会有多少呢?所以我暂时就此打住了,我的直觉告诉我,深入下去将一定是一个无底洞。
我想有必要先听听大家的想法,避免走弯路的最佳方式就是及时沟通。