6

spring声明式事务(@Transactional)开发常犯的几个错误及解决办法 - 梦在旅途

 7 months ago
source link: https://www.cnblogs.com/zuowj/p/18005149
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

目前JAVA的微服务项目基本都是SSM结构(即:springCloud +springMVC+Mybatis),而其中Mybatis事务的管理也是交由spring来管理,大部份都是使用声明式事务(@Transactional)来进行事务一致性的管理,然后在实际日常开发过程中,发现很多开发同学都用错了spring声明式事务(@Transactional)或者说使用非常不规范,导致出现各种事务问题。我(梦在旅途)今天周日休息,花了几个小时把目前我已知的开发常犯的几个错误都列举出来并逐一分析根本原因同时针对原因给出解决方案及示例,希望能帮助到广大JAVA开发者。

1. 事务不生效

  • 问题现象:明明有事务注解,在事务方法内部有抛错,但事务却没有回滚,该执行的SQL都执行了。示例代码如下:(doInsert方法是有事务注解的)

    /**
     * @author zuowenjun
     * @see wwww.zuowenjun.cn
     */
    @Service
    public class DemoUserService {
        //... ...
       public DemoUser doGet() {
            try {
                doInsert(1);
            } catch (Exception ex) {
                System.out.println("insert error: " + ex.toString());
            }
            return demoUserMapper.get(1);
        }
        
            @Transactional
        public int doInsert(int id) {
            DemoUser user = new DemoUser(id, "zs", 18, new BigDecimal("8888.88"),
                    "shenzhen,cn", new Timestamp(System.currentTimeMillis()), new Timestamp(System.currentTimeMillis()));
    
            int result = demoUserMapper.insert(user);
    
            throw new RuntimeException("mock insert ex");  //模拟抛错
    
     		return result;
        }
    }
        
    //演示调用,最终打印出了ID为1的那条记录,事务并没有回滚
    DemoUser result = demoUserService.doGet();
    System.out.println(result != null ? result.toString() : "none");
    
  • 根本原因:没有执行事务AOP切面,因为在BEAN方法内部直接调用另一个公开的事务方法,是原生的方法之间调用,并非是被代理后的BEAN方法,所以SPRING事务注解在这种情况下失去作用。

  • 解决方案:不论是在BEAN外部或BEAN方法内部,要确保一定是调用代理BEAN的公开事务方法,确保调用事务方法有被SPRING事务拦截处理,示例代码如下:【在BEAN内部则需要先注入BEAN本身的代理BEAN实例(有很多中获取当前BEAN的代理BEAN方案,在此不细说),然后通过代理BEAN调事务方法即可。】

    /**
     * @author zuowenjun
     * @see wwww.zuowenjun.cn
     */
    @Service
    public class DemoUserService {
        @Autowired
        @Lazy //加上这个,是防止循环自依赖
        private DemoUserService selfService; //注入自己的代理BEAN实例
    
            //... ...
        
        public DemoUser doGet() {
            try {
               selfService.doInsert(1); //这里改为使用代理BEAN调doInsert的事务方法,确保走切面
            } catch (Exception ex) {
                System.out.println("insert error: " + ex.toString());
            }
            return demoUserMapper.get(1);
        }
    
        @Transactional
        public int doInsert(int id) {
            DemoUser user = new DemoUser(id, "zs", 18, new BigDecimal("8888.88"),
                    "shenzhen,cn", new Timestamp(System.currentTimeMillis()), new Timestamp(System.currentTimeMillis()));
    
            int result = demoUserMapper.insert(user);
    
            throw new RuntimeException("mock insert ex");
    
    //        return result;
        }
    }
    
    //演示调用,最终打印出了none,说明事务有回滚,无法查出ID为1的那个记录
    DemoUser result = demoUserService.doGet();
    System.out.println(result != null ? result.toString() : "none");
    

