秒殺場景下超賣問題解決方案

2021-10-23 00:11:30 字數 3507 閱讀 8889

秒殺超賣現象:在高併發下,多個執行緒併發更新庫存,導致庫存為負的情況。

乙個簡單的訂單表

create table orders (

sku_id int primary key,

count int

)

乙個/buy介面

begin()

count = db.query(`select count from orders where sku_id = '123'`)

if count < 0

db.exec(`update orders set count = count -1 where sku_id = '123'`)

commit()

由於sql 支援並行加上事務的隔離性,所以當多個事務並行時,select出來的值並不一定準確的,進而update之後的值也就不正確了。

資料庫設定欄位為無符號型

當併發超賣時直接報異常

通過捕獲異常提示已經售空。

採用排他鎖

當使用者同時到達更新操作,同時到達的使用者乙個個執行

在當前這個update語句commit之前,其他使用者等待執行

解決方法:

begin()

update orders set count = count -1 where count > 0 and sku_id = '123'

commit()

這裡是利用了update造成的行級鎖,避免了超賣的問題。

優點:確保了執行緒安全。

上述的方案的確解決了執行緒安全的問題,但是,別忘記,我們的場景是「高併發」。也就是說,會很多這樣的修改請求,每個請求都需要等待「鎖」,某些執行緒可能永遠都沒有機會搶到這個「鎖」,這種請求就會死在那裡。針對這個問題我們稍微修改一下上面的場景,我們直接將請求放入佇列中的,採用fifo(first input first output,先進先出),這樣的話,我們就不會導致某些請求永遠獲取不到鎖。可以採用redis佇列+mysql事務控制的方案.下面是整個執行流程圖:

當使用者搶到一件**商品後先觸發檔案鎖,防止其他使用者進入,該使用者搶到**品後再解開檔案鎖,放其他使用者進行操作。這樣可以解決超賣的問題,但是會導致檔案得i/o開銷很大。

優點確保了執行緒安全。

缺點是磁碟 io 開銷會變大。

由於資料庫的效能問題,無法應對高併發的秒殺場景,所以通常的解決方案是利用快取來完成,先在快取中完成計數,然後再通過訊息佇列非同步地入庫

redis由於其高速+單程序模型,省掉了很多併發的問題,所以可以被選來進行高速秒殺的工作。

方案一,fifo佇列序列化( redis list)

通過 fifo 佇列,使修改庫存的操作序列化。

我們建立sku:id為鍵的list結構, 在得到sku的庫存餘量n之後,在sku:id中push n個值為1的元素。當秒殺時,執行lpop即可。當lpop返回nil時, 即代表庫存賣完了。

優點:實現簡單,不需要在單獨加鎖(無論是悲觀鎖還是樂觀鎖)。

缺點:佇列的長度是有限的,必須控制好,不然請求會越積越多。當庫存非常大時, 會占用非常多的記憶體。

方案二, redis原子操作(redis incr)

仍然是sku:id為鍵,但是直接設定為庫存餘量n。當秒殺時,執行decr 即可,當decr返回值小於0時,即代表庫存賣完了。

優點: 節約記憶體

缺點:decr incr的操作範圍都是int64,當decr min_int64時,redis會報告overflow。不過好在考慮到業務實際,幾乎不會出現該情況,畢竟庫存終究會重新整理的,秒殺也不可能一直持續下去。

方案三, redis 分布式鎖

鎖似乎一直都是用來解決併發問題的良藥。redis由於其單程序的模型,很多設計都可以作為分布式鎖來用,在sku:id中儲存庫存餘量, 以sku_lock:id作為其鎖,當成功獲得鎖後, 對sku:id進行獲取,減1。

比較常見的是setnx來做分布式鎖。

setnx: 當只在鍵 key 不存在的情況下, 將鍵 key 的值設定為 value ,返回1。

若鍵 key 已經存在, 則 setnx 命令不做任何動作,返回0。

