最近在做一个需求,碰到了死锁的问题,记录下解决问题的过程
背景
这个需求要改动一个接口,我这边称为A接口,原先的逻辑是A接口内部会调用c方法,c方法是一个dubbo方法,
现在需要再A接口里添加调用B方法,b方法是本地调用。
A接口的入参是某个商品的编码,拿到这个商品编码后匹配到一个交易订单,B方法和C方法都是需要操作这个订单商品的库存,所以都上了锁。
原逻辑
现逻辑
加完B方法后,发现每次调用接口都会超时,分析日志发现是c方法加库存锁超时了。c方法的库存锁还未释放,同时B方法又加了这个锁,B需要等待C占用的锁释放,而c的锁释放也需要等待B方法结束,于是就产生了死锁
于是我产生了以下几个疑问
1、我们的锁不是支持可重入吗,为什么还会死锁?
是因为可重入是需要同一个线程,而c方法因为是dubbo调用,已经不是一个同一个线程了,就会产生互斥的效果
2、我们项目中的锁做了统一处理,都是在事务结束后才释放锁,是否可以在B方法执行完就释放锁?
这个地方我们之前是踩过坑的,如果在事务完成前释放锁,那么另一个线程会拿到锁,但是因为前面的事务还未提交,所以他查询的数据还是老数据。这样加锁就没有达到目的了,所以必须在事务完成后再释放锁
解决方法
方案1
首先我的想法是能否从业务上解决,如果业务上不关心B方法跟A方法的一致性的一致性的话,就是如果A方法报错了,B方法不需要回滚,那么最简单的做法就是B方法新起一个事务,这样当B方法结束后,B的事务也就结束了,库存锁也就释放了,也就不会影响到c方法
但是产品的想法是需要保证一致,所以这个方法不能搞
方案2
将B方法的加锁和c方法的加锁挪到最外层A方法上,B方法和c方法都不用加锁了
这种方案可以解决问题,但是会引发其他问题,一个就是加锁的范围变大了,会影响这个接口整体的性能,其次需要改动B方法和c方法,也容易改出问题
方案3
B方法和c方法顺序调换,先执行B方法再执行C方法,因为B方法是dubbo方法,所以B方法执行完后它自己的事务也就结束了,B占用的库存锁也会释放。再执行c方法就不会导致锁等待
这个方案是可以实施的,但是会出现一种情况,当c方法报错时,B方法因为已经执行完了,就无法回滚了。之前B方法调用放在最后调用是不会有问题的。同时B方法和c方法顺序调换需要调整一些代码,不是简单的调换就可以的,基于这两点我最后也没有采用这个方案
方案4
结合方案1,B方法另起一个事务,同时如果后续逻辑报错了,捕获异常,调用B方法的回滚方法(也是新事务)
有点像tcc模式
最终采用了这个方案,改动量和影响点最小
碰到了另一个坑
另起一个事务用了Propagation.REQUIRES_NEW 这个传播级别,但是测试的时候发现还是死锁,似乎没有起到效果,于是我回想起事务失效的几种场景,又复习了遍AOP的底层原理,知道自己踩坑了
原始代码A方法调用B方法
service A {
@Transactional
method A() {
B();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
method B() {
}
}
spring启动后会扫描@Transactional注解,生成代理类AProxy,添加事务的逻辑
service AProxy {
A a;
method A() {
// 开启事务
a.B();
// 结束事务
}
// 没有走这个方法
method B() {
// 新开事务
// 结束事务
}
}
当我们执行A方法,会执行代理类AProxy的方法,但是调用B方法是没有调用代理类的B方法,而是service A自己原本的方法,所以method B上的注解没有其效果
解决方法也有好几种,我这边是将method B方法写在其他类中,跨类调用就可以了