現(xiàn)象描述
運(yùn)營突然找到開發(fā)人員反饋:交易成功针史,但是賬戶數(shù)據(jù)不對。開發(fā)查看日志發(fā)現(xiàn)了大量的樂觀鎖更新異常碟狞。
背景描述
簡略的業(yè)務(wù)背景是這樣啄枕,一個交易請求過來,需要兩個主要操作族沃,一個是記錄交易記錄频祝,另一個是更新賬戶數(shù)據(jù)。這里我們默認(rèn)為這兩個操作是同步的脆淹,也就是需要在一個請求中串聯(lián)執(zhí)行完成后返回常空。
并發(fā)更新賬戶,有兩種方式:
- 使用樂觀鎖加上重試機(jī)制未辆,也就是數(shù)據(jù)庫字段加一個字段(比如:version)更新是對比version是否是同一個窟绷,如果不是則拋異常。配合重試機(jī)制再次執(zhí)行更新操作達(dá)到最終更新完成的目標(biāo)咐柜。
- 加鎖串行化執(zhí)行兼蜈。
兩種方案各有優(yōu)缺點(diǎn)。本文討論的是第二種方案拙友。
我們的業(yè)務(wù)系統(tǒng)實(shí)現(xiàn)時采用了第二種方案为狸,但是保留了樂觀鎖只是沒有重試機(jī)制,才有了現(xiàn)象描述中的樂觀鎖更新異常遗契。
方案的簡略代碼是這樣的:
@Transactional
public void transfer(......) {
lock.lock();
.....do something
accountRepository.save(accountInfo)
lock.unlock();
}
這里的lock可以是分布式鎖也可以是synchronized及其他辐棒,這里只是個實(shí)例。這個代碼的意思是在更新賬戶的時候加鎖牍蜂,串行執(zhí)行漾根。如果鎖生效那么就不會出現(xiàn)樂觀鎖異常,那問什么鎖會失效呢鲫竞。
問題分析
回到方案最初的目的辐怕,加鎖的目的在于保證更新賬戶操作是串行的。那么在lock和unlock之間的save如果成功就達(dá)到了目的从绘。問題就出在了這里寄疏,這里的save,commit了嗎僵井?我們看這個方法是使用了@Transactional 陕截,事務(wù)交給了Spring 管理。Spring的具體實(shí)現(xiàn)方式呢就是通過AOP批什。AOP簡單點(diǎn)講就是在你方法執(zhí)行的前后去做統(tǒng)一的一個處理农曲。那么問題就來了,這個lock和unlock操作是在方法里面執(zhí)行的驻债,但是事務(wù)的提交是在unlock后切面里面執(zhí)行的朋蔫。這樣相當(dāng)于沒有生效罚渐。
解決方案
解決方案就很簡單了,鎖加在方法之外就可以了驯妄。