声明式事务原理

请看下面这一段代码:

01 @Bean
02 public class ProductServiceImpl extends BaseService implements ProductService {
03  
04     ...
05  
06     @Override
07     public boolean createProduct(Map<String, Object> productFieldMap) {
08         String sql = SQLHelper.getSQL("insert.product");
09         Object[] params = {
10             productFieldMap.get("productTypeId"),
11             productFieldMap.get("productName"),
12             productFieldMap.get("productCode"),
13             productFieldMap.get("price"),
14             productFieldMap.get("description")
15         };
16         int rows = DBHelper.update(sql, params);
17         return rows == 1;
18     }
19 }

我们先不去考虑 createProduct() 方法中那段不够优雅的代码,总之这一坨 shi 就是为了完成一个 insert 语句的,后续我会将其简化。

除此以外,大家可能已经看出一些问题。没有事务管理!

如果执行过程中抛出了一个异常,事务无法回滚。这个案例仅仅是一条 SQL 语句,如果是多条呢?前面的执行成功了,就最后一条执行失败,那应该是整个事务都要回滚,前面做的都不算数才对。

为了实现这个目标,我山寨了 Spring 的做法,它有一个 @Transactional 注解,可以标注在方法上,那么被标注的方法就是具备事务特性了,还可以设置事务传播方式与隔离级别等功能,确实够强大的,完全取代了以前的 XML 配置方式。

于是我也做了一个 @Transaction 注解(注意:我这里是事务的名词,Spring 用的是形容词),代码如下:

01 @Bean
02 public class ProductServiceImpl extends BaseService implements ProductService {
03  
04     ...
05  
06     @Override
07     @Transaction
08     public boolean createProduct(Map<String, Object> productFieldMap) {
09         String sql = SQLHelper.getSQL("insert.product");
10         Object[] params = {
11             productFieldMap.get("productTypeId"),
12             productFieldMap.get("productName"),
13             productFieldMap.get("productCode"),
14             productFieldMap.get("price"),
15             productFieldMap.get("description")
16         };
17         int rows = DBHelper.update(sql, params);
18         if (true) {
19             throw new RuntimeException("Insert log failure!"); // 故意抛出异常,让事务回滚
20         }
21         return rows == 1;
22     }
23 }

在执行 DBHelper.update() 方法以后,我故意抛出了一个 RuntimeException,我想看看事务能否回滚,也就是那条 insert 语句没有生效。

做了一个单元测试,测了一把,果然报错了,product 表里也没有插入任何数据。

看来事务管理功能的确生效了,那么,我是如何实现 @Transaction 这个注解所具有的功能?请接着往下看,下面的才是精华所在。

一开始我修改了 DBHelper 的代码:

