发布于 

bilibili up01-Transaction failure

标题:快进来看看你写的代码,事务是不是坑了别人!!

你的程序有问题?

当我还是一个初出茅庐的小程序猿的时候,产品经理给我提了一个需求:

现在有两张表:分别是订单表订单详情表。在订单保存的时候,订单表一定要落库数据。订单详情表保存的时候如果有异常,则回滚。不能影响订单表的数据。

我一气呵成地在键盘上飞舞,只花了30分钟就完成需求。甚至嘴角上扬,心里想着:就这,我真是天才~

sticker

在我摸鱼了一个上午之后,下午美丽漂亮的测试小姐姐过来跟我说:靓仔,我刚才测试订单保存,订单表没有数据哦,你程序是不是有bug?

image-20221027072258284

我开始检查我的代码,我在保存订单表和订单详情表的时方法上都加了@Transactional注解。当进入了保存订单的逻辑,如果保存订单详情的方法抛出了异常也会被捕获,都try-catch住了。最后得出结论:没毛病呀,我真是个天才~。

我的程序有bug?

此时我review完代码之后,我的底气十分强硬,怀疑是测试小姐姐的环境有问题,或者是测试姿势有问题。站起来对测试小姐姐说:再来一笔试试,我盯着日志呢。

当看到结果的时候,我脸打得真疼。默默坐下来,开始排查问题。

Talk is cheap. Show me the code

为了方便演示,我搭建了一个最小的原型。开始展示~~

我们CRUD BOY,精通Controller+service+db,三层三明治结构,迅速完成演示项目的搭建。

  • 搭建OrderController.java,直接提供Http服务调用。
@RestController
@RequestMapping("/order")
public class OrderController {

@Resource
OrderService orderService;

@GetMapping("save")
public String save() {
orderService.doOrder();
return "success";
}
}
  • 核心逻辑都在OrderService.java

serivce

我们先过下整体的逻辑:

标号1和标号2:是我们保存订单和订单详情的入口。分别加上@Transactional注解,并且加上了传播属性的设置(propagation)

标号3:设置传播属性。REQUIRED:必须要有事务,事务不存在,则新建一个。REQUIRES_NEW:创建新事务,如果存在当前的事务,则暂停该事务。

标号4:对于保存订单详情如果抛出异常,捕获异常,不影响保存订单主流程。

标号5:模拟业务抛出异常。

  • 订单表和订单详情表结构:

image-20221030203430555

image-20221030203351639

通过上面的操作,我们进行了场景铺垫,代码复线,表结构。我们先说下我们期待的结果。

order_info表有一条数据,order_exta表没有数据

那么接下来,我们启动服务,调用http服务,结果如下:

mysql-show.pic

和预期结果不同,这脸打得真疼,为什么呢?order_extra会插入数据成功呢?这不会是spring的bug吧?

  • 看到这里,大家可以先回想一下自己在项目中是否看到过这样的代码?

需要事务处理的业务逻辑,在当前bean中,直接调用当前bean的内部事务方法。

如果你的回答是,那么老板,如果这个代码是别人写的,那么你踩坑了。

如果这个代码是你写的,那老板你留坑了。

  • 如果你去请教你们大佬,这时候你们大佬就会告诉你

image-20221112231038162

听哥一句劝:在bean中不要直接调用或者使用this调用,某个被@Transactional注解标注的方法。this下@Transactional注解是不生效的

@Transactional why

这里我们先说结论:Spring的事务是基于AOP实现的,AOP是基于动态代理实现的。所以@Transactional注解如果想要生效,那么其调用方,需要是被Spring动态代理后的类。

前面我们已经观察到现象了,我们现在深入原理,看本质。

image-20221030213345285

可以看出,这个this并不是动态代理的子类对象,而是一个原始对象,所以this调用的doOrderExtra()方法虽然加了@Transactional注解,但是无法却通过动态代理来增强,从而导致事务失效。

现在我们已经知道事务失效的原因,是因为我们没有获取到调用方动态代理的类。

@Transactional fix it

这里我介绍4种方案:

方案一:业务搬迁,也是我们最常用的。

fix-01

将doOrderExtra()的业务逻辑,迁移到其他bean。然后在注入目标bean。

方案二:自己注入自己:不建议,但是可以让事务生效。

image-20221030214912008

可以看到我们注入orderService自己,是被CGLIB动态代理增加后的类了。

方案三:注入ApplicationContext

image-20221030221154275

通过ApplicationContext获取bean。

方案四:使用AopContext获取被代理的bean

fix-4

这里需要注意一点,我们需要开启:@EnableAspectJAutoProxy

我们提出了问题,并且解决了问题。想着第一期视频就这样吧,完结撒花,下期再见~

但是Spring声明事务的坑还有很多,我们再一起看下吧。

总结-扩展

Spring 声明式事务,可能遇到的三类坑,包括:

第一,因为配置不正确,导致方法上的事务没生效。我们务必确认调用 @Transactional 注解标记的方法是 public 的,并且是通过 Spring 注入的 Bean 进行调用的。

第二,因为异常处理不正确,导致事务虽然生效但出现异常时没回滚。Spring 默认只会对标记 @Transactional 注解的方法出现了 RuntimeException 和 Error 的时候回滚,如果我们的方法捕获了异常,那么需要通过手动编码处理事务回滚。如果希望 Spring 针对其他异常也可以回滚,那么可以相应配置 @Transactional 注解的 rollbackFor 和 noRollbackFor 属性来覆盖其默认设置。

第三,如果方法涉及多次数据库操作,并希望将它们作为独立的事务进行提交或回滚,那么我们需要考虑进一步细化配置事务传播方式,也就是 @Transactional 注解的 Propagation 属性。