資料庫方案
以下的方案重點在于防止超賣,庫存資訊不加載到緩存Redis,而是直接同DB互動,實際場景下通常不會如此,但是其中用到的細節還是值得學習的。
FOR UPDATE
該方案是在MySQL層面進行加鎖,行鎖Or表鎖,要根據Where條件來判定。
該方案通過
事務+for update
進行保證,僞代碼如下所示:
begin transcational
count = SELECT number FROM seckill WHERE seckill_id=? FOR UPDATE
if count > 0
UPDATE seckill SET number=number-1 WHERE seckill_id=?
INSERT order //下訂單
commit
說明:
- 同一個事務中 如果查詢最後限定了
那麼非本次事務中的 其他SQL指令會被阻塞。for update
- 如果
條件為where
或者主鍵
的時候才會鎖住行,即索引列
。否則會鎖表,即行級鎖
。目前的where條件很明确是鎖表
為主鍵,是以是行級鎖。seckill_id
- select操作和update操作都是在本次事務中進行的。
庫存大于0判定
該方案主要通過在執行update減少庫存的時候,加上對庫存大于0的判定。
begin transcational
count = UPDATE seckill SET number=number-1 WHERE seckill_id=? AND number>0
if count > 0
INSERT order //下訂單
commit
其中最為核心的就是最後一個條件
number>0
庫存設定為無符号整形
核心就是将庫存設定為
無符号整形
,就是不允許庫存為負數
begin transcational
count = UPDATE seckill SET number=number-1 WHERE seckill_id=?
if count > 0
INSERT order //下訂單
commit
這點同where條件有些像。
樂觀鎖
通過樂觀鎖來保證商品在每一個隻會被消費一次,通過對
number
進行樂觀鎖來進行判定,僞代碼如下所示:
BEGIN transcational
n = SELECT goods number
count = UPDATE seckill SET number=number-1 WHERE seckill_id=? AND number = n
if count > 0
INSERT order //下訂單
else
Loop //再次循環操作
commit
說明
- 請注意每次進行update前先查詢,查詢的目的是擷取版本号。
- 通過對CAS的思想通過版本号對庫存進行更新,如果符合預期那麼更新,否則肯定是被其他線程消費過。
分布式鎖
通過分布式鎖來保證,同一時間隻會有一個線程在處理某類商品秒殺業務:查詢庫存-->判定庫存 -->減少庫存。
dis-lock goods type //1. 通過分布式鎖:鎖住商品類型
begin transcational //2. 開啟事務
count = SELECT number FROM seckill WHERE seckill_id=?
if count > 0
UPDATE seckill SET number=number-1 WHERE seckill_id=?
INSERT order //下訂單
// dis-un-lock goods type 錯誤釋放分布式鎖
commit //3. 送出事務
dis-un-lock goods type //4. 釋放分布式鎖
說明
-
請注意3和4 的順序,一定要先送出事務後,再釋放分布式鎖。
為什麼了?假設此時的庫存為1。第一個線程在第送出事務前釋放了鎖,假設送出事務需要5個機關的時間。另外一個線程在第一個線程釋放鎖的瞬間,搶占了鎖,然後在3個時間就完成了查詢和減少庫存并送出事務的操作,此時庫存為0。2個機關後第一個線程事務送出才完成,此時庫存為-1了。這樣導緻了超賣。
是以事務的送出一定要釋放分布式鎖之前。
- 在Spring中事務通過是通過注解
來實作的,如果直接在@Transcational
包裹的方法裡面擷取鎖和釋放鎖可能會出現超賣,此時需要通過另外一個AOP進行包裝,這裡涉及到2個知識點。@Transcational
- 一個方法有多個AOP注解時候,切面的執行順序怎麼确定的問題。該問題是每個切面都可以通過
來定義順序,越小的越先執行,而@order
的order是最大,是以肯定是在内部執行。@Transcational
- :多個切面的執行順序和退出順序問題,可以參考此文:http://www.hicode.club/2018/03/01/apsect-order/
- 一個方法有多個AOP注解時候,切面的執行順序怎麼确定的問題。該問題是每個切面都可以通過
利用Redis
因為
Reis
是單線程的,是以可以通過其特性
decr
後進行判定,實際場景也是推薦這麼做的。
- 秒殺業務前,将秒殺商品和庫存資訊緩存到
中。Redis
- 對
中緩存的商品數量做Redis
減1操作,如果小于0則将商品id加入decr
中。避免每次都往Redis請求。商品售賣完成緩存
總結
技術是為了解決業務難題而存在和發展的,切記脫離業務來學習技術。
上述方案都是解決秒殺業務的初步原形,大概思路如上所述,其中其實有很多細節,後續抽時間補上。
業務處理上需要結合自己的業務進行擴充,個人推薦:
redis > 樂觀鎖 > 庫存大于0 > 分布式鎖 > for update
- 原則上盡可能的避免鎖表操作。
- 盡量避免請求直接打到資料庫上。