Redis秒殺實(shí)戰(zhàn)-微信搶紅包-秒殺庫(kù)存,附案例源碼(Jmeter壓測(cè))
原文鏈接:https://www.cnblogs.com/chenyanbin/p/13587508.html
已原創(chuàng)授權(quán)
為啥寫(xiě)這個(gè)微信搶紅包項(xiàng)目呢,公司 0202 年 08 月 22 日,公司周年慶,搶了100多紅包?,O(∩_∩)O哈哈~

業(yè)務(wù)流程分析

功能拆解
新建紅包
在 DB、Redis 分別新增一條記錄
搶紅包(并發(fā))
「使用技術(shù)」
Redis 中數(shù)據(jù)類型的 String 特性的原子遞減(DECR key)和減少指定值(DECRBY key decrement)
「業(yè)務(wù)」
請(qǐng)求 Redis ,當(dāng)剩余紅包個(gè)數(shù)大于 0,紅包個(gè)數(shù)原子遞減,隨機(jī)獲取紅包 計(jì)算金額,當(dāng)最后一個(gè)紅包時(shí),最后一個(gè)紅包金額=總金額-總已搶紅包金額 更新數(shù)據(jù)庫(kù)
「查詢紅包記錄」
查詢 DB 即可
數(shù)據(jù)庫(kù)設(shè)計(jì)
紅包流水表
CREATE TABLE `red_packet_info` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`red_packet_id` bigint(11) NOT NULL DEFAULT 0 COMMENT '紅包id,采?
timestamp+5位隨機(jī)數(shù)',
`total_amount` int(11) NOT NULL DEFAULT 0 COMMENT '紅包總?額,單位分',
`total_packet` int(11) NOT NULL DEFAULT 0 COMMENT '紅包總個(gè)數(shù)',
`remaining_amount` int(11) NOT NULL DEFAULT 0 COMMENT '剩余紅包?額,單位
分',
`remaining_packet` int(11) NOT NULL DEFAULT 0 COMMENT '剩余紅包個(gè)數(shù)',
`uid` int(20) NOT NULL DEFAULT 0 COMMENT '新建紅包?戶的?戶標(biāo)識(shí)',
`create_time` timestamp COMMENT '創(chuàng)建時(shí)間',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE
CURRENT_TIMESTAMP COMMENT '更新時(shí)間',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='紅包信息
表,新建?個(gè)紅包插??條記錄';
紅包記錄表
CREATE TABLE `red_packet_record` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`amount` int(11) NOT NULL DEFAULT '0' COMMENT '搶到紅包的?額',
`nick_name` varchar(32) NOT NULL DEFAULT '0' COMMENT '搶到紅包的?戶的?戶
名',
`img_url` varchar(255) NOT NULL DEFAULT '0' COMMENT '搶到紅包的?戶的頭像',
`uid` int(20) NOT NULL DEFAULT '0' COMMENT '搶到紅包?戶的?戶標(biāo)識(shí)',
`red_packet_id` bigint(11) NOT NULL DEFAULT '0' COMMENT '紅包id,采?
timestamp+5位隨機(jī)數(shù)',
`create_time` timestamp COMMENT '創(chuàng)建時(shí)間',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE
CURRENT_TIMESTAMP COMMENT '更新時(shí)間',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='搶紅包記
錄表,搶?個(gè)紅包插??條記錄';


發(fā)紅包 API
發(fā)紅包接口開(kāi)發(fā)
新增一條紅包記錄 往 mysql 里面添加一條紅包記錄 往 redis 里面添加一條紅包數(shù)量記錄 往redis里面添加一條紅包金額記錄

?往db中就單純存入一條記錄,Service層和Mapper層,就簡(jiǎn)單的一條sql語(yǔ)句,主要是提供思路,下面會(huì)附案例源碼,不要慌
?
搶紅包 API
搶紅包功能屬于原子減操作 當(dāng)大小小于 0 時(shí)原子減失敗 當(dāng)紅包個(gè)數(shù)為0時(shí),后面進(jìn)來(lái)的用戶全部搶紅包失敗,并不會(huì)進(jìn)入拆紅包環(huán)節(jié) 搶紅包功能設(shè)計(jì) 將紅包ID的請(qǐng)求放入請(qǐng)求隊(duì)列中,如果發(fā)現(xiàn)超過(guò)紅包的個(gè)數(shù),直接返回 注意事項(xiàng) 搶到紅包不一定能拆成功
搶紅包算法拆解

