首頁 > 軟體

淺談Redis在秒殺場景的作用

2023-01-26 18:02:07

秒殺業務特點:限時限量,業務系統要處理瞬時高並行請求,Redis是必需品。

秒殺可分成秒殺前、秒殺中和秒殺後三階段,每個階段的請求處理需求不同,Redis具體在秒殺場景的哪個環節起到作用呢?

1 秒殺負載特徵

秒殺商品的庫存量<<購買該商品的使用者數,且會限定使用者只能在一定時間段內購買。
這給秒殺系統帶來兩個明顯負載特徵:

1.1 瞬時並行存取量很高

一般DB每秒只能支撐k級並行,而Redis並行能達到w級。所以,當大量並行請求湧入秒殺系統時,要使用Redis先攔截大部分請求,避免大量請求直接發給DB

1.2 讀多寫少

讀還是簡單的查詢操作。秒殺下,使用者需先查驗商品是否還有庫存(即根據商品ID查詢該庫存量),只有庫存有餘量時,秒殺系統才能進行庫存扣減、下單。可本地快取儲存庫存是否為 0 的標識,避免再請求 redis。

庫存查驗操作是典型KV查詢,Redis正滿足。但秒殺只有小部分使用者能成功下單,所以:
商品庫存查詢操作(讀操作)>>庫存扣減、下單操作(寫操作)

一般把秒殺活動分成三個階段:

2 秒殺階段

2.1 秒殺前

使用者不斷重新整理商品詳情頁,導致詳情頁瞬時請求量猛增。

一般儘量靜態化商品詳情頁的頁面元素,然後使用CDN或瀏覽器快取這些靜態化元素。
秒殺前的大量請求可直接由CDN或瀏覽器快取服務,不會到達伺服器端。

2.2 秒殺中

大量使用者點選商品詳情頁上的秒殺按鈕,會產生大量的並行請求查詢庫存。一旦某個請求查詢到有庫存,緊接著系統就會進行庫存扣減。然後,系統會生成實際訂單,並進行後續處理,例如訂單支付和物流服務。如果請求查不到庫存,就會返回。使用者通常會繼續點選秒殺按鈕,繼續查詢庫存。

該階段主要操作:

  • 庫存查驗
  • 庫存扣減
  • 訂單處理

每個秒殺請求都會查詢庫存,而請求只有查到有庫存餘量,後續的庫存扣減和訂單處理才會被執行。所以,該階段最大並行壓力在庫存查驗。就需使用Redis儲存庫存量,請求直接從Redis讀庫存並查驗。

庫存扣減和訂單處理是否都可交給後端DB執行?

訂單處理可在DB執行,但庫存扣減操作,不能交給DB。

為何非在DB處理訂單呢?

訂單處理涉及支付、商品出庫、物流等多個關聯操作,這些操作本身涉及DB中的多張表,要保證事務性,需在DB完成。
訂單處理時,請求壓力已不大,DB完全可支撐。

為啥庫存扣減操作不能在DB執行

一旦請求查到有庫存,即傳送該請求的使用者獲得商品購買資格,使用者就會下單了。同時,商品庫存餘量也需-1。

若把庫存扣減的操作放到DB,會帶來風險:

額外開銷
Redis儲存庫存量,而庫存量最新值又是DB在維護,所以DB更新後,還要和Redis進行同步,這增加額外操作邏輯

下單量>實際庫存量,超賣!
由於DB處理效能較慢,無法及時更新庫存餘量,可能導致大量庫存查驗請求讀到舊庫存值,並下單。就會出現下單數量>實際庫存量,導致超賣

所以,要在Redis進行庫存扣減:

  • 當庫存查驗完成後,一旦庫存有餘量,立即在Redis扣庫存
  • 為避免請求查詢到舊庫存值,庫存查驗、庫存扣減兩個操作需保證原子性

秒殺中需要Redis參與的兩個環節:

2.3 秒殺結束後

該階段,可能還有部分使用者重新整理商品詳情頁,嘗試等待有其他使用者退單。而已成功下單的使用者會重新整理訂單詳情,跟蹤訂單進度。
不過,此階段的使用者請求量已下降很多,伺服器端一般都能支撐。

3 Redis可支撐秒殺的特性

3.1 支援高並行

Redis先天支援。且若有多個秒殺商品,也可使用切片叢集,用不同範例儲存不同商品的庫存,避免使用單範例導致所有秒殺請求都集中在一個範例。
使用切片叢集時,先CRC計算不同秒殺商品K對應Slot,然後在分配Slot和範例對應關係時,才能把不同秒殺商品對應的Slot分配到不同範例儲存。

