bilibili up01-Transaction failure
标题:快进来看看你写的代码,事务是不是坑了别人!!
你的程序有问题?
当我还是一个初出茅庐的小程序猿的时候,产品经理给我提了一个需求:
现在有两张表:分别是订单表和订单详情表。在订单保存的时候,订单表一定要落库数据。订单详情表保存的时候如果有异常,则回滚。不能影响订单表的数据。
我一气呵成地在键盘上飞舞,只花了30分钟就完成需求。甚至嘴角上扬,心里想着:就这,我真是天才~
在我摸鱼了一个上午之后,下午美丽漂亮的测试小姐姐过来跟我说:靓仔,我刚才测试订单保存,订单表没有数据哦,你程序是不是有bug?
我开始检查我的代码,我在保存订单表和订单详情表的时方法上都加了@Transactional
注解。当进入了保存订单的逻辑,如果保存订单详情的方法抛出了异常也会被捕获,都try-catch住了。最后得出结论:没毛病呀,我真是个天才~。
我的程序有bug?
此时我review完代码之后,我的底气十分强硬,怀疑是测试小姐姐的环境有问题,或者是测试姿势有问题。站起来对测试小姐姐说:再来一笔试试,我盯着日志呢。
当看到结果的时候,我脸打得真疼。默默坐下来,开始排查问题。
Talk is cheap. Show me the code
为了方便演示,我搭建了一个最小的原型。开始展示~~
我们CRUD BOY,精通Controller+service+db,三层三明治结构,迅速完成演示项目的搭建。
- 搭建
OrderController.java
,直接提供Http服务调用。
|
- 核心逻辑都在
OrderService.java
中
我们先过下整体的逻辑:
标号1和标号2:是我们保存订单和订单详情的入口。分别加上@Transactional注解,并且加上了传播属性的设置(propagation)
标号3:设置传播属性。REQUIRED:必须要有事务,事务不存在,则新建一个。REQUIRES_NEW:创建新事务,如果存在当前的事务,则暂停该事务。
标号4:对于保存订单详情如果抛出异常,捕获异常,不影响保存订单主流程。
标号5:模拟业务抛出异常。
- 订单表和订单详情表结构:
通过上面的操作,我们进行了场景铺垫,代码复线,表结构。我们先说下我们期待的结果。
order_info表有一条数据,order_exta表没有数据 |
那么接下来,我们启动服务,调用http服务,结果如下:
和预期结果不同,这脸打得真疼,为什么呢?order_extra会插入数据成功呢?这不会是spring的bug吧?
- 看到这里,大家可以先回想一下自己在项目中是否看到过这样的代码?
需要事务处理的业务逻辑,在当前bean中,直接调用当前bean的内部事务方法。
如果你的回答是,那么老板,如果这个代码是别人写的,那么你踩坑了。
如果这个代码是你写的,那老板你留坑了。
- 如果你去请教你们大佬,这时候你们大佬就会告诉你
听哥一句劝:在bean中不要直接调用或者使用this调用,某个被@Transactional注解标注的方法。this下@Transactional注解是不生效的
@Transactional why
这里我们先说结论:Spring的事务是基于AOP实现的,AOP是基于动态代理实现的。所以@Transactional注解如果想要生效,那么其调用方,需要是被Spring动态代理后的类。
前面我们已经观察到现象了,我们现在深入原理,看本质。
可以看出,这个this并不是动态代理的子类对象,而是一个原始对象,所以this调用的doOrderExtra()方法虽然加了@Transactional注解,但是无法却通过动态代理来增强,从而导致事务失效。
现在我们已经知道事务失效的原因,是因为我们没有获取到调用方动态代理的类。
@Transactional fix it
这里我介绍4种方案:
方案一:业务搬迁,也是我们最常用的。
将doOrderExtra()的业务逻辑,迁移到其他bean。然后在注入目标bean。
方案二:自己注入自己:不建议,但是可以让事务生效。
可以看到我们注入orderService自己,是被CGLIB动态代理增加后的类了。
方案三:注入ApplicationContext
通过ApplicationContext获取bean。
方案四:使用AopContext获取被代理的bean
这里需要注意一点,我们需要开启:@EnableAspectJAutoProxy
我们提出了问题,并且解决了问题。想着第一期视频就这样吧,完结撒花,下期再见~
但是Spring声明事务的坑还有很多,我们再一起看下吧。
总结-扩展
Spring 声明式事务,可能遇到的三类坑,包括:
第一,因为配置不正确,导致方法上的事务没生效。我们务必确认调用 @Transactional 注解标记的方法是 public 的,并且是通过 Spring 注入的 Bean 进行调用的。
第二,因为异常处理不正确,导致事务虽然生效但出现异常时没回滚。Spring 默认只会对标记 @Transactional 注解的方法出现了 RuntimeException 和 Error 的时候回滚,如果我们的方法捕获了异常,那么需要通过手动编码处理事务回滚。如果希望 Spring 针对其他异常也可以回滚,那么可以相应配置 @Transactional 注解的 rollbackFor 和 noRollbackFor 属性来覆盖其默认设置。
第三,如果方法涉及多次数据库操作,并希望将它们作为独立的事务进行提交或回滚,那么我们需要考虑进一步细化配置事务传播方式,也就是 @Transactional 注解的 Propagation 属性。