外貿(mào)領(lǐng)航
首頁展會(huì)信息 > 如何使用redis來實(shí)現(xiàn)秒殺「redis秒殺」

如何使用redis來實(shí)現(xiàn)秒殺「redis秒殺」

來源:互聯(lián)網(wǎng) 2024-07-17 14:04:01

redis實(shí)現(xiàn)秒殺

背景:

某電商網(wǎng)站實(shí)現(xiàn)秒殺功能,用戶在某個(gè)時(shí)間段內(nèi)能夠搶購(gòu)到特價(jià)商品,且某一商品最多只能被同一用戶搶購(gòu)一次。

基本思路:

秒殺商品由商家后臺(tái)添加,秒殺商品數(shù)據(jù)保存在tb_seckilll_goods表中,關(guān)鍵字段包括:id,status(審核狀態(tài)),start_time(開始時(shí)間),end_time(結(jié)束時(shí)間),stock_count(庫存量);寫一個(gè)定時(shí)器,定時(shí)從秒殺商品表中掃描數(shù)據(jù),將符合條件的商品加載到緩存中;條件:審核狀態(tài)="1",start_time < 當(dāng)前時(shí)間 < end_time,庫存量大于0;前端展示,此處略點(diǎn)擊搶購(gòu),拿著秒殺商品的id去緩存中查詢,如果緩存中商品不存在或者為空,提示“已售罄”,否則生成訂單,保存到緩存中,訂單表tb_seckill_order庫存-1,判斷減完之后緩存中商品的庫存是否大于0,大于0則更新緩存,否則刪除該秒殺商品的緩存,并更新到數(shù)據(jù)庫

技術(shù)選型:緩存redis,定時(shí)器:spring整合quartz

如下完成了一個(gè)基本的秒殺下單的業(yè)務(wù):

掃描秒殺商品加載到redis:

@Scheduled(cron = "0 */1 * * * ?")//cron表達(dá)式:每分鐘執(zhí)行一次,周期可任意定義

public void importToRedis(){

//1.查詢合法秒殺商品數(shù)據(jù)

TbSeckillGoodsExample example = new TbSeckillGoodsExample();

Date date = new Date();

example.createCriteria().andStatusEqualTo("1").andStockCountGreaterThan(0)

.andStartTimeLessThan(date).andEndTimeGreaterThan(date);

List<TbSeckillGoods> tbSeckillGoods = seckillGoodsMapper.selectByExample(example);

for (TbSeckillGoods seckillGood : tbSeckillGoods) {//將秒殺商品依次存入redis

//注意如果redis中已經(jīng)有的商品,則不更新,只添加之前未加入過的秒殺商品

if(redisTemplate.boundHashOps("TbSeckillGoods").get(seckillGood.getId()) == null){

redisTemplate.boundHashOps("TbSeckillGoods").put(seckillGood.getId(), seckillGood);

}

}

}

對(duì)所有的秒殺商品都使用同一個(gè)key:“TbSeckillGoods”,值的存儲(chǔ)類型為hash

下單的service代碼:

public Result saveOrder(Long id, String userId) {

//根據(jù)商品id從redis中查出商品

TbSeckillGoods seckillGood = (TbSeckillGoods) redisTemplate.boundHashOps(TbSeckillGoods.class.getSimpleName()).get(id);

//如果緩存中秒殺商品不存在或者庫存為空,則提示已售罄

if(seckillGood == null || seckillGood.getStockCount() <= 0){

return new Result(false, "已售罄");

}

//如果時(shí)間已截止,提示秒殺時(shí)間已結(jié)束

if(seckillGood.getEndTime().getTime() < System.currentTimeMillis()){

return new Result(false, "活動(dòng)已結(jié)束");

}

//生成訂單保存到緩存中

TbSeckillOrder seckillOrder = new TbSeckillOrder();

seckillOrder.setUserId(userId);

seckillOrder.setSeckillId(idWorker.nextId());

seckillOrder.setSellerId(seckillGood.getSellerId());

seckillOrder.setMoney(seckillGood.getCostPrice());

seckillOrder.setStatus("0");//未支付

seckillOrder.setCreateTime(new Date());

redisTemplate.boundHashOps(TbSeckillOrder.class.getSimpleName()).put(userId, seckillOrder);

//秒殺商品庫存量減1

seckillGood.setStockCount(seckillGood.getStockCount() - 1);

//判斷減完之后redis中商品的庫存是否大于0,大于0則更新緩存,否則刪除該秒殺商品的緩存,并更新到數(shù)據(jù)庫

if(seckillGood.getStockCount() > 0){

redisTemplate.boundHashOps(TbSeckillGoods.class.getSimpleName()).put(seckillGood.getGoodsId(), seckillGood);

}else {

redisTemplate.boundHashOps(TbSeckillGoods.class.getSimpleName()).delete(seckillGood.getGoodsId());

seckillGoodsMapper.updateByPrimaryKey(seckillGood);

}

return new Result(true, "恭喜您搶購(gòu)到商品,請(qǐng)盡快支付");

}

