在秒殺系統設計中,超賣是一個經典、常見的問題,任何商品都會有數量上限,如何避免成功下訂單買到商品的人數不超過商品數量的上限,這是每個搶購活動都要面臨的難點。
一、問題描述
在多個用戶同時發起對同一個商品的下單請求時,先查詢商品庫存,再修改商品庫存,會出現資源競爭問題,導致庫存的最終結果出現異常。問題:
當商品A一共有庫存15件,用戶甲先下單10件,用戶乙下單8件,這時候庫存只能滿足一個人下單成功,如果兩個人同時提交,就出現了超賣的問題。
二、解決的三種方案
1.解決方案1
- 悲觀鎖
當查詢某條記錄時,即讓數據庫爲該記錄加鎖,鎖住記錄後別人無法操作,使用類似如下語法:
select stock from tb_sku where id=1 for update;
SKU.objects.select_for_update().get(id=1)在這裏插入代碼片
- 悲觀鎖類似於我們在多線程資源競爭時添加的互斥鎖,容易出現死鎖現象,採用不多。
2.解決方案2
- 樂觀鎖
樂觀鎖並不是真實存在的鎖,而是在更新的時候判斷此時的庫存是否是之前查詢出的庫存,如果相同,表示沒人修改,可以更新庫存,否則表示別人搶過資源,不再執行庫存更新。類似如下操作:
update tb_sku set stock=2 where id=1 and stock=7;
SKU.objects.filter(id=1, stock=7).update(stock=2)
- 使用樂觀鎖需修改數據庫的事務隔離級別:
使用樂觀鎖的時候,如果一個事務修改了庫存並提交了事務,那其他的事務應該可以讀取到修改後的數據值,所以不能使用可重複讀的隔離級別,應該修改爲讀取已提交(Read committed)。
修改方式:
3.解決方案3
- 任務隊列
將下單的邏輯放到任務隊列中(如celery),將並行轉爲串行,所有人排隊下單。比如開啓只有一個進程的Celery,一個訂單一個訂單的處理。
三、網上超賣問題解決方案節選
節選自如何解決秒殺系統的性能問題和超賣的討論的片段:如何解決秒殺系統的性能問題和超賣的討論
解決方案2:
引入隊列,然後將所有寫DB操作在單隊列中排隊,完全串行處理。當達到庫存閥值的時候就不在消費隊列,並關閉購買功能。這就解決了超賣問題。
優點:解決超賣問題,略微提升性能。
缺點:性能受限於隊列處理機處理性能和DB的寫入性能中最短的那個,另外多商品同時搶購的時候需要準備多條隊列。
解決方案3:
將寫操作前移到Memcached中,同時利用Memcached的輕量級的鎖機制CAS來實現減庫存操作。
優點:讀寫在內存中,操作性能快,引入輕量級鎖之後可以保證同一時刻只有一個寫入成功,解決減庫存問題。
缺點:沒有實測,基於CAS的特性不知道高併發下是否會出現大量更新失敗?不過加鎖之後肯定對併發性能會有影響。
解決方案4:
將提交操作變成兩段式,先申請後確認。然後利用Redis的原子自增操作(相比較MySQL的自增來說沒有空洞),同時利用Redis的事務特性來發號,保證拿到小於等於庫存閥值的號的人都可以成功提交訂單。然後數據異步更新到DB中。
優點:解決超賣問題,庫存讀寫都在內存中,故同時解決性能問題。
缺點:由於異步寫入DB,可能存在數據不一致。另可能存在少買,也就是如果拿到號的人不真正下訂單,可能庫存減爲0,但是訂單數並沒有達到庫存閥值。