天天看點

秒殺業務中不超賣的實作方案彙總資料庫方案分布式鎖利用Redis總結

資料庫方案

以下的方案重點在于防止超賣,庫存資訊不加載到緩存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
           

說明:

  1. 同一個事務中 如果查詢最後限定了 

    for update

     那麼非本次事務中的 其他SQL指令會被阻塞。
  2. 如果

    where

    條件為

    主鍵

    或者

    索引列

    的時候才會鎖住行,即

    行級鎖

    。否則會鎖表,即

    鎖表

    。目前的where條件很明确是

    seckill_id

    為主鍵,是以是行級鎖。
  3. 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
           

說明

  1. 請注意每次進行update前先查詢,查詢的目的是擷取版本号。
  2. 通過對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. 釋放分布式鎖
           

說明

  1. 請注意3和4 的順序,一定要先送出事務後,再釋放分布式鎖。

    為什麼了?假設此時的庫存為1。第一個線程在第送出事務前釋放了鎖,假設送出事務需要5個機關的時間。另外一個線程在第一個線程釋放鎖的瞬間,搶占了鎖,然後在3個時間就完成了查詢和減少庫存并送出事務的操作,此時庫存為0。2個機關後第一個線程事務送出才完成,此時庫存為-1了。這樣導緻了超賣。

    是以事務的送出一定要釋放分布式鎖之前。

  2. 在Spring中事務通過是通過注解

    @Transcational

    來實作的,如果直接在

    @Transcational

    包裹的方法裡面擷取鎖和釋放鎖可能會出現超賣,此時需要通過另外一個AOP進行包裝,這裡涉及到2個知識點。
    • 一個方法有多個AOP注解時候,切面的執行順序怎麼确定的問題。該問題是每個切面都可以通過

      @order

      來定義順序,越小的越先執行,而

      @Transcational

      的order是最大,是以肯定是在内部執行。
    • :多個切面的執行順序和退出順序問題,可以參考此文:http://www.hicode.club/2018/03/01/apsect-order/

利用Redis

因為

Reis

是單線程的,是以可以通過其特性

decr

後進行判定,實際場景也是推薦這麼做的。

  1. 秒殺業務前,将秒殺商品和庫存資訊緩存到

    Redis

    中。
  2. Redis

    中緩存的商品數量做

    decr

    減1操作,如果小于0則将商品id加入

    商品售賣完成緩存

    中。避免每次都往Redis請求。

總結

技術是為了解決業務難題而存在和發展的,切記脫離業務來學習技術。

上述方案都是解決秒殺業務的初步原形,大概思路如上所述,其中其實有很多細節,後續抽時間補上。

業務處理上需要結合自己的業務進行擴充,個人推薦:

redis > 樂觀鎖 > 庫存大于0 > 分布式鎖 > for update
           
  1. 原則上盡可能的避免鎖表操作。
  2. 盡量避免請求直接打到資料庫上。

繼續閱讀