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 应用系统,那么这类复杂的事务处理情况又会有多少呢?所以我暂时就此打住了,我的直觉告诉我,深入下去将一定是一个无底洞。

我想有必要先听听大家的想法,避免走弯路的最佳方式就是及时沟通。

原文地址:https://www.cnblogs.com/qwangwei/p/5029192.html