以上是關(guān)鍵代碼,其他業(yè)務(wù)代碼可不關(guān)注,完整代碼可在我的github中查看

分析上述代碼:

上述代碼在多線程環(huán)境下存在三個(gè)問題:

1.超賣:

if(seckillGood == null || seckillGood.getStockCount() <= 0){

return new Result(false, "已售罄");

}

業(yè)務(wù)邏輯是如果seckillGood不為null,且?guī)齑?gt;0,即可進(jìn)行下單,但是在實(shí)際環(huán)境中,可能會(huì)有很多的用戶同時(shí)獲取到redis中的商品信息,每個(gè)用戶讀取到的庫存量一樣且均大于0,假如庫存只有2,但是有三個(gè)用戶都符合下單條件,就出現(xiàn)了超賣情況

2.沒有對(duì)用戶多次搶購(gòu)做限制

3.下單和生成訂單串行,影響并發(fā)效率。完全可以在用戶搶購(gòu)之后立即能夠下單成功,后續(xù)的訂單處理可以利用多線程來異步操作

解決方案:

1.對(duì)于超賣問題,很容易想到是就是對(duì)下單操作加鎖,一次只能有一個(gè)用戶進(jìn)行下單并減庫存。這種方法可以避免超賣問題,但是卻會(huì)導(dǎo)致效率下降。

redis中有一種存儲(chǔ)結(jié)構(gòu)list,它的元素在彈出時(shí)能夠保證一次只有一個(gè)線程進(jìn)行操作,并且效率比較高。例如,我們?cè)阡浫朊霘⑸唐返耐瑫r(shí),對(duì)每一種商品都創(chuàng)建一個(gè)list,該商品的庫存有多少,list中的元素就有多少個(gè),每次下單就從list中彈出一個(gè)元素,防止超賣。

如圖:以“SECKILLGOODS_ID_PREFIX_秒殺商品ID”的格式字符串作為list的key,商品庫存有n,則該list就有n個(gè)元素,元素的壓入在錄入商品時(shí)完成,每下單一次,就彈出一個(gè)元素。

2.對(duì)于同一用戶多次搶購(gòu)的問題,我們同樣可以使用redis來記錄每種商品已搶購(gòu)成功的用戶id,我們使用set來記錄用戶id,防止用戶id重復(fù)

如圖:以“USER_ID_PREFIX_秒殺商品ID”的格式字符串作為set的key,一旦有一個(gè)用戶搶購(gòu)了該商品,則在先判斷Set集合中是否存在用戶id,不存在則添加

3.多線程處理訂單,在redis中創(chuàng)建一個(gè)隊(duì)列,每當(dāng)一個(gè)用戶成功搶購(gòu)一個(gè)商品,就往隊(duì)列中壓入一個(gè)下單數(shù)據(jù),包含商品id和用戶id即可。線程從隊(duì)列中彈出一個(gè)包含下單數(shù)據(jù)的元素,進(jìn)行訂單的生成

如圖:OrederRecorder作為key,集合中記錄了搶購(gòu)成功的商品id和用戶id,等待多線程去從集合中彈出元素進(jìn)行處理

整個(gè)秒殺業(yè)務(wù)的大致流程如下:

完整代碼可參考https://github.com/ithushuai/seckill-demo

秒殺活動(dòng)是絕大部分電商選擇的低價(jià)促銷、推廣品牌的方式。不僅可以給平臺(tái)帶來用戶量,還可以提高平臺(tái)知名度。一個(gè)好的秒殺系統(tǒng),可以提高平臺(tái)系統(tǒng)的穩(wěn)定性和公平性,獲得更好的用戶體驗(yàn),提升平臺(tái)的口碑,從而提升秒殺活動(dòng)的最大價(jià)值。本文討論云數(shù)據(jù)庫Redis版緩存設(shè)計(jì)高并發(fā)的秒殺系統(tǒng)。

秒殺的特征

秒殺活動(dòng)對(duì)稀缺或者特價(jià)的商品進(jìn)行定時(shí)定量售賣,吸引成大量的消費(fèi)者進(jìn)行搶購(gòu),但又只有少部分消費(fèi)者可以下單成功。因此,秒殺活動(dòng)將在較短時(shí)間內(nèi)產(chǎn)生比平時(shí)大數(shù)十倍,上百倍的頁面訪問流量和下單請(qǐng)求流量。

秒殺活動(dòng)可以分為3個(gè)階段:

秒殺前:用戶不斷刷新商品詳情頁,頁面請(qǐng)求達(dá)到瞬時(shí)峰值。秒殺開始:用戶點(diǎn)擊秒殺按鈕,下單請(qǐng)求達(dá)到瞬時(shí)峰值。秒殺后:一部分成功下單的用戶不斷刷新訂單或者產(chǎn)生退單操作,大部分用戶繼續(xù)刷新商品詳情頁等待退單機(jī)會(huì)。

