Java 业务开发常见错误 100 例
朱晔
贝壳金服资深架构师
52944 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 48 讲
代码篇 (23讲)
Java 业务开发常见错误 100 例
15
15
1.0x
00:00/00:00
登录|注册

06 | 20%的业务代码的Spring声明式事务,可能都没处理正确

使用AspectJ配合lombok可能会遇到一些问题
MyBatis配合Spring的声明式事务也需要注意相同的问题
建议在单元测试时覆盖多的异常场景
正确配置事务可以提高业务项目的健壮性
通过设置Propagation属性来细化配置事务传播方式
需要手动设置事务处于回滚状态或配置rollbackFor来覆盖默认设置
默认情况下,出现RuntimeException或Error时,Spring才会回滚事务
只有异常传播出了标记了@Transactional注解的方法,事务才能回滚
Debug日志可以帮助了解Spring事务实现的细节
通过代理过的类从外部调用目标方法才能生效
@Transactional注解只在public方法上生效
思考与讨论
重点回顾
请确认事务传播配置是否符合自己的业务逻辑
事务即便生效也不一定能回滚
Spring声明式事务可能没有生效
事务处理细节对业务系统的重要性
20%的业务代码的Spring声明式事务,可能都没处理正确

该思维导图由 AI 生成,仅供参考

你好,我是朱晔。今天,我来和你聊聊业务代码中与数据库事务相关的坑。
Spring 针对 Java Transaction API (JTA)、JDBC、Hibernate 和 Java Persistence API (JPA) 等事务 API,实现了一致的编程模型,而 Spring 的声明式事务功能更是提供了极其方便的事务配置方式,配合 Spring Boot 的自动配置,大多数 Spring Boot 项目只需要在方法上标记 @Transactional 注解,即可一键开启方法的事务性配置。
据我观察,大多数业务开发同学都有事务的概念,也知道如果整体考虑多个数据库操作要么成功要么失败时,需要通过数据库事务来实现多个操作的一致性和原子性。但,在使用上大多仅限于为方法标记 @Transactional,不会去关注事务是否有效、出错后事务是否正确回滚,也不会考虑复杂的业务代码中涉及多个子业务逻辑时,怎么正确处理事务。
事务没有被正确处理,一般来说不会过于影响正常流程,也不容易在测试阶段被发现。但当系统越来越复杂、压力越来越大之后,就会带来大量的数据不一致问题,随后就是大量的人工介入查看和修复数据。
所以说,一个成熟的业务系统和一个基本可用能完成功能的业务系统,在事务处理细节上的差异非常大。要确保事务的配置符合业务功能的需求,往往不仅仅是技术问题,还涉及产品流程和架构设计的问题。今天这一讲的标题“20% 的业务代码的 Spring 声明式事务,可能都没处理正确”中,20% 这个数字在我看来还是比较保守的。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

