為什么庫存扣減不需要加鎖?尤其是樂觀鎖?
在我的數(shù)藏項(xiàng)目中,庫存扣減的方法中(本文特指數(shù)據(jù)庫的扣減,不包含 Redis 層),我們?nèi)淌菦]有加鎖的,甚至樂觀鎖都沒加。
沒加悲觀鎖是因?yàn)槊霘鼍安贿m合用悲觀鎖(為什么秒殺不能用悲觀鎖或者分布式鎖來實(shí)現(xiàn),詳見我的面試寶典),那么為啥樂觀鎖都沒加呢?這個是我們的庫存預(yù)扣減的 SQL:
圖片
大家可以先忽略 SQL 中`/*+ COMMIT_ON_SUCCESS ROLLBACK_ON_FAIL TARGET_AFFECT_ROW 1 */`部分內(nèi)容,這是利用了阿里云 DMS 的 Inventory Hint 的機(jī)制抗高并發(fā)的實(shí)現(xiàn)。具體可以到我的項(xiàng)目課程中學(xué)習(xí)。
如上的 SQL 中,我們只做了數(shù)量的扣減,以及數(shù)量的上限的控制。但是沒有通過 lock_version 做樂觀鎖的版本控制。
雖然我們用了MybatisPlus。也用到了他的@Version 注解,但是我們自己寫的 SQL 的這種方式,MyBatisPlus 也不會主動幫我們加樂觀鎖的控制:
圖片
那就是說我們這里沒有做加鎖,為什么呢?
如果你知道樂觀鎖和悲觀鎖的區(qū)別,那么就會知道,樂觀鎖因?yàn)楸容^樂觀,所以一般是先做業(yè)務(wù)邏輯操作,比如參數(shù)處理,內(nèi)存中進(jìn)行模型組裝調(diào)整,然后再去更新數(shù)據(jù)庫。悲觀鎖因?yàn)楸容^悲觀,所以會先嘗試加鎖,然后再去做業(yè)務(wù)邏輯操作。
也就是說,樂觀鎖是先干活,后加鎖。悲觀鎖是先加鎖,再干活。樂觀鎖適合用在并發(fā)沖突不高的場景中。
而我們的這個庫存扣減這里,其實(shí)并發(fā)沖突還挺高的,如果這里用了樂觀鎖,那么就會有大量的失敗。
舉個例子,100個線程同時來查詢庫存,得到的 lock_version = 1,然后同時去更新庫存,都要求當(dāng)前的 lock_version = 1,這就會導(dǎo)致99個線程都失敗,那么就會導(dǎo)致整體失敗,如果事務(wù)中有其他操作,就要回滾。
庫存扣減的時候,我們的目的肯定是讓多個線程都扣減成功,并且不扣錯就行了。
如果你的 SQL 是這樣的:
UPDATE collection
SET saleable_inventory = #{saleableInventory}
WHERE id = #{id}
也就是說這個變更后的saleableInventory是你自己算好的,給到 SQL 去執(zhí)行的,那么就必須要加鎖來避免并發(fā)的時候計算出錯,但是我們通過在 SQL 中扣減不會有任何并發(fā)導(dǎo)致出錯的問題。
而如果這里不加樂觀鎖,只通過庫存扣減,以及庫存不能小于0來做控制,那么只需要他們各自搶鎖然后按順序扣減就行了,反正每次庫存都是在前面的結(jié)果上依次扣減的:
SET saleable_inventory = saleable_inventory - #{quantity}
所以,這里我們只需要保證不會超賣就行了,剩下的并發(fā)問題 update 過程中的鎖會幫我們解決的。
本文的技術(shù)方案來自于我出的數(shù)藏項(xiàng)目實(shí)戰(zhàn)課,里面還有很多類似的干貨內(nèi)容。