3.2 保證庫存查驗和庫存扣減的原子性

使用Redis的原子操作或分散式鎖。

4 基於原子操作支撐秒殺

秒殺中的一個商品的庫存對應兩個資訊:

  • 總庫存量
  • 已秒殺量

這種資料模型正好一個key(商品ID)對應兩個屬性(總庫存量和已秒殺量),可用Hash儲存:

key: itemID
value: {total: N, ordered: M}

itemID 商品編號total,總庫存量ordered,已秒殺量

因為庫存查驗、庫存扣減這兩個操作要保證一起執行,一個直接的方法就是使用Redis的原子操作。

庫存查驗、庫存扣減是兩個操作,需Lua指令碼保證原子執行:

#獲取商品庫存資訊            
local counts = redis.call("HMGET", KEYS[1], "total", "ordered");
#將總庫存轉換為數值
local total = tonumber(counts[1])
#將已被秒殺的庫存轉換為數值
local ordered = tonumber(counts[2])  
#如果當前請求的庫存量加上已被秒殺的庫存量仍然小於總庫存量,就可以更新庫存         
if ordered + k <= total then
    #更新已秒殺的庫存量
    redis.call("HINCRBY",KEYS[1],"ordered",k)                              return k;  
end               
return 0

然後就能在Redis使用者端,使用EVAL命令執行指令碼。使用者端根據指令碼返回值,確定秒殺是否成功:

  • 返回值k,成功
  • 0,失敗

5 基於分散式鎖支撐秒殺

讓使用者端向Redis申請分散式鎖,拿到鎖的使用者端才能執行庫存查驗、庫存扣減。
這樣大量秒殺請求就會在爭奪分散式鎖時被過濾掉。
庫存查驗、扣減也不用原子操作了,因為多個並行使用者端只有一個使用者端能夠拿到鎖,已保證使用者端並行存取的互斥性。

// 使用商品ID作為key
key = itemID
// 使用使用者端唯一標識作為value
val = clientUniqueID
//申請分散式鎖,Timeout是超時時間
lock =acquireLock(key, val, Timeout)
//當拿到鎖後,才能進行庫存查驗和扣減
if(lock == True) {
   //庫存查驗和扣減
   availStock = DECR(key, k)
   //庫存已經扣減完了,釋放鎖,返回秒殺失敗
   if (availStock < 0) {
      releaseLock(key, val)
      return error
   }
   //庫存扣減成功,釋放鎖
   else{
     releaseLock(key, val)
     //訂單處理
   }
}
//沒有拿到鎖,直接返回
else
   return

使用分散式鎖時,使用者端要先向Redis請求鎖,只有請求到鎖,才能進行庫存查驗等操作,這樣使用者端在爭搶分散式鎖時,大部分秒殺請求本身就會因為搶不到鎖而被攔截。

推薦使用切片叢集中的不同範例來分別儲存分散式鎖和商品庫存資訊。秒殺請求會先存取儲存分散式鎖的範例。若使用者端沒拿到鎖,這些使用者端就不會查詢商品庫存,減輕儲存庫存資訊的範例的壓力。

6 總結

秒殺系統是個系統性工程,Redis實現對庫存查驗、扣減環節的支撐。

此外,還有環節需要處理:

前端靜態頁面的設計

秒殺頁面上能靜態化處理的頁面元素,要儘量靜態化,充分利用CDN或瀏覽器快取服務秒殺開始前的請求

請求攔截和流控

在秒殺系統的接入層,對惡意請求進行攔截,避免對系統的惡意攻擊,例如使用黑名單禁止惡意IP進行存取。如果Redis範例的存取壓力過大,為了避免範例崩潰,我們也需要在接入層進行限流,控制進入秒殺系統的請求數量。

庫存資訊過期時間處理

Redis中儲存的庫存資訊其實是資料庫的快取,為了避免快取擊穿問題,不要給庫存資訊設定過期時間。
資料庫訂單例外處理。如果資料庫沒能成功處理訂單,可以增加訂單重試功能,保證訂單最終能被成功處理。

資源隔離

秒殺活動帶來的請求流量巨大,我們需要把秒殺商品的庫存資訊用單獨的範例儲存,而不要和日常業務系統的資料儲存在同一個範例上,這樣可以避免干擾業務系統的正常執行。

到此這篇關於淺談Redis在秒殺場景的作用的文章就介紹到這了,更多相關Redis 秒殺內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com