通過(guò)上圖算法得出,靠前面的人,手氣最佳幾率小,手氣最佳,往往在后面
發(fā) 100 元,共 10 個(gè)紅包,那么平均值是 10 元一個(gè),那么發(fā)出來(lái)的紅包金額在 0.01~20 元之間波動(dòng) 當(dāng)前面 4 個(gè)紅包總共被領(lǐng)了 30 元時(shí),剩下 70 元,總共 6 個(gè)紅包,那么這 6 個(gè)紅包的金額在 0.01~23.3 元之間波動(dòng)
搶紅包接口開(kāi)發(fā)

「測(cè)試」
「發(fā)紅包」

模擬高并發(fā)搶紅包(Jmeter壓測(cè)工具)
因?yàn)槲野l(fā)了 10 個(gè)紅包,金額是 20000,使用壓測(cè)工具,模擬50個(gè)請(qǐng)求,只允許前10個(gè)請(qǐng)求能搶到紅包,并且金額等于20000。



布隆過(guò)濾器
介紹
布隆過(guò)濾器是1970年由布隆提出的。它實(shí)際上是一個(gè)很長(zhǎng)的二進(jìn)制向量和一系列隨機(jī)映射函數(shù)。布隆過(guò)濾器可以用于檢索一個(gè)元素是否在一個(gè)集合中。它的優(yōu)點(diǎn)是空間效率和查詢時(shí)間都遠(yuǎn)遠(yuǎn)超過(guò)一般的算法,缺點(diǎn)是有一定的誤識(shí)別率和刪除困難。
優(yōu)點(diǎn)
相比于其他的數(shù)據(jù)結(jié)構(gòu),布隆過(guò)濾器在空間和時(shí)間方面都有巨大的優(yōu)勢(shì)。布隆過(guò)濾器存儲(chǔ)空間和插入/查詢時(shí)間都是常數(shù)。另外三列函數(shù)相互之間沒(méi)有關(guān)系,方便由硬件并行實(shí)現(xiàn)。布隆過(guò)濾器不需要存儲(chǔ)元素本身,在某些對(duì)保密要求非常嚴(yán)格的場(chǎng)合有優(yōu)勢(shì)。
缺點(diǎn)
但是布隆過(guò)濾器的缺點(diǎn)和有點(diǎn)一樣明顯。誤算率是其中之一。隨著存入的元素?cái)?shù)量增加,誤算率隨之增加。但是如果元素?cái)?shù)量太少,則使用散列表足矣。
布隆過(guò)濾器有什么用
黑客流量攻擊:故意訪問(wèn)不存在的數(shù)據(jù),導(dǎo)致查程序不斷訪問(wèn)DB的數(shù)據(jù) 黑客安全阻截:當(dāng)黑客訪問(wèn)不存在的緩存時(shí)迅速返回避免緩存及DB掛掉 網(wǎng)頁(yè)爬蟲(chóng)對(duì) URL 的去重,避免爬取相同的URL地址 反垃圾郵件,從數(shù)十億個(gè)垃圾郵件列表中判斷某郵件是否垃圾郵件(同理,垃圾短信) 緩存擊穿,將已存在的緩存放到布隆中,當(dāng)黑客訪問(wèn)不存在的緩存時(shí)迅速返回避免緩存及 DB 掛掉
布隆過(guò)濾器實(shí)現(xiàn)會(huì)員轉(zhuǎn)盤(pán)抽獎(jiǎng)
需求
一個(gè)抽獎(jiǎng)程序,只針對(duì)會(huì)員用戶有效

