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

目前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;
    }

4. 死锁

  • 问题现象:执行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年龙年大吉,新春快乐!

spring声明式事务(@Transactional)开发常犯的几个错误及解决办法的更多相关文章

  1. Spring声明式事务@Transactional 详解,事务隔离级别和传播行为

    @Transactional注解支持9个属性的设置,这里只讲解其中使用较多的三个属性:readOnly.propagation.isolation.其中propagation属性用来枚举事务的传播行为 ...

  2. Spring声明式事务管理基于@Transactional注解

    概述:我们已知道Spring声明式事务管理有两种常用的方式,一种是基于tx/aop命名空间的xml配置文件,另一种则是基于@Transactional 注解.         第一种方式我已在上文为大 ...

  3. Spring 声明式事务与编程式事务详解

    本文转载自IBM开发者论坛:https://developer.ibm.com/zh/articles/os-cn-spring-trans 根据自己的学习理解有所调整,用于学习备查. 事务管理对于企 ...

  4. 深刻理解Spring声明式事务

    问题引入 Spring中事务传播有哪几种,分别是怎样的? 理解注解事务的自动配置? SpringBoot启动类为什么不需要加@EnableTransactionManagement注解? 声明式事务的 ...

  5. spring声明式事务管理总结

    事务配置 首先在/WEB-INF/applicationContext.xml添加以下内容: <!-- 配置事务管理器 --> <bean id="transactionM ...

  6. spring 声明式事务管理

    简单理解事务: 比如你去ATM机取5000块钱,大体有两个步骤:首先输入密码金额,银行卡扣掉5000元钱:然后ATM出5000元钱.这两个步骤必须是要么都执行要么都不执行.如果银行卡扣除了5000块但 ...

  7. Spring声明式事务管理基于tx/aop命名空间

    目的:通过Spring AOP 实现Spring声明式事务管理; Spring支持编程式事务管理和声明式事务管理两种方式. 而声明式事务管理也有两种常用的方式,一种是基于tx/aop命名空间的xml配 ...

  8. Spring声明式事务配置管理方法

    环境配置 项目使用SSH架构,现在要添加Spring事务管理功能,针对当前环境,只需要添加Spring 2.0 AOP类库即可.添加方法: 点击项目右键->Build Path->Add ...

  9. 161117、使用spring声明式事务抛出 identifier of an instance of

    今天项目组有成员使用spring声明式事务出现下面异常,这里跟大家分享学习下. 异常信息: org.springframework.orm.hibernate3.HibernateSystemExce ...

  10. Spring声明式事务管理与配置详解

    转载:http://www.cnblogs.com/hellojava/archive/2012/11/21/2780694.html 1.Spring声明式事务配置的五种方式 前段时间对Spring ...

随机推荐

  1. Puppeteer 入门与实战

    本文首发于 vivo互联网技术 微信公众号 链接:https://mp.weixin.qq.com/s/P-YdQPOQ9GZgjDEP7VG8ag作者:Wang Zhenzheng Puppetee ...

  2. java调用百度地图接口输入名称查经度纬度

    如何注册ak号请参考https://blog.csdn.net/weixin_42512684/article/details/115843299 package manager.tool; impo ...

  3. python之HtmlTestRunner(一)生成测试报告

    一.下载安装 windows10,cmd环境通过如下命令

  4. Go 标准库之 io.Copy 和 ioutil.ReadAll

    1. go 标准库之 io.Copy 和 ioutil.ReadAll 1.1 介绍 go 标准库中通过 ioutil.ReadAll 实现数据流的读取,io.Copy 实现数据流的读取和写入. 那两 ...

  5. 每天学五分钟 Liunx 0100 | 服务篇:进程状态

    多任务和 CPU 时间片 前面说了 Liunx 是多用户多任务的,所谓的多任务就是多个进程"同时"执行.比如,同时开多个软件(进程),对于用户来说好像每个软件(进程)都在工作,但是 ...

  6. [转帖]shell编程:变量的数值计算实践(五)

    https://www.cnblogs.com/luoahong/articles/9224495.html 算术运算符 变量的数值(整数)计算   1)(())用法:(此方法很常用)** 范例:sh ...

  7. [转帖]Jmeter连接InfluxDB2.0.4

    Jmeter连接InfluxDB2.0.4 问题描述:在用Jmeter+InfluxDB构建监控时,因为docker构建的InfluxDB的版本是2.0.4,按照网上的教程进行后端监听器的填写,但是一 ...

  8. [转帖]Docker相关的概念和原理

    https://www.jianshu.com/p/9737cbe33304 chroot chroot就是可以改变某进程的根目录,使这个程序不能访问目录之外的其他目录.Docker是利用Linux的 ...

  9. 编译打包rabbitmq然后一键部署的简单方法

    摘要 之前总结过一版,但是感觉不太全面 想着本次能够将使用中遇到的问题总结一下. 所以本次是第二版 介质下载 rabbitmq 不区分介质的打包文件 rabbitmq-server-generic-u ...

  10. Linux 界面能够出现ip地址提示的方法

    cat <<EOF >/etc/profile.d/ip.sh if [[ `tty | grep "pts"` ]]; then export PS1='['& ...