2. 事务提交报错

  • 问题现象:事务方法内有catch住错误,但却无法正常提交事务,报错:Transaction rolled back because it has been marked as rollback-only,示例代码如下:

    /**
     * @author zuowenjun
     * @see wwww.zuowenjun.cn
     */
    @Service
    public class DemoUserService {
        @Autowired
        @Lazy //加上这个,是防止循环自依赖
        private DemoUserService selfService; //注入自己的代理BEAN实例
    
            //... ...
            
        @Transactional
        public DemoUser doGet() {
            try {
               selfService.doInsert(1);
            } catch (Exception ex) { //有catch错误,但当doGet返回时却报错了
                System.out.println("insert error: " + ex.toString());
            }
            return demoUserMapper.get(1);
        }
    
        @Transactional
        public int doInsert(int id) {
            DemoUser user = new DemoUser(id, "zs", 18, new BigDecimal("8888.88"),
                    "shenzhen,cn", new Timestamp(System.currentTimeMillis()), new Timestamp(System.currentTimeMillis()));
    
            int result = demoUserMapper.insert(user);
    
            if (id==1) {
                throw new RuntimeException("mock insert ex");
            }
            return result;
        }
    }
    
    //演示调用,最终有报错:Transaction rolled back because it has been marked as rollback-only
    DemoUser result = demoUserService.doGet();
    System.out.println(result != null ? result.toString() : "none");
    
  • 根本原因:事务继承“惹的祸”【事务传播特性】,入口事务方法内部再调其他事务方法,其他事务方法若有抛错则会在方法返回时被事务切面标记当前事务仅能回滚,若最后入口事务方法执行完成并想提交事务时却因为事务是继承的且有被标记为仅能回滚后则只能报错

  • 解决方案:避免事务继承 或 确保事务方法内部不再调用其他事务方法(即:事务方法变成普通方法,小技巧参照我之前文章:任何Bean通过实现ProxyableBeanAccessor接口即可获得动态灵活的获取代理对象或原生对象的能力 - 梦在旅途 - 博客园 (cnblogs.com)),示例代码如下:

    /**
     * @author zuowenjun
     * @see wwww.zuowenjun.cn
     */
    @Service
    public class DemoUserService {
        @Autowired
        @Lazy //加上这个,是防止循环自依赖
        private DemoUserService selfService; //注入自己的代理BEAN实例
    
            //... ...
            
        @Transactional
        public DemoUser doGet() {
            try {
               selfService.doInsert(1);
               // doInsert(1);  方案二:内部直接doInsert方法,此时是原生方法调用,不走事务切面,也就不会触发事务记录的情况
            } catch (Exception ex) { //有catch错误
                System.out.println("insert error: " + ex.toString());
            }
            selfService.doInsert(2);
            return demoUserMapper.get(2);
        }
    
        @Transactional(propagation = Propagation.REQUIRES_NEW) //方案一:这里加上REQUIRES_NEW、或NOT_SUPPORTED,确保不继承外部事务即可
        public int doInsert(int id) {
            DemoUser user = new DemoUser(id, "zs", 18, new BigDecimal("8888.88"),
                    "shenzhen,cn", new Timestamp(System.currentTimeMillis()), new Timestamp(System.currentTimeMillis()));
    
            int result = demoUserMapper.insert(user);
    
            if (id==1) {
                throw new RuntimeException("mock insert ex");
            }
            return result;
        }
    }
    
    //演示调用,最终正确打印了ID为2的记录,说明虽然插入ID=1的记录失败了,但插入2的记录是正确的,入口事务有正确的提交
    DemoUser result = demoUserService.doGet();
    System.out.println(result != null ? result.toString() : "none");
    

