高併發下防止庫存超賣的解決方案

2021-10-03 08:54:38 字數 3632 閱讀 6088

最近在看秒殺相關的專案,針對防止庫存超賣的問題,查閱了很多資料,其解決方案可以分為悲觀鎖、樂觀鎖、分布式鎖、redis原子操作、佇列序列化等等,這裡進行淺顯的記錄總結。

首先我們來看下庫存超賣問題是怎樣產生的:1 2

3 45 6

//1.查詢出商品

庫存資訊

select

stock

from

t_goods 

where

id=1;

//2.根據商品資訊生成訂單

insert

into

t_orders (id,goods_id) 

values

(null

,1);

//3.修改商品

庫存 update

t_goods 

setstock

=stock-1

where

id=1;

在高併發場景下,如果同時有兩個執行緒a和b,同時查詢到商品庫存為1,他們都認為存庫充足,於是開始下單減庫存。如果執行緒a先完成減庫存操作,庫存為0,接著執行緒b也是減庫存,於是庫存就變成了-1,商品被超賣了。

下面讓我們來看看針對庫存超賣問題的解決方案;

所謂悲觀鎖,即悲觀的認為自己在運算元據庫時,會大機率出現併發,於是在操作前會先進行加鎖,操作完成後再釋放鎖。如果加鎖失敗說明該記錄正在被修改,那麼當前操作可以等待後嘗試。

以我們常用的mysql為例,行鎖、表鎖、排他鎖等都是悲觀鎖,為避免衝突,會在操作時先加鎖,其他執行緒必須等待它的完成。

這裡我們通過使用select...for update語句,在查詢商品表庫存時將該條記錄加鎖,待下單減庫存完成後,再釋放鎖。1 2

3 45 6

7 89 10

//0.開始事務

begin

;/begin

work

;/start 

transaction

; (三者選一就可以)

//1.查詢出商品資訊

select

stock

from

t_goods 

where

id=1 

forupdate

; //2.根據商品資訊生成訂單

insert

into

t_orders (id,goods_id) 

values

(null

,1);

//3.修改商品

stock減一

update

t_goods 

setstock

=stock-1

where

id=1

; //4.提交事務

commit;

這樣可以解決併發時庫存超賣的問題,然而高併發時,所有的操作都被序列化了,效率很低,將嚴重影響系統的吞吐量。而且使用悲觀鎖還有可能造成死鎖問題。

現在我們嘗試下使用樂觀鎖,所謂樂觀鎖,是相對於悲觀鎖而言的,它假設資料一般情況下不會發生併發,因此不會對資料進行加鎖,操作完成提交時才對資料是否衝突進行檢測,如果發現衝突則返回錯誤。

比較常見的實現方式是,在表中增加乙個version欄位,操作前先查詢version資訊,在資料提交時檢查version欄位是否被修改,如果沒有被修改則進行提交,否則認為是過期資料。1 2

3 45 6

//1.查詢出商品資訊

select

stock

, version

from

t_goods 

where

id=1;

//2.根據商品資訊生成訂單

insert

into

t_orders (id,goods_id) 

values

(null

,1);

//3.修改商品

庫存 update

t_goods 

setstock

=stock-1

, version = version+1

where

id=1

, version=version;

這樣,在併發時,如果執行緒a嘗試修改商品庫存時,發現版本號已經被執行緒b修改了,執行緒a執行update語句條件不滿足便不再執行了,庫存也不會被超賣。

但是這種樂觀鎖的方式,在高併發時,只有乙個執行緒能執行成功,會造成大量的失敗,這給使用者的體驗顯然是很不好的。

這裡我們可以減小鎖的顆粒度,最大程度提公升系統的吞吐量,提高併發能力:1 2

//修改商品

庫存時判斷庫存是否大於0

update

t_goods 

setstock

=stock-1

where

id=1

andstock>0;

上面的update語句通過stock>0進行樂觀鎖的控制,在執行時,會在一次原子操作中查詢stock的值,並扣減一。

除了在資料庫層面加鎖,我們還可以通過在記憶體中加鎖,實現分布式鎖。例如我們可以在redis中設定乙個鎖,拿到鎖的執行緒搶購成功,拿不到鎖的搶購失敗。

redis的setnx方法可以實現鎖機制,key不存在時建立,並設定value,返回值為1;key存在時直接返回0。執行緒呼叫setnx方法成功返回1認為加鎖成功,其他執行緒要等到當前執行緒業務操作完成釋放鎖後,才能再次呼叫setnx加鎖成功。

long timeout_secound = 120000l;

jedis

client =

jedispool

.getresource();

//執行緒設定

lock鎖成功

while

(client.setnx(

"lock"

,string.valueof(system.

currenttimemillis

())) == 1)

thread.sleep(10000);

} ......

......

client.del(

"lock");

雖然通過以上方按可以防止庫存超賣,但是高併發情況下對資料庫進行頻繁操作,會造成嚴重的效能問題。因此我們必須在前端對請求進行限制。

我們可以在redis中設定乙個佇列key為商品的id,佇列的長度為商品庫存量。每次請求到達時pop出乙個元素,這樣拿到元素的請求即認為秒殺成功,後續通過mq傳送訊息非同步完成資料庫減庫存操作。沒有拿到元素的請求即認為秒殺失敗。

由於redis是工作執行緒是單執行緒的,而list的pop操作是原子性的,因此併發的請求都被序列化了,庫存就不會超賣了。

//獲取商品庫存

string

token

=redistemplate

.opsforlist().leftpop(

goodsstock);

if

(token

==null

) //

非同步傳送

mq訊息,執行資料庫操作

sendsecondkillmsg(

goodsid, userid

); ...

當然除此之外還有很多其他解決方案,也有很多可以優化的地方,繼續學習吧~

電商防止庫存超賣解決方案

悲觀鎖,也就是在修改資料的時候,採用鎖定狀態,排斥外部請求的修改。遇到加鎖的狀態,就必須等待。可以採用redis佇列 mysql事務控制的方案,下面是流程圖 mysql的執行 begintranse 開啟事務 try catch e exception commit 提交事務 先執行update鎖住...

庫存超賣的解決方案

update sku info set kc kc 1 where sku id and kc 0 在高併發下,多人搶同一庫存,由於資料庫讀寫可以並行執行的原因,會導致修改庫存時,庫存不足出現超賣。悲觀鎖解決 在select加乙個行鎖,與更新庫存操作互斥,保證查詢庫存時,庫存不被修改 在查詢和更新庫...

《轉》 mysql處理高併發,防止庫存超賣

今天王總又給我們上了一課,其實mysql處理高併發,防止庫存超賣的問題,在去年的時候,王總已經提過 但是很可惜,即使當時大家都聽懂了,但是在現實開發中,還是沒這方面的意識。今天就我的一些理解,整理一下這個問題,並希望以後這樣的課程能多點。先來就庫存超賣的問題作描述 一般電子商務 都會遇到如 秒殺 之...