解密 Redis 助力雙 11 背后電商秒殺系統(tǒng)
掃描二維碼
隨時隨地手機看文章
背景
秒殺的特征
秒殺系統(tǒng)
背景
秒殺活動是絕大部分電商選擇的低價促銷、推廣品牌的方式。不僅可以給平臺帶來用戶量,還可以提高平臺知名度。
一個好的秒殺系統(tǒng),可以提高平臺系統(tǒng)的穩(wěn)定性和公平性,獲得更好的用戶體驗,提升平臺的口碑,從而提升秒殺活動的最大價值。
本文討論云數(shù)據(jù)庫 Redis 版緩存設(shè)計高并發(fā)的秒殺系統(tǒng)。
秒殺的特征
秒殺活動對稀缺或者特價的商品進(jìn)行定時定量售賣,吸引成大量的消費者進(jìn)行搶購,但又只有少部分消費者可以下單成功。因此,秒殺活動將在較短時間內(nèi)產(chǎn)生比平時大數(shù)十倍,上百倍的頁面訪問流量和下單請求流量。
秒殺活動可以分為3個階段:
秒殺前:用戶不斷刷新商品詳情頁,頁面請求達(dá)到瞬時峰值。
秒殺開始:用戶點擊秒殺按鈕,下單請求達(dá)到瞬時峰值。
秒殺后:一部分成功下單的用戶不斷刷新訂單或者產(chǎn)生退單操作,大部分用戶繼續(xù)刷新商品詳情頁等待退單機會。
消費者提交訂單,一般做法是利用數(shù)據(jù)庫的行級鎖,只有搶到鎖的請求可以進(jìn)行庫存查詢和下單操作。
但是在高并發(fā)的情況下,數(shù)據(jù)庫無法承擔(dān)如此大的請求,往往會使整個服務(wù) blocked,在消費者看來就是服務(wù)器宕機。
秒殺系統(tǒng)
秒殺系統(tǒng)的流量雖然很高,但是實際有效流量是十分有限的。利用系統(tǒng)的層次結(jié)構(gòu),在每個階段提前校驗,攔截?zé)o效流量,可以減少大量無效的流量涌入數(shù)據(jù)庫。
利用瀏覽器緩存和 CDN 抗壓靜態(tài)頁面流量
秒殺前,用戶不斷刷新商品詳情頁,造成大量的頁面請求。所以,我們需要把秒殺商品詳情頁與普通的商品詳情頁分開。
對于秒殺商品詳情頁盡量將能靜態(tài)化的元素靜態(tài)化處理,除了秒殺按鈕需要服務(wù)端進(jìn)行動態(tài)判斷,其他的靜態(tài)數(shù)據(jù)可以緩存在瀏覽器和 CDN 上。這樣,秒殺前刷新頁面導(dǎo)致的流量進(jìn)入服務(wù)端的流量只有很小的一部分。
利用讀寫分離 Redis 緩存攔截流量
CDN 是第一級流量攔截,第二級流量攔截我們使用支持讀寫分離的 Redis。在這一階段我們主要讀取數(shù)據(jù),讀寫分離 Redis 能支持高達(dá)60萬以上 qps,完全可以支持需求。
首先通過數(shù)據(jù)控制模塊,提前將秒殺商品緩存到讀寫分離 Redis,并設(shè)置秒殺開始標(biāo)記如下:
"goodsId_count": 100 //總數(shù)
"goodsId_start": 0 //開始標(biāo)記
"goodsId_access": 0 //接受下單數(shù)
秒殺開始前,服務(wù)集群讀取 goodsId_Start 為 0,直接返回未開始。
數(shù)據(jù)控制模塊將 goodsId_start 改為1,標(biāo)志秒殺開始。
服務(wù)集群緩存開始標(biāo)記位并開始接受請求,并記錄到 redis 中 goodsId_access,商品剩余數(shù)量為(goodsId_count - goodsId_access)。
當(dāng)接受下單數(shù)達(dá)到 goodsId_count 后,繼續(xù)攔截所有請求,商品剩余數(shù)量為 0。
可以看出,最后成功參與下單的請求只有少部分可以被接受。在高并發(fā)的情況下,允許稍微多的流量進(jìn)入。因此可以控制接受下單數(shù)的比例。
利用主從版 Redis 緩存加速庫存扣量
成功參與下單后,進(jìn)入下層服務(wù),開始進(jìn)行訂單信息校驗,庫存扣量。
為了避免直接訪問數(shù)據(jù)庫,我們使用主從版 Redis 來進(jìn)行庫存扣量,主從版 Redis 提供10萬級別的 QPS。使用 Redis 來優(yōu)化庫存查詢,提前攔截秒殺失敗的請求,將大大提高系統(tǒng)的整體吞吐量。
通過數(shù)據(jù)控制模塊提前將庫存存入 Redis,將每個秒殺商品在 Redis 中用一個 hash 結(jié)構(gòu)表示。
"goodsId" : {
"Total": 100
"Booked": 100
}
扣量時,服務(wù)器通過請求 Redis 獲取下單資格,通過以下 lua 腳本實現(xiàn),由于 Redis 是單線程模型,lua 可以保證多個命令的原子性。
local n = tonumber(ARGV[1])
if not n or n == 0 then
return 0
end
local vals = redis.call("HMGET", KEYS[1], "Total", "Booked");
local total = tonumber(vals[1])
local blocked = tonumber(vals[2])
if not total or not blocked then
return 0
end
if blocked + n <= total then
redis.call("HINCRBY", KEYS[1], "Booked", n)
return n;
end
return 0
先使用SCRIPT LOAD
將 lua 腳本提前緩存在 Redis,然后調(diào)用EVALSHA
調(diào)用腳本,比直接調(diào)用EVAL
節(jié)省網(wǎng)絡(luò)帶寬:
redis 127.0.0.1:6379>SCRIPT LOAD "lua code"
"438dd755f3fe0d32771753eb57f075b18fed7716"
redis 127.0.0.1:6379>EVAL 438dd755f3fe0d32771753eb57f075b18fed7716 1 goodsId 1
秒殺服務(wù)通過判斷 Redis 是否返回?fù)屬弬€數(shù) n,即可知道此次請求是否扣量成功。
使用主從版 Redis 實現(xiàn)簡單的消息隊列異步下單入庫
扣量完成后,需要進(jìn)行訂單入庫。如果商品數(shù)量較少的時候,直接操作數(shù)據(jù)庫即可。
如果秒殺的商品是1萬,甚至10萬級別,那數(shù)據(jù)庫鎖沖突將帶來很大的性能瓶頸。
因此,利用消息隊列組件,當(dāng)秒殺服務(wù)將訂單信息寫入消息隊列后,即可認(rèn)為下單完成,避免直接操作數(shù)據(jù)庫。
消息隊列組件依然可以使用 Redis 實現(xiàn),在 R2 中用 list 數(shù)據(jù)結(jié)構(gòu)表示。
orderList {
[0] = {訂單內(nèi)容}
[1] = {訂單內(nèi)容}
[2] = {訂單內(nèi)容}
...
}將訂單內(nèi)容寫入 Redis:
LPUSH orderList {訂單內(nèi)容}
異步下單模塊從 Redis 中順序獲取訂單信息,并將訂單寫入數(shù)據(jù)庫。
BRPOP orderList 0
通過使用 Redis 作為消息隊列,異步處理訂單入庫,有效的提高了用戶的下單完成速度。
數(shù)據(jù)控制模塊管理秒殺數(shù)據(jù)同步
最開始,利用讀寫分離 Redis 進(jìn)行流量限制,只讓部分流量進(jìn)入下單。對于下單檢驗失敗和退單等情況,需要讓更多的流量進(jìn)來。
因此,數(shù)據(jù)控制模塊需要定時將數(shù)據(jù)庫中的數(shù)據(jù)進(jìn)行一定的計算,同步到主從版 Redis,同時再同步到讀寫分離的 Redis,讓更多的流量進(jìn)來。
End
作者:AlibabaCloud , 本文版權(quán)歸作者所有
https://github.com/AlibabaCloudDocs
特別推薦一個分享架構(gòu)+算法的優(yōu)質(zhì)內(nèi)容,還沒關(guān)注的小伙伴,可以長按關(guān)注一下:
長按訂閱更多精彩▼
如有收獲,點個在看,誠摯感謝
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務(wù)。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!