3. 事务不回滚

  • 问题现象:事务方法内部有报错,但事务却仍提交了,示例代码如下:

      /**代码片段
     * @author zuowenjun
     * @see wwww.zuowenjun.cn
     */
    
    //第一种情况:错误被catch住了
    @Transactional
        public DemoUser doGet1() {
            try {
                doInsert(1); //doInsert原生调用,代码看似有事务,实际此时无事务,也就不存在事务回滚的情况
            } catch (Exception ex) { //catch错误,doGet事务正常提交
                System.out.println("insert error: " + ex.toString());
            }
            selfService.doInsert(2);
            return demoUserMapper.get(2);
        }
    
    //第二种情况:外层报错,内层事务正常提交
    	@Transactional
        public DemoUser doGet2() {
            selfService.doInsert(2); //doInsert切面调用,有事务且单独事务,执行完即提交
            throw new RuntimeException("mock doGet ex");//这里抛错不影响doInsert的提交
            return demoUserMapper.get(2);
        }
    
    
        @Transactional(propagation = Propagation.REQUIRES_NEW)
        public int doInsert(int id) {
            DemoUser user = new DemoUser(id, "zs", 18, new BigDecimal("8888.88"),
                    "shenzhen,cn", new Timestamp(System.currentTimeMillis()), new Timestamp(System.currentTimeMillis()));
    
            int result = demoUserMapper.insert(user);
    
            if (id==1) {
                throw new RuntimeException("mock insert ex");
            }
    
            return result;
        }
    
    //演示调用,第1种情况
    DemoUser result = demoUserService.doGet1();
    System.out.println(result != null ? result.toString() : "none");
    
    //演示调用,第2种情况
    DemoUser result = demoUserService.doGet2();
    System.out.println(result != null ? result.toString() : "none");
    
  • 根本原因:一是错误被catch住了,这种情况下事务切面认为是正常的则会正常执行提交事务,二是根本就没有事务或事务并非同一个事务(与事务传播特性有关),这种情况就好理解,没事务就不存在事务提交(方法中的每个SQL即为一个小事务,执行即提交),若是事务方法内部有嵌套调用其他事务方法,入口的外层事务会受内部其他事务方法的影响,反之若其他事务方法与外层事务不是同一个事务,那么外层事务有报错并不会影响内部其他事务方法

    • 这里还补充一种特殊情况,若在事务方法中异步调用其他事务方法(@Async 或线程池直接调用等情况),那么由于不在同一个线程上下文,即使默认是继承的传播特性也无变成2个不相干的事务各自执行,异步事务方法的报错不会影响外层的事务方法
  • 解决方案:若需保证事务的完整性,需确保若有异常一定要抛错而非catch错误,另外需确保一定有事务,当事务方法内部有嵌套调用其他事务方法时,若希望被调用的事务方法与当前事务保持一致,那么就应确保是事务继承,否则就说明可以允许局部事务不一致,示例代码如下:

     /**代码片段
     * @author zuowenjun
     * @see wwww.zuowenjun.cn
     */
    
    @Transactional
        public DemoUser doGet() {
            doInsert(1);//不要catch,若catch后记录日志后再抛出,总之一定要抛错
            selfService.doInsert(1);//这种也可以,当doInsert报错,则doInsert与doGet方法均回滚(本质是同一个事务)
            selfService.doInsert(2);
            return demoUserMapper.get(2);
        }
    
        @Transactional(propagation = Propagation.REQUIRED) //若需与外层事务这一致,这里建议采用REQUIRED的传播特性
        public int doInsert(int id) {
            DemoUser user = new DemoUser(id, "zs", 18, new BigDecimal("8888.88"),
                    "shenzhen,cn", new Timestamp(System.currentTimeMillis()), new Timestamp(System.currentTimeMillis()));
    
            int result = demoUserMapper.insert(user);
    
            if (id==1) {
                throw new RuntimeException("mock insert ex");
            }
    
            return result;
        }
    
  • 问题现象:执行SQL有报死锁,示例代码如下:

      /**代码片段
     * @author zuowenjun
     * @see wwww.zuowenjun.cn
     */
    
    @Transactional
        public DemoUser doGetX() {
            selfService.doInsert(1);
            DemoUser user=selfService.get(1);
            user.setName("xxx");
            update(user); //这里是原生方法调用,等同于在doGetX同一个事务方法内部执行
    
            user.setName("xxx2");
            selfService.update(user); //这里新开事务调用,由于doGetX中已经有调用update(id=1)且事务还未提交,故这里需要等待doGetX事务提交以便释放锁,而doGetX事务则因为这里等待无法往下执行,形成事务循环自依赖了
            return demoUserMapper.get(1);
        }
        
        @Transactional(propagation = Propagation.REQUIRES_NEW) //这里新开事务
        public int update(DemoUser demoUser) {
            return demoUserMapper.update(demoUser);
        }
        
    //演示调用,执行报错,不同DB的报错提示可能有所不同
    DemoUser result = demoUserService.doGetX();
    System.out.println(result != null ? result.toString() : "none");    
    
  • 根本原因:事务被循环自依赖了,再准确的说就是同一个记录被2个事务相互依赖,导致相互等待获取锁

  • 解决方案:避免事务被循环自依赖,示列代码如下:

     /**代码片段
     * @author zuowenjun
     * @see wwww.zuowenjun.cn
     */
    
    //优化一
        @Transactional
        public DemoUser doGetX() {
            selfService.doInsert(1);
            DemoUser user=selfService.get(1);
            user.setName("xxx");
            update(user); //这里是原生方法调用,等同于在doGetX同一个事务方法内部执行
    
            user.setName("xxx2");
            update(user); //这里也改为原生方法调用,等同于在doGetX同一个事务方法内部执行
            return demoUserMapper.get(1);
        }
        
         //优化二
        @Transactional
        public DemoUser doGetX() {
            selfService.doInsert(1);
            DemoUser user=selfService.get(1);
            user.setName("xxx");
            selfService.update(user); //这里是代理BEAN方法调用,新开事务,直接执行并提交,与doGetX事务互不影响
    
            user.setName("xxx2");
            selfService.update(user); //这里是代理BEAN方法调用,新开事务,直接执行并提交,与doGetX事务互不影响
            return demoUserMapper.get(1);
        }
        
        @Transactional(propagation = Propagation.REQUIRES_NEW) //这里新开事务
        public int update(DemoUser demoUser) {
            return demoUserMapper.update(demoUser);
        }
        
    //演示调用,执行报错,不同DB的报错提示可能有所不同
    DemoUser result = demoUserService.doGetX();
    System.out.println(result != null ? result.toString() : "none");    
    

