关注公众号:云下风澜 , 免费领取Java学习 面试资料
强调一下,笔者是很注意保护公司代码的,所以接下来都是用的伪代码。如果有什么问题可以和笔者私信沟通。
本次出问题的代码是一个审批接口的一部分逻辑。经笔者简化后(去掉各种各样的风骚判断+数据处理),最终剩下的逻辑如下(笔者重新简写的伪代码)。
@Transactional //开启事物
public void approval(){
Boolean lock = getLock();//获取锁
if(!lock){
throw new BzException("获取锁失败");
}
try {
Integer status = getStatus();//从数据库获取当前数据的状态
//如果数据库的状态是待审批的话就将其改为已审批
if(status == WAIT_APPROVAL){
setStatus(APPROVALED);
}else{
return;
}
addApprovalRecord();//新增一条审批记录
return;
}catch (Exception e){
}finally {
unLock();//释放锁
}
}
- 首先是获取分布式锁。如果获取成功了就往下执行逻辑。
- 然后获取当前单子的状态,发现状态如果是待审批的话,就将其改为已审批。然后释放锁,方法return。
本身直接看感觉方法也没什么问题,但恰恰越感觉没问题的地方就越容易出问题。
代码刚上线不久,笔者就被产品经理叫了过去,倒不是什么大问题。
产品经理:我怀疑你有bug,你这个为啥一个单子审批过后有两条审批记录。
笔者:我跟你说,我就没有过bug,是不是用户做了什么骚操作。
经过一番排查后,笔者顶着厚厚的脸皮承认了自己的bug。感觉自己的段位被人生生的打回了青铜。
bug原因如下。
- 当两个人同时操作时,A获取锁成功,往下走,发现状态是待审批就正常进行审批,到下面return,笔者竟然忘记了,return的时候代码是先走finally的,所以代码在执行完逻辑走到了finally释放了锁。
- 好巧不巧的在这个时候用户B也对这个单子进行了审核操作,进来之后发现没有锁,获取锁成功,往下走,然后获取状态发现是待审核(因为A操作的事物还没有提交)。
- 这时用户A的操作执行了return,事物提交,将单子状态改成了已审核。
- 但是B已经获取到当前单子的状态了,所以就继续正常审核了,结果状态是一致的,但是多了一条记录。
发现这个问题之后,笔者想到了两个解决方式,一个是将需要执行事务操作的审批部分代码,单独抽出一个方法后加上事务注解。
public void approval(){
Boolean lock = getLock();
if(!lock){
throw new BzException("获取锁失败");
}
try {
((zj) AopContext.currentProxy()).doApproval();
}finally {
unLock();
}
return;
}
这样就可以当完全执行完审批操作这个事务时再释放锁。
这里也有一个小知识点,在一个方法中调用另一个带有事物的方法时,要用获取当前对象的代理,通过代理类去调用带有事物的方法,否则事物不会生效(因为Spring的事物是通过代理和切面去实现的)
第二种方式就是不用Spring的事物注解,做手动事物管理,但是不推荐此方法,太low。