天天看點

【分布式鎖的演化】電商“超賣”場景實戰

電商“超賣”,帶你領略鎖的實戰

從本篇開始,老貓會通過電商中的業務場景和大家分享鎖在實際應用場景下的演化過程。從Java單體鎖到分布式環境下鎖的實踐。

其實在電商業務場景中,會有一個這樣讓人忌諱的現象,那就是“超賣”,那麼什麼是超賣呢?舉個例子,某商品的庫存數量隻有10件,最終卻賣出了15件,簡而言之就是商品賣出的數量超過了商品本身的庫存數目。“超賣”會導緻商家沒有商品發貨,發貨的時間延長,從引起交易雙方的糾紛。

我們來一起分析一下該現象産生的原因:假如商品隻有最後一件,A使用者和B使用者同時看到了商品,并且同時加入了購物車送出了訂單,此時兩個使用者同時讀取庫存中的商品數量為一件,各自進行記憶體扣減之後,進行更新資料庫。是以産生超賣,我們具體看一下流程示意圖:

【分布式鎖的演化】電商“超賣”場景實戰

遇到上述問題,在單台伺服器的時候我們如何解決呢?我們來看一下具體的方案。之前描述中提到,我們在扣減庫存的時候是在記憶體中進行。接下來我們将其進行下沉到資料庫中進行庫存的更新操作,我們可以向資料庫傳遞庫存增量,扣減一個庫存,增量為-1,在資料庫進行update語句計算庫存的時候,我們通過update行鎖解決并發問題。(資料庫行鎖:在資料庫進行更新的時候,目前行被鎖定,即為行鎖,此處老貓描述比較簡單,有興趣的小夥伴可以自發研究一下資料庫的鎖)。我們來看一下具體的代碼例子。

業務邏輯代碼如下:

通過以上代碼我們可以看到的是庫存的扣減在記憶體中完成。那麼我們再看一下具體的單元測試代碼:

代碼執完畢之後我們看一下結果:

很顯然,資料庫中雖然隻有一個庫存,但是産生了五個下單記錄,如下圖:

【分布式鎖的演化】電商“超賣”場景實戰
【分布式鎖的演化】電商“超賣”場景實戰

這也就産生了超賣的現象,那麼如何才能解決這個問題呢?

那麼如果是這種解決方案的話,我們就要将我們扣減庫存的動作下沉到我們的資料庫中,利用資料庫的行鎖解決并發情況下同時操作的問題,我們來看一下代碼的改造點。

我們再來看一下執行的結果

【分布式鎖的演化】電商“超賣”場景實戰
【分布式鎖的演化】電商“超賣”場景實戰

從上述結果中,我們發現我們的訂單數量依舊是5個訂單,但是庫存數量此時不再是0,而是由1變成了-4,這樣的結果顯然依舊不是我們想要的,那麼此時其實又是超賣的另外一種現象。我們來看一下超賣現象二所産生的原因。

上述其實是第二種現象,那麼産生的原因是什麼呢?其實是在校驗庫存的時候出現了問題,在校驗庫存的時候是并發進行對庫存的校驗,五個線程同時拿到了庫存,并且發現庫存數量都為1,造成了庫存充足的假象。此時由于寫操作的時候具有update的行鎖,是以會依次扣減執行,扣減操作的時候并無校驗邏輯。是以就産生了這種超賣顯現。簡單的如下圖所示:

【分布式鎖的演化】電商“超賣”場景實戰

單體架構中,利用資料庫行鎖解決電商超賣問題。就針對目前該案例,其實我們的解決方式也比較簡單,就是更新完畢之後,我們立即查詢一下庫存的數量是否大于等于0即可。如果為負數的時候,我們直接抛出異常即可。(當然由于此種操作并未涉及到鎖的知識,是以此方案僅做提出,不做實際代碼實踐)

校驗庫存和扣減庫存的時候統一加鎖,讓其成為原子性的操作,并發的時候隻有擷取鎖的時候才會去讀庫庫存并且扣減庫存操作。當扣減結束之後,釋放鎖,確定庫存不會扣成負數。那此時我們就需要用到前面博文提到的java中的兩個鎖的關鍵字<code>synchronized</code>關鍵字 和 <code>ReentrantLock</code>。

關于<code>synchronized</code>關鍵字的用法在之前的博文中也提到過,有方法鎖和代碼塊鎖兩種方式,我們一次來通過實踐看一下代碼,首先是通過方法鎖的方式,具體的代碼如下:

此時我們看一下運作的結果。

【分布式鎖的演化】電商“超賣”場景實戰
【分布式鎖的演化】電商“超賣”場景實戰

此時我們很明顯地發現資料還是存在問題,那麼這個是什麼原因呢?

其實聰明的小夥伴其實已經發現了,我們第二個線程讀取到的資料依舊是1,那麼為什麼呢?其實很簡單,第二個線程在讀取商品庫存的時候是1的原因是因為上一個線程的事務并沒有送出,我們也能比較清晰地看到目前我們方法上的事務是在鎖的外面的。是以就産生了該問題,那麼針對這個問題,我們其實可以将事務的送出進行手動送出,然後放到鎖的代碼塊中。具體改造如下。

此時我們再看一下運作的結果:

根據上面的結果我們可以很清楚的看到隻有第一個線程讀取到了庫存是1,後面所有的線程擷取到的都是0庫存。我們再來看一下具體的資料庫。

【分布式鎖的演化】電商“超賣”場景實戰
【分布式鎖的演化】電商“超賣”場景實戰

很明顯,我們到此資料庫的庫存和訂單數量也都正确了。

後面<code>synchronized</code>代碼塊鎖以及<code>ReentrantLock</code>交給小夥伴們自己去嘗試着完成,當然老貓也已經把相關的代碼寫好了。具體的源碼位址為:https://github.com/maoba/kd-distribute

本文通過電商中兩種超賣現象和小夥伴們分享了一下單體鎖解決問題過程。當然這種鎖的使用是無法跨越jvm的,當遇到多個jvm的時候就失效了,是以後面的文章中會和大家分享分布式鎖的實作。當然也是通過電商中超賣的例子和大家分享。敬請期待。

繼續閱讀