因此當setnx返回1時即意味著加鎖成功,返回0即加鎖失敗。解鎖操作比較簡單,del掉這個key就可以了。

優點: 鎖似乎永遠都可以作為解決併發問題的銀彈,因此當你發現併發造成資料不一致的時候,從解決問題的思路出發,第一反應應該是加鎖,第二反應才應該是無鎖化的優化操作。

缺點:明顯比前兩種方案增加了一些代價。另外,由於該鎖並不是阻塞型的,沒有排隊機制,並不會遵循先到先到的邏輯。

悲觀鎖的解決方案解決了鎖的問題,全部請求採用「先進先出」的佇列方式來處理。那麼新的問題來了,高併發的場景下,因為請求很多,系統處理佇列內請求的速度根本無法和瘋狂湧入佇列中的數目相比,很可能一瞬間將佇列記憶體「撐爆」,最終web系統平均響應時候還是會大幅下降,系統還是陷入異常。

這個時候,我們就可以討論一下「樂觀鎖」的思路了。樂觀鎖,是相對於「悲觀鎖」採用更為寬鬆的加鎖機制,大都是採用帶版本號(version)更新。實現就是,這個資料所有請求都有資格去修改,但會獲得乙個該資料的版本號,只有版本號符合的才能更新成功,其他的返回搶購失敗。這樣的話,我們就不需要考慮佇列的問題,不過,它會增大cpu的計算開銷。但是,綜合來說,這是乙個比較好的解決方案。

樂觀鎖的執行流程如下:
redis 的 watch機制

採用redis資料庫,前置到mysql。思路如下:

2.1系統啟動後,初始化sku資訊到redis資料庫,記錄其可用量和鎖定量

2.2使用樂觀鎖,採用redis的watch機制。邏輯為:

1.定義門票號變數,設定初始值為0。watchkey

2.watch該變數,watch(watchkey);

3.使用redis事務加減庫存。首先獲取可用量和搶購量比較,如果curcount>buycount,那麼正常執行減庫存和加鎖定量操作:

multi;

redis incr watchkey;

redis decrby curcount buycount;

redis incrby lockcount buycount;

exec;

由於上述操作都在事務內進行,一旦watchkey被其他的事務修改過,那麼exec將返回nil,如此就放棄本次請求。一般都是在迴圈中重複嘗試直到成功或沒有可用量。

最後通過訂單資訊流,保證mysql資料庫的最終一致性。

總的來說,不能把壓力放在資料庫上,所以使用"select *** for update"的方式在高併發的場景下是不可行的。fifo 同步佇列的方式,可以結合庫存限制佇列長,但是在庫存較多的場景下,又不太適用。所以相對來說,我會傾向於選擇:樂觀鎖/快取鎖/分布式鎖的方式。

參考:

mysql 樂觀鎖 超賣 秒殺超賣解決方案

方案一 redis事務處理 multi 我們可以使用redis中的監聽 watch 方法,去監聽庫存數量,一旦庫存數量在其他客戶端發生改變,後續操作則會失敗。watch key1 key2 監聽key1 key2有沒有變化,如果有變,則事務取消 方案二 redis分布式鎖 分布式鎖確保只有乙個執行緒...

liunx 下ctrl D問題解決方案

在啟動過程中最容易遇到的問題就是硬碟可能有壞道或扇區錯亂 資料損壞 的情況,這種情況多由於異常斷電 不正常關機導致。在系統啟動時會顯示 press root password or ctrl d 此時我們可以進入單使用者模式進行修復,修復之前要和客戶溝通修復期間會影響到一部分資料,提前和客戶溝通好!...

ubuntu下亂碼問題解決方案

ubuntu下亂碼問題解決方案 txt檔案在windows下可以正常顯示,ubuntu下開啟檔案亂碼。這是中文編碼問題,windows下用的是gb2312,而linux下用的是utf8。在此提供5種解決方案 1.在文件所在目錄執行命令 www.2cto.com iconv f gb2312 t ut...