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

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

  • 相关阅读:
    mmseg4j 中文分词 1.6 版发布
    中文分词 mmseg4j 在 lucene 中的使用示例
    Solr Multicore 试用小记
    学习Javascript和Jquery必备资料
    前端开发 IE 中的常用调试工具
    jQuery学习教程(九):动画
    【jquery插件】SELECT美化,下拉框美化
    Jquery 基础实例源码分享
    关于Jquery显示隐藏的分析
    jQuery判断一个元素是否为另一个元素的子元素(或者其本身)
  • 原文地址:https://www.cnblogs.com/qwangwei/p/5029192.html
Copyright © 2020-2023  润新知