本文通过具体案例和技术细节,提醒读者在业务代码中正确处理Spring声明式事务,以避免因事务处理不当而导致的业务逻辑Bug。作者指出,大多数开发人员对事务概念有所了解,但在实际应用中往往只是简单地在方法上标记@Transactional注解,而忽略了事务是否有效、异常情况下是否正确回滚等细节。文章展示了在使用@Transactional注解时可能出现的问题,并提出了解决方案。通过示例和技术细节,读者可以了解到如何正确处理Spring声明式事务,避免因事务处理不当而导致的业务逻辑Bug。文章内容涉及事务传播配置、异常处理、事务回滚等方面,对于开发人员在实际项目中遇到的事务处理问题具有一定的指导意义。文章还提到了在使用MyBatis配合Spring的声明式事务时需要注意的问题,以及针对private方法启用事务时的AOP配置建议。整体而言,本文为开发人员提供了关于Spring声明式事务处理的实用指南,有助于他们在项目开发中避免常见的事务处理陷阱。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Java 业务开发常见错误 100 例》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(70)

  • 最新
  • 精选
  • Darren
    AspectJ与lombok,都是字节码层面进行增强,在一起使用时会有问题,根据AspectJ维护者Andy Clement的当前答案是由于ECJ(Eclipse Compiler for Java)软件包存在问题在AspectJ编译器基础结构中包含和重命名。 解决问题可以参考下面连接: http://aspectj.2085585.n4.nabble.com/AspectJ-with-Lombok-td4651540.html https://stackoverflow.com/questions/41910007/lombok-and-aspectj 分享一个使用lombok的坑: 之前为了set赋值方便,在VO或者DTO上使用了@Accessors(chain=true),这样就可以链式赋值,但是在动态通过内省获取set方法进行赋值时,是获取不到对应的set方法,因为默认的set方法返回值是void,但是加了@Accessors(chain=true)之后,set方法的返回值变成了this,这样通过内省就获取到对应的set方法了,通过去掉@Accessors(chain=true)即可实现,通过内省动态给属性赋值。

    作者回复: 👍🏻

    2020-03-23
    8
    59
  • hanazawakana
    否则只有定义在 public 方法上的 @Transactional 才能生效。这里一定要用public吗,用protected不行吗,protected在子类中应该也可见啊,是因为包不同吗

    作者回复: 这个问题很好,首先JDK动态代理肯定是不行的只能是public,理论上CGLIB方式的代理是可以代理protected方法的,不过如果支持,那么意味着事务可能会因为切换代理实现方式表现不同,大大增加出现Bug的可能性,我觉得为了一致性所以Spring考虑只支持public,这是最好的。

    2020-03-21
    4
    47
  • Seven.Lin澤耿
    我还遇到一个坑,就是子方法使用了REQUIRES_NEW,但是业务逻辑需要的数据是来源于父方法的,也就是父方法还没提交,子方法获取不到。当时的解决方法是把事务隔离级别改成RC,现在回想起来,不知道这种解决方法是否正确?

    作者回复: 你说的隔离级别应该是指READ_UNCOMMITTED。我不认为这是很好的解决方案,子方法内需要依赖的数据来自父方法,可以方法传值,而不是用这种隔离级别。

    2020-03-22
    42
  • 看不到de颜色
    Spring默认事务采用动态代理方式实现。因此只能对public进行增强(考虑到CGLib和JDKProxy兼容,protected也不支持)。在使用动态代理增强时,方法内调用也可以考虑采用AopContext.currentProxy()获取当前代理类。

    作者回复: 没错

    2020-03-29
    25
  • Seven.Lin澤耿
    老师,可以问一下为啥国内大多数公司使用MyBatis呢?是为了更加接近SQL吗?难倒国外业务不会遇到复杂的场景吗?

    作者回复: 1、容易上手简单 2、国内BAT大厂对于Mybatis的使用量大,影响力大 3、国内大部分项目还是面向表结构的编程,从下到上的思考方式而非OOP的思考方式

    2020-03-22
    8
    25
  • 九时四
    老师您好,有个数据库事务和spring事务的问题想请教下(我是一个入职半年的菜鸟)。 业务场景:为了实现同一个时间的多个请求,只有一个请求生效,在数据库字段上加了一个字段(signature_lock)标识锁状态。(没有使用redis锁之类的中间件,只讨论数据库事务和Spring的事务,以下的请求理解为同时请求) 1.在数据库层面,通过sql语句直接操作数据库,数据库事务隔离级别为可重复读: -- 请求1 show VARIABLES like 'tx_isolation'; START TRANSACTION; select * from subscribe_info where id = 29; -- update语句只有一个请求可以执行,另一个请求在等待 update trade_deal_subscribe_info set signature_lock =1 where id = 1 and signature_lock = 0; commit; -- 请求2 show VARIABLES like 'tx_isolation'; START TRANSACTION; select * from trade_deal_subscribe_info where id = 29; -- update语句只有一个请求可以执行,另一个请求在等待 update subscribe_info set signature_lock =1 where id = 1 and signature_lock = 0; commit; 两个请求中只有一个可以执行成功update语句,将signature_lock更新为1。 2.在代码层面按照在数据库层面的逻辑,service层的伪代码如下: public void test(ParamDto paramDto) { //取数据 Data data = getByParamDto(paramDto); // 尝试加锁,返回1表示加锁成功 Integer lockStatus = lockData(paramDto); // 加锁失败直接返回 if(!Objects.equals(1,lockStatus)){ return; } try{ // 处理业务代码,大概2到3秒 handle(); }catch(Exception e){ } finally{ // 释放锁 releaseLock(paramDto); } } 按照这样的方式,在方法上面不加注解的情况下,执行结果与在写sql的结果是一致的,两个请求只有一个可以执行成功;加上@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)之后,两个请求都可以拿到锁。 疑问是,Spring的事务和数据库的事务有什么关系,加上事务注解后,为什么和数据库的结果不一致。

    作者回复: 如果要通过数据库来实现锁,那么加锁解锁,需要是单独的事务,不能跟业务的sql事务混合在一起,加锁和业务在一个事务里了,锁就没用了,因为每个事务里,都认为自己拿到了锁。

    2020-03-21
    8
    19
  • 火很大先生
    @Transactional public int createUserRight(String name) throws IOException { try { userRepository.save(new UserEntity(name)); throw new RuntimeException("error"); } catch (Exception ex) { log.error("create user failed because {}", ex.getMessage()); TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); } return userRepository.findByName(name).size(); } 请教老师,我这种写法,控制台打出了Initiating transaction rollback 但是数据库还是存上了数据,没有回滚,是因为findByName 这个查询语句的默认commit给提交了吗

    作者回复: 需要明确几点: 1、我觉得这个事务最终是回滚的,你看到的这个查询有值,并不代表数据库有值 2、这个查询有值的原因是因为在一个事务内,此时事务并没有回滚,事务要到离开了这个createUserRight方法才会回滚(回想一下AOP原理) 3、在一个事务内肯定可以看到事务之前做的修改

    2020-04-10
    13
  • 王刚
    老师问个问题,您说得@Transactional事物回滚,只有是RuntimeException 或error时,才会回滚; 但是我在做测试时,发现@Transactional有一个rollbackFor属性,该属性可以指定什么异常回滚,如果@Transactional 不指定rollbackFor,默认得是RuntimeException?

    作者回复: 是啊,所以我们才需要设置 @Transactional(rollbackFor = Exception.class) 来不仅仅回滚RuntimeException

    2020-03-26
    2
    6
  • 汝林外史
    老师,创建主子用户那个业务,应该是子用户创建失败不影响主用户,但是主用户失败应该子用户也要回滚吧?如果是这样,那传播机制是不是应该用Propagation.NESTED

    作者回复: 理论上NESTED显然是比两个独立都事务好,NESTED因为JPA Hibernate不支持,所以这里没有采用这种方式(而且对于本例而言,主用户创建在先,如果先出异常的话后面也不会到子用户的逻辑,所以问题不大),抽空我再传一个例子上去: https://github.com/JosephZhu1983/java-common-mistakes/tree/master/src/main/java/org/geekbang/time/commonmistakes/transaction/nested

    2020-03-24
    4
    6
  • Yanni
    要注意,@Transactional 与 @Async注解不能同时在一个方法上使用, 这样会导致事物不生效。

    作者回复: 能否分析一下原因给大家分享一下?

    2020-04-10
    5
    5
收起评论
显示
设置
留言
70
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部