通過(guò)google布隆過(guò)濾器存儲(chǔ)會(huì)員數(shù)據(jù)
程序啟動(dòng)時(shí)將數(shù)據(jù)放入內(nèi)存中 google自動(dòng)創(chuàng)建布隆過(guò)濾器 用戶ID進(jìn)來(lái)之后判斷是否是會(huì)員
代碼實(shí)現(xiàn)
引入依賴
??com.google.guava
??guava
??29.0-jre
數(shù)據(jù)庫(kù)會(huì)員表
CREATE TABLE `sys_user` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`user_name` varchar(11) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '?戶名',
`image` varchar(11) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '?戶頭像',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8;

初始化布隆過(guò)濾器
dao 層和 dao 映射文件,就單純的一個(gè) sql 查詢,看核心方法,下面會(huì)附源碼滴,不要慌好嘛

控制層

測(cè)試

缺點(diǎn)
內(nèi)存級(jí)別產(chǎn)部 重啟即失效 本地內(nèi)存無(wú)法用在分布式場(chǎng)景 不支持大數(shù)據(jù)量存儲(chǔ)
Redis布隆過(guò)濾器
優(yōu)點(diǎn)
可擴(kuò)展性 Bloom 過(guò)濾器 不存在重啟即失效或定時(shí)任務(wù)維護(hù)的成本
缺點(diǎn)
需要網(wǎng)絡(luò)IO,性能比基于內(nèi)存的過(guò)濾器低
布隆過(guò)濾器安裝
「下載」
github:https://github.com/RedisBloom/RedisBloom
鏈接:?https://pan.baidu.com/s/16DlKLm8WGFzGkoPpy8y4Aw??密碼:?25w1
「編譯」
make

「將 Rebloom 加載到 Redis 中」
先把 Redis 給停掉!!!在 redis.conf 里面添加一行命令->加載模塊
loadmodule?/usr/soft/RedisBloom-2.2.4/redisbloom.so

「測(cè)試布隆過(guò)濾器」

SpringBoot 整合 Redis 布隆過(guò)濾器
編寫(xiě)兩個(gè)lua腳本
添加數(shù)據(jù)到指定名稱的布隆過(guò)濾器 從指定名稱的布隆過(guò)濾器獲取key是否存在的腳本

local?bloomName?=?KEYS[1]
local?value?=?KEYS[2]
--bloomFilter
local?result_1?=?redis.call('BF.ADD',bloomName,value)
return?result_1

local?bloomName?=?KEYS[1]
local?value?=?KEYS[2]
--bloomFilter
local?result_1?=?redis.call('BF.EXISTS',bloomName,value)
return?result_1
在 RedisService.java 中添加 2 個(gè)方法

驗(yàn)證

秒殺
秒殺業(yè)務(wù)流程圖