5. 在事务提交后回调事件方法中开事务不生效

  • 问题现象:在事务提交后回调事件方法中【即:afterCommit】开启事务不生效(即:添加了@Transactional,也执行了代理方法的调用,但就像没有事务一样,出现报错事务不回滚,也无法在事务方法中再次注册事务提交后回调事务件方法),示例代码如下:

     /**代码片段
     * @author zuowenjun
     * @see wwww.zuowenjun.cn
     */
    
    @Transactional
        public DemoUser doGetX() {
            doInsert(1);
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
                @Override
                public void afterCommit() {
                    selfService.doInsert(2);//走切面调用,确保执行代理的事务方法,但实际还是无事务,报错也不会回滚
                }
            });
    
            return demoUserMapper.get(1);
        }
        
        @Transactional(propagation = Propagation.REQUIRED)
        public int doInsert(int id) {
            DemoUser user = new DemoUser(id, "zs", 18, new BigDecimal("8888.88"),
                    "shenzhen,cn", new Timestamp(System.currentTimeMillis()), new Timestamp(System.currentTimeMillis()));
    
            int result = demoUserMapper.insert(user);
    
            if (id==2) {
                throw new RuntimeException("mock insert ex");
            }
    
            return result;
        }
        
        //演示调用:虽然doGetX有报错,但最终doInsert方法均有执行,且都能查出ID=1 与2的记录
            try {
                DemoUser result = demoUserService.doGetX();
                System.out.println(result != null ? result.toString() : "none");
            }catch (Exception e){
                System.out.println("error " + e.toString());
            }
    
            DemoUser result1 =demoUserService.get(1);
            System.out.println(result1 != null ? result1.toString() : "none");
    
            DemoUser result2 =demoUserService.get(2);
            System.out.println(result2 != null ? result2.toString() : "none");
    
  • 根本原因:在事务提交后回调事件方法中【即:afterCommit】,spring事务的管理状态仍保留(即:仍是事务激活状态)但DB事务其实已提交,当回调方法中又遇到有事务注解的方法时且判断已有事务(即spring事务的管理状态是激活状态transactionActive=true)时,若是默认继承状态则不会再开启新事务,仅复用DB连接

  • 解决方案:在事务提交后回调事件方法中【即:afterCommit】开启新事务(即:传播特性为:REQUIRES_NEW) 或者 执行前强制清除事务状态【需要编写事务状态清除工具类】,示例代码如下:

     /**代码片段
     * @author zuowenjun
     * @see wwww.zuowenjun.cn
     */
    
    @Transactional
        public DemoUser doGetX() {
    	    TxManagerUtils.clearTxStatus();//方案二:通过事务状态清除工具类注册事务回调后首先清除事务状态,二选其一即可
            doInsert(1);
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
                @Override
                public void afterCommit() {
                    selfService.doInsert(2);//走切面调用,确保执行代理的事务方法
                }
            });
    
            return demoUserMapper.get(1);
        }
        
        @Transactional(propagation = Propagation.REQUIRES_NEW) //方案一:这里强制开启新事务,二选其一即可
        public int doInsert(int id) {
            DemoUser user = new DemoUser(id, "zs", 18, new BigDecimal("8888.88"),
                    "shenzhen,cn", new Timestamp(System.currentTimeMillis()), new Timestamp(System.currentTimeMillis()));
    
            int result = demoUserMapper.insert(user);
    
            if (id==2) {
                throw new RuntimeException("mock insert ex");
            }
    
            return result;
        }
        
        //演示调用:虽然doGetX有报错,但只能查出ID=1的记录,ID=2由于报错事务回滚了,说明afterCommit中再开启事务是OK的
            try {
                DemoUser result = demoUserService.doGetX();
                System.out.println(result != null ? result.toString() : "none");
            }catch (Exception e){
                System.out.println("error " + e.toString());
            }
    
            DemoUser result1 =demoUserService.get(1);
            System.out.println(result1 != null ? result1.toString() : "none");
    
            DemoUser result2 =demoUserService.get(2);
            System.out.println(result2 != null ? result2.toString() : "none");
    

    事务状态清除工具类如下:

    package org.springframework.jdbc.datasource; //必需放在这个包目录下,因为connectionHolder.setTransactionActive 是protected方法
    
    
    import com.example.springwebapp.utils.SpringUtils;
    import org.springframework.transaction.support.TransactionSynchronizationAdapter;
    import org.springframework.transaction.support.TransactionSynchronizationManager;
    
    import javax.sql.DataSource;
    
    /**
     * @author zuowenjun
     * @see wwww.zuowenjun.cn
     */
    public class TxManagerUtils {
    
        //建议在每个事务方法的第一行调用,避免事务方法内部中途若有其他方法需要注册事务提交后回调方法
        public static void clearTxStatus() {
            DataSource dataSource = SpringUtils.getBean(DataSource.class);
            ConnectionHolder connectionHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
    
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
                @Override
                public int getOrder() {
                    return Integer.MIN_VALUE; //确保最先执行
                }
    
                @Override
                public void afterCommit() {
                    doClearTxStatus(); //第一个回调事件中先清除事务状态
                }
    
                @Override
                public void afterCompletion(int status) {
                    TransactionSynchronizationManager.bindResource(dataSource, connectionHolder); //恢复DB连接绑定,避免执行事务清理时报错
                }
            });
        }
    
        private static void doClearTxStatus() {
            DataSource dataSource = SpringUtils.getBean(DataSource.class);
            TransactionSynchronizationManager.setActualTransactionActive(false); //设置事务状态为非激活
            ConnectionHolder connectionHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
            connectionHolder.setTransactionActive(false);//设置事务状态为非激活
            TransactionSynchronizationManager.unbindResource(dataSource); //暂时解绑DB连接
        }
    
    }
    
    

    注:后面我预计还会针对spring事务这块进行其他方面的分享(比如:spring事务在多数据源中切换数据源不生效、事务隔离级别下的并发处理等),敬请期待,原创不易,若有不足欢迎指出,谢谢!

    最后预祝大家2024年龙年大吉,新春快乐!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK