最近在看秒殺相關的專案,針對防止庫存超賣的問題,查閱了很多資料,其解決方案可以分為悲觀鎖、樂觀鎖、分布式鎖、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處理高併發,防止庫存超賣的問題,在去年的時候,王總已經提過 但是很可惜,即使當時大家都聽懂了,但是在現實開發中,還是沒這方面的意識。今天就我的一些理解,整理一下這個問題,並希望以後這樣的課程能多點。先來就庫存超賣的問題作描述 一般電子商務 都會遇到如 秒殺 之...