數(shù)據(jù)落地存儲(chǔ)方案
通過(guò)分布式redis減庫(kù)存 DB存最終訂單信息數(shù)據(jù)
API性能調(diào)優(yōu)
性能瓶頸在高并發(fā)秒殺 技術(shù)難題在于超賣(mài)問(wèn)題
實(shí)現(xiàn)步驟
提前將秒殺數(shù)據(jù)緩存到 redis
set?skuId_start_1?0_1554045087?--秒殺標(biāo)識(shí)
set?skuId_access_1?12000?--允許搶購(gòu)數(shù)
set?skuId_count_1?0?--搶購(gòu)計(jì)數(shù)
set?skuId_booked_1?0?--真實(shí)秒殺數(shù)
秒殺開(kāi)始前,skuId_start為0,代表活動(dòng)未開(kāi)始 當(dāng)skuId_start改為1時(shí),活動(dòng)開(kāi)始,開(kāi)始秒殺叭 當(dāng)接受下單數(shù)達(dá)到sku_count*1.2后,繼續(xù)攔截所有請(qǐng)求,商品剩余數(shù)量為0(為啥接受搶購(gòu)數(shù)為1萬(wàn)2呢,看業(yè)務(wù)流程圖,涉及到“校驗(yàn)訂單信息”,一般設(shè)置的值要比總數(shù)多一點(diǎn),多多少自己定)
利用 Redis 緩存加速增庫(kù)存數(shù)
"skuId_booked":10000?//從0開(kāi)始累加,秒殺的個(gè)數(shù)只能加到1萬(wàn)
將用戶訂單數(shù)據(jù)寫(xiě)入 MQ(異步方式)。
另外一臺(tái)服務(wù)器監(jiān)聽(tīng) mq,將訂單信息寫(xiě)入到 DB。
好了,以上就是完整的開(kāi)發(fā)步驟,下面我們開(kāi)始編寫(xiě)代碼
代碼實(shí)戰(zhàn)
網(wǎng)關(guān)瀏覽攔截層
1、先判斷秒殺是否已經(jīng)開(kāi)始
2、利用 Redis 緩存 incr 攔截流量
用 incr 方法原子加 通過(guò)原子加帕努單當(dāng)前 skuId_access 是否達(dá)到最大值
訂單信息校驗(yàn)層
1、校驗(yàn)當(dāng)前用戶是否已經(jīng)買(mǎi)過(guò)這個(gè)商品
需要存儲(chǔ)用戶的uid 存數(shù)據(jù)庫(kù)效率太低 存Redis value方式數(shù)據(jù)太大 存布隆過(guò)濾器性能高且數(shù)據(jù)量小(推薦)
2、校驗(yàn)通過(guò)直接返回?fù)屬?gòu)成功
開(kāi)發(fā)lua腳本實(shí)現(xiàn)庫(kù)存扣除
1、庫(kù)存扣除成功,獲取當(dāng)前最新庫(kù)存
2、如果庫(kù)存大于0,即馬上進(jìn)行庫(kù)存扣除,并且訪問(wèn)搶購(gòu)成功給用戶
3、考慮原子性問(wèn)題
保證原子性的方式,采用 lua 腳本 采用lua腳本方式保證原子性帶來(lái)缺點(diǎn),性能有所下降 不保證原子性缺點(diǎn),放入請(qǐng)求量可能大于預(yù)期 當(dāng)前扣除庫(kù)存場(chǎng)景必須保證原子性,否則會(huì)導(dǎo)致超賣(mài)
4、返回?fù)屬?gòu)結(jié)果
搶購(gòu)成功 庫(kù)存沒(méi)了,搶購(gòu)失敗
控制層

Service 層

布隆過(guò)濾器

初始化redis緩存

set?skuId_start_1?0_1554045087?--秒殺標(biāo)識(shí)
set?skuId_access_1?12000?--允許搶購(gòu)數(shù)
set?skuId_count_1?0?--搶購(gòu)計(jì)數(shù)
set?skuId_booked_1?0?--真實(shí)秒殺數(shù)
秒殺驗(yàn)證
jmeter 配置

壓測(cè)秒殺驗(yàn)證原子性



項(xiàng)目下載

鏈接(奶牛?快傳):?https://cowtransfer.com/s/74998eaf64da44??取件碼:?rqzbyj尾聲
演示的時(shí)候,我使用的 Redis 單機(jī)的,吞吐量不是很大,感興趣的,可以自己搭建個(gè) Redis 主從復(fù)制+哨兵+集群,然后再測(cè)試。
最近比較忙,沒(méi)時(shí)間完善微信搶紅包秒殺的原子性。下面那個(gè)完整案例搶庫(kù)存的,親自使用 Jmeter 壓測(cè)幾次,是原子性的,可以拿來(lái)借鑒,感興趣的同學(xué),可以借鑒下面搶庫(kù)存的代碼,把微信搶紅包的功能在完善下,我就不修改啦。
閑聊
蘇州之行,被敖丙、帥地、3y、小林這幾個(gè)狗賊叫“小胖墩”。我能忍不?那肯定不能!回去就減肥。而且,我真的也不胖的嘛!

這是昨晚的晚餐,不過(guò),晚上沒(méi)抗住,還是去吃了夜宵。。。

文章有幫助可以點(diǎn)個(gè)「在看」或「分享」,都是支持,我都喜歡!
我是Guide哥,Java后端開(kāi)發(fā),會(huì)一點(diǎn)前端知識(shí),喜歡烹飪,自由的少年。一個(gè)三觀比主角還正的技術(shù)人。我們下期再見(jiàn)!
往期推薦
