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

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

你好,我是朱晔。今天,我来和你聊聊业务代码中与数据库事务相关的坑。
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
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结
仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Java 业务开发常见错误 100 例》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(69)

  • 最新
  • 精选
  • 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)即可实现,通过内省动态给属性赋值。

    作者回复: 👍🏻

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

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

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

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

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

    作者回复: 没错

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

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

    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事务混合在一起,加锁和业务在一个事务里了,锁就没用了,因为每个事务里,都认为自己拿到了锁。

    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、在一个事务内肯定可以看到事务之前做的修改

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

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

    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

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

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

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