现象描述
运营突然找到开发人员反馈:交易成功,但是账户数据不对。开发查看日志发现了大量的乐观锁更新异常。
背景描述
简略的业务背景是这样,一个交易请求过来,需要两个主要操作,一个是记录交易记录,另一个是更新账户数据。这里我们默认为这两个操作是同步的,也就是需要在一个请求中串联执行完成后返回。
并发更新账户,有两种方式:
- 使用乐观锁加上重试机制,也就是数据库字段加一个字段(比如:version)更新是对比version是否是同一个,如果不是则抛异常。配合重试机制再次执行更新操作达到最终更新完成的目标。
- 加锁串行化执行。
两种方案各有优缺点。本文讨论的是第二种方案。
我们的业务系统实现时采用了第二种方案,但是保留了乐观锁只是没有重试机制,才有了现象描述中的乐观锁更新异常。
方案的简略代码是这样的:
@Transactional
public void transfer(......) {
lock.lock();
.....do something
accountRepository.save(accountInfo)
lock.unlock();
}
这里的lock可以是分布式锁也可以是synchronized及其他,这里只是个实例。这个代码的意思是在更新账户的时候加锁,串行执行。如果锁生效那么就不会出现乐观锁异常,那问什么锁会失效呢。
问题分析
回到方案最初的目的,加锁的目的在于保证更新账户操作是串行的。那么在lock和unlock之间的save如果成功就达到了目的。问题就出在了这里,这里的save,commit了吗?我们看这个方法是使用了@Transactional ,事务交给了Spring 管理。Spring的具体实现方式呢就是通过AOP。AOP简单点讲就是在你方法执行的前后去做统一的一个处理。那么问题就来了,这个lock和unlock操作是在方法里面执行的,但是事务的提交是在unlock后切面里面执行的。这样相当于没有生效。
解决方案
解决方案就很简单了,锁加在方法之外就可以了。