今天线上出现了一个故障,用户充值成功后,账户的充值记录一直没有变更.
第一个反应,就是支付公司没有回调,但是通过查询日志发现,回调时有的,只是在处理时,使用乐观锁更新账户余额失败了!
WHAT? 我不是加了分布式锁吗,为什么还会出现这个问题.
伪代码如下
@Transactional(rollbackFor = Exception.class)
public void add(Param param) {
// 获取redis 锁
boolean lock = redisLock.getLock();
if(!lock){
throw new BaseException("获取锁失败");
}
// 获取账号信息
UserAccount userAccount = getUserAccount(param.userId);
// 更新
userAccount.setBalance = (userAccount.getBalance + param.amount);
userAccount.setDataVersion(userAccount.getDataVersion()+1);
// 乐观锁更新
if(!update(userAccount)){
throw new BaseException("乐观锁更新失败");
}
// 解锁
redisLock.unlock();
}
通过日志发现,是在乐观锁更新时失败了,不可能呀,我明明先获取的锁,再去获取的账户信息,为什么还会更新失败呢?
最终经过脑洞思考和排查,发现了问题,详细的可以看下面的流程图.
问题的原因就是在没有提交事务的时候,提前释放了锁,导致其余的线程获取到的是同一个乐观锁版本的数据,最终更新时,导致乐观锁失败.
解决的办法,也很简单,可以将账户操作放在一个新的类方法里面,在这里进行事务操作.同时在原来的方法里面加锁调用.这样就可以解决了.
class A{
public void addAndLock(Param param){
// 获取redis 锁
boolean lock = redisLock.getLock();
// 执行操作
B.add(param);
// 解锁
redisLock.unlock();
}
}
class B{
@Transactional(rollbackFor = Exception.class)
public void add(Param param) {
if(!lock){
throw new BaseException("获取锁失败");
}
// 获取账号信息
UserAccount userAccount = getUserAccount(param.userId);
// 更新
userAccount.setBalance = (userAccount.getBalance + param.amount);
userAccount.setDataVersion(userAccount.getDataVersion()+1);
// 乐观锁更新
if(!update(userAccount)){
throw new BaseException("乐观锁更新失败");
}
}
}