01 public class DBHelper {
02  
03     private static final BasicDataSource ds = new BasicDataSource();
04     private static final QueryRunner runner = new QueryRunner(ds);
05  
06     // 定义一个局部线程变量(使每个线程都拥有自己的连接)
07     private static ThreadLocal<Connection> connContainer = new ThreadLocal<Connection>();
08  
09     static {
10         System.out.println("Init DBHelper...");
11  
12         // 初始化数据源
13         ds.setDriverClassName(ConfigHelper.getStringProperty("jdbc.driver"));
14         ds.setUrl(ConfigHelper.getStringProperty("jdbc.url"));
15         ds.setUsername(ConfigHelper.getStringProperty("jdbc.username"));
16         ds.setPassword(ConfigHelper.getStringProperty("jdbc.password"));
17         ds.setMaxActive(ConfigHelper.getNumberProperty("jdbc.max.active"));
18         ds.setMaxIdle(ConfigHelper.getNumberProperty("jdbc.max.idle"));
19     }
20  
21     // 获取数据源
22     public static DataSource getDataSource() {
23         return ds;
24     }
25  
26     // 开启事务
27     public static void beginTransaction() {
28         Connection conn = connContainer.get();
29         if (conn == null) {
30             try {
31                 conn = ds.getConnection();
32                 conn.setAutoCommit(false);
33             catch (Exception e) {
34                 e.printStackTrace();
35             finally {
36                 connContainer.set(conn);
37             }
38         }
39     }
40  
41     // 提交事务
42     public static void commitTransaction() {
43         Connection conn = connContainer.get();
44         if (conn != null) {
45             try {
46                 conn.commit();
47                 conn.close();
48             catch (Exception e) {
49                 e.printStackTrace();
50             finally {
51                 connContainer.remove();
52             }
53         }
54     }
55  
56     // 回滚事务
57     public static void rollbackTransaction() {
58         Connection conn = connContainer.get();
59         if (conn != null) {
60             try {
61                 conn.rollback();
62                 conn.close();
63             catch (Exception e) {
64                 e.printStackTrace();
65             finally {
66                 connContainer.remove();
67             }
68         }
69     }
70  
71     ...
72  
73     // 执行更新(包括 UPDATE、INSERT、DELETE)
74     public static int update(String sql, Object... params) {
75         // 若当前线程中存在连接,则传入(用于事务处理),否则将从数据源中获取连接
76         Connection conn = connContainer.get();
77         return DBUtil.update(runner, conn, sql, params);
78     }
79 }

首先,我将 Connection 放到 ThreadLocal 容器中了,这样每个线程之间对 Connection 的访问就是隔离的了(不会共享),保证了线程安全。

然后,我增加了几个关于事务的方法,例如:beginTransaction()、commitTransaction()、rollbackTransaction(),这三个方法中的代码非常重要,一定要细看!我就不解释了。 

最后,我修改了 update() 方法,先从 ThreadLocal 中拿出 Connection,然后传入到 DBUtil.update() 方法中。注意:有可能从 ThreadLocal 中根本拿不到 Connection,因为此时的 Connection 是从 DataSource 中获取的(这是非事务的情况),只要执行了 beginTransaction() 方法,就会从 DataSource 中获取一个 Connection,然后将事务自动提交功能关闭,最后往 ThreadLocal 中放入一个 Connection。

提示:对 ThreadLocal 不太理解的朋友们,可阅读这篇博文《ThreadLocal 那点事儿》。

那问题来了,DBUtil 又是如何处理事务的呢?我对 DBUtil 是这样修改的:

01 public class DBUtil {
02  
03     ...
04  
05     // 更新(包括 UPDATE、INSERT、DELETE,返回受影响的行数)
06     public static int update(QueryRunner runner, Connection conn, String sql, Object... params) {
07         int result = 0;
08         try {
09             if (conn != null) {
10                 result = runner.update(conn, sql, params);
11             else {
12                 result = runner.update(sql, params);
13             }
14         catch (SQLException e) {
15             e.printStackTrace();
16         }
17         return result;
18     }
19 }

这里,我首先对传入进来的 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() 方法呢?又是在哪里调用的呢?

最简单又最直接的方式莫过于此:

01 @Bean
02 public class ProductServiceImpl extends BaseService implements ProductService {
03  
04     ...
05  
06     public boolean createProduct(Map<String, Object> productFieldMap) {
07         int rows = 0;
08         try {
09             // 开启事务
10             DBHelper.beginTransaction();
11  
12             String sql = SQLHelper.getSQL("insert.product");
13             Object[] params = {
14                 productFieldMap.get("productTypeId"),
15                 productFieldMap.get("productName"),
16                 productFieldMap.get("productCode"),
17                 productFieldMap.get("price"),
18                 productFieldMap.get("description")
19             };
20             rows = DBHelper.update(sql, params);
21         catch (Exception e) {
22             // 回滚事务
23             DBHelper.rollbackTransaction();
24  
25             e.printStackTrace();
26             throw new RuntimeException();
27         finally {
28             // 提交事务
29             DBHelper.commitTransaction();
30         }
31         return rows == 1;
32     }
33 }

但这样写,总感觉太累赘,以后凡是需要考虑事务问题的,都要用一个 try...catch...finally 语句来处理,还要手工调用那些 DBHelper.xxxTransaction() 方法。对于开发人员而言,简直这就像噩梦!

这里就要用到一点设计模式了,我选择了“Proxy 模式”,就是“代理模式”,说准确一点应该是“动态代理模式”。

提示:对 Proxy 不太理解的朋友,可阅读这篇博文《Proxy 那点事儿》。

我想把一头一尾的代码都放在 Proxy 中,这里仅保留最核心的逻辑。代理类会自动拦截到 Service 类中所有的方法,先判断该方法是否带有 @Transaction 注解,如果有的话,就开启事务,然后调用方法,最后提交事务,遇到异常还要回滚事务。若没有 @Transaction 注解呢?什么都不做,直接调用目标方法即可。

这就是我的思路,下面看看这个动态代理类是如何实现的吧:

01 public class TransactionProxy implements MethodInterceptor {
02  
03     private static TransactionProxy instance = new TransactionProxy();
04  
05     private TransactionProxy() {
06     }
07  
08     public static TransactionProxy getInstance() {
09         return instance;
10     }
11  
12     @SuppressWarnings("unchecked")
13     public <T> T getProxy(Class<T> cls) {
14         return (T) Enhancer.create(cls, this);
15     }
16  
17     @Override
18

你可能感兴趣的:(声明式事务原理)