消費(fèi)者提交訂單,一般做法是利用數(shù)據(jù)庫的行級(jí)鎖,只有搶到鎖的請(qǐng)求可以進(jìn)行庫存查詢和下單操作。但是在高并發(fā)的情況下,數(shù)據(jù)庫無法承擔(dān)如此大的請(qǐng)求,往往會(huì)使整個(gè)服務(wù)blocked,在消費(fèi)者看來就是服務(wù)器宕機(jī)。

秒殺系統(tǒng)

秒殺系統(tǒng)的流量雖然很高,但是實(shí)際有效流量是十分有限的。利用系統(tǒng)的層次結(jié)構(gòu),在每個(gè)階段提前校驗(yàn),攔截?zé)o效流量,可以減少大量無效的流量涌入數(shù)據(jù)庫。

利用瀏覽器緩存和CDN抗壓靜態(tài)頁面流量

秒殺前,用戶不斷刷新商品詳情頁,造成大量的頁面請(qǐng)求。所以,我們需要把秒殺商品詳情頁與普通的商品詳情頁分開。對(duì)于秒殺商品詳情頁盡量將能靜態(tài)化的元素靜態(tài)化處理,除了秒殺按鈕需要服務(wù)端進(jìn)行動(dòng)態(tài)判斷,其他的靜態(tài)數(shù)據(jù)可以緩存在瀏覽器和CDN上。這樣,秒殺前刷新頁面導(dǎo)致的流量進(jìn)入服務(wù)端的流量只有很小的一部分。

利用讀寫分離Redis緩存攔截流量

CDN是第一級(jí)流量攔截,第二級(jí)流量攔截我們使用支持讀寫分離的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)記位并開始接受請(qǐng)求,并記錄到Redis中g(shù)oodsId_access,商品剩余數(shù)量為(goodsId_count - goodsId_access)。當(dāng)接受下單數(shù)達(dá)到goodsId_count后,繼續(xù)攔截所有請(qǐng)求,商品剩余數(shù)量為0。

可以看出,最后成功參與下單的請(qǐng)求只有少部分可以被接受。在高并發(fā)的情況下,允許稍微多的流量進(jìn)入。因此可以控制接受下單數(shù)的比例。

利用主從版Redis緩存加速庫存扣量

成功參與下單后,進(jìn)入下層服務(wù),開始進(jìn)行訂單信息校驗(yàn),庫存扣量。為了避免直接訪問數(shù)據(jù)庫,我們使用主從版Redis來進(jìn)行庫存扣量,主從版Redis提供10萬級(jí)別的QPS。使用Redis來優(yōu)化庫存查詢,提前攔截秒殺失敗的請(qǐng)求,將大大提高系統(tǒng)的整體吞吐量。

通過數(shù)據(jù)控制模塊提前將庫存存入Redis,將每個(gè)秒殺商品在Redis中用一個(gè)hash結(jié)構(gòu)表示。

"goodsId" : {

"Total": 100

"Booked": 100

}

扣量時(shí),服務(wù)器通過請(qǐng)求Redis獲取下單資格,通過以下lua腳本實(shí)現(xiàn),由于Redis是單線程模型,lua可以保證多個(gè)命令的原子性。

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>EVALSHA 438dd755f3fe0d32771753eb57f075b18fed7716 1 goodsId 1

秒殺服務(wù)通過判斷Redis是否返回?fù)屬?gòu)個(gè)數(shù)n,即可知道此次請(qǐng)求是否扣量成功。

使用主從版Redis實(shí)現(xiàn)簡(jiǎn)單的消息隊(duì)列異步下單入庫

扣量完成后,需要進(jìn)行訂單入庫。如果商品數(shù)量較少的時(shí)候,直接操作數(shù)據(jù)庫即可。如果秒殺的商品是1萬,甚至10萬級(jí)別,那數(shù)據(jù)庫鎖沖突將帶來很大的性能瓶頸。因此,利用消息隊(duì)列組件,當(dāng)秒殺服務(wù)將訂單信息寫入消息隊(duì)列后,即可認(rèn)為下單完成,避免直接操作數(shù)據(jù)庫。

消息隊(duì)列組件依然可以使用Redis實(shí)現(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作為消息隊(duì)列,異步處理訂單入庫,有效的提高了用戶的下單完成速度。

數(shù)據(jù)控制模塊管理秒殺數(shù)據(jù)同步

最開始,利用讀寫分離Redis進(jìn)行流量限制,只讓部分流量進(jìn)入下單。對(duì)于下單檢驗(yàn)失敗和退單等情況,需要讓更多的流量進(jìn)來。因此,數(shù)據(jù)控制模塊需要定時(shí)將數(shù)據(jù)庫中的數(shù)據(jù)進(jìn)行一定的計(jì)算,同步到主從版Redis,同時(shí)再同步到讀寫分離的Redis,讓更多的流量進(jìn)來

鄭重聲明:本文版權(quán)歸原作者所有,轉(zhuǎn)載文章僅為傳播更多信息之目的,如有侵權(quán)行為,請(qǐng)第一時(shí)間聯(lián)系我們修改或刪除,多謝。

CopyRight ? 外貿(mào)領(lǐng)航 2023 All Rights Reserved.