SpringBoot2 + Redis + MySQL實現(xiàn)一個搶紅包系統(tǒng)
SpringBoot2 + Redis 實現(xiàn)一個搶紅包系統(tǒng)。
本文分析一個具體的實現(xiàn)方案,不喜輕噴!
需求分析
常見的紅包系統(tǒng),由用戶指定金額、紅包總數(shù)來完成紅包的創(chuàng)建,然后通過某個入口將紅包下發(fā)至目標(biāo)用戶,用戶看到紅包后,點擊紅包,隨機獲取紅包,最后,用戶可以查看自己搶到的紅包。整個業(yè)務(wù)流程不復(fù)雜,難點在于搶紅包這個行為可能有很高的并發(fā)。所以,系統(tǒng)設(shè)計的優(yōu)化點主要關(guān)注在搶紅包這個行為上。
由于查看紅包過于簡單,所以本文不討論。那么系統(tǒng)用例就只剩下發(fā)、搶兩種。
發(fā)紅包:用戶設(shè)置紅包總金額、總數(shù)量 搶紅包:用戶從總紅包中隨機獲得一定金額
沒什么好說的,相信大家的微信紅包沒少搶,一想都明白??雌饋順I(yè)務(wù)很簡單,卻其實還有點小麻煩。首先,搶紅包必須保證高可用,不然用戶會很憤怒。其次,必須保證系統(tǒng)數(shù)據(jù)一致性不能超發(fā),不然搶到紅包的用戶收不到錢,用戶會很憤怒。最后一點,系統(tǒng)可能會有很高的并發(fā)。
OK,分析完直接進行詳細設(shè)計。所以簡簡單單只有兩個接口:發(fā)紅包、搶紅包。
表結(jié)構(gòu)設(shè)計
這里直接給出建表語句:
紅包活動表
CREATE?TABLE?`t_redpack_activity`
(
????`id`?????????bigint(20)?????NOT?NULL?COMMENT?'主鍵',
????`total_amount`?????decimal(10,?2)?NOT?NULL?DEFAULT?'0.00'?COMMENT?'總金額',
????`surplus_amount`?????decimal(10,?2)?NOT?NULL?DEFAULT?'0.00'?COMMENT?'剩余金額',
????`total`?bigint(20)?????NOT?NULL?DEFAULT?'0'?COMMENT?'紅包總數(shù)',
????`surplus_total`?bigint(20)?????NOT?NULL?DEFAULT?'0'?COMMENT?'紅包剩余總數(shù)',
????`user_id`????bigint(20)?????NOT?NULL?DEFAULT?'0'?COMMENT?'用戶編號',
????`version`?bigint(20)?????NOT?NULL?DEFAULT?'0'?COMMENT?'版本號',
????PRIMARY?KEY?(`id`)
)?ENGINE?=?InnoDB?DEFAULT?CHARSET?=?utf8;
紅包表
CREATE?TABLE?`t_redpack`
(
????`id`?????????bigint(20)?????NOT?NULL?COMMENT?'主鍵',
????`activity_id`?????????bigint(20)?????NOT?NULL?DEFAULT?0?COMMENT?'紅包活動ID',
????`amount`?????decimal(10,?2)?NOT?NULL?DEFAULT?'0.00'?COMMENT?'金額',
????`status`?????TINYINT(4)?NOT?NULL?DEFAULT?0?COMMENT?'紅包狀態(tài)?1可用?2不可用',
????`version`?bigint(20)?????NOT?NULL?DEFAULT?'0'?COMMENT?'版本號',
????PRIMARY?KEY?(`id`)
)?ENGINE?=?InnoDB?DEFAULT?CHARSET?=?utf8;
明細表
CREATE?TABLE?`t_redpack_detail`
(
????`id`?????????bigint(20)?????NOT?NULL?COMMENT?'主鍵',
????`amount`?????decimal(10,?2)?NOT?NULL?DEFAULT?'0.00'?COMMENT?'金額',
????`user_id`????bigint(20)?????NOT?NULL?DEFAULT?'0'?COMMENT?'用戶編號',
????`redpack_id`?bigint(20)?????NOT?NULL?DEFAULT?'0'?COMMENT?'紅包編號',
????`create_time`?datetime?NOT?NULL?DEFAULT?CURRENT_TIMESTAMP?COMMENT?'創(chuàng)建時間',
????`update_time`?datetime?NOT?NULL?DEFAULT?CURRENT_TIMESTAMP?ON?UPDATE?CURRENT_TIMESTAMP?COMMENT?'更新時間',
????PRIMARY?KEY?(`id`)
)?ENGINE?=?InnoDB?DEFAULT?CHARSET?=?utf8;
活動表,就是你發(fā)了多少個紅包,并且需要維護剩余金額。明細表是用戶搶到的紅包明細。紅包表是每一個具體的紅包信息。為什么需要三個表呢?事實上如果沒有紅包表也是可以的。但我們的方案預(yù)先分配紅包需要使用一張表來記錄紅包的信息,所以設(shè)計的時候才有此表。
OK,分析完表結(jié)構(gòu)其實方案已經(jīng)七七八八差不多了。請接著看下面的方案,從簡單到復(fù)雜的過度。
流程說明
本項目提供了兩個接口:
發(fā)紅包 搶紅包
方案說明
基于分布式鎖的實現(xiàn)

基于分布式鎖的實現(xiàn)最為簡單粗暴,整個搶紅包接口以activityId作為key進行加鎖,保證同一批紅包搶行為都是串行執(zhí)行。分布式鎖的實現(xiàn)是由spring-integration-redis工程提供,核心類是RedisLockRegistry。鎖通過Redis的lua腳本實現(xiàn),且實現(xiàn)了阻塞式本地可重入。
基于樂觀鎖的實現(xiàn)

第二種方式,為紅包活動表增加樂觀鎖版本控制,當(dāng)多個線程同時更新同一活動表時,只有一個clien會成功。其它失敗的client進行循環(huán)重試,設(shè)置一個最大循環(huán)次數(shù)即可。此種方案可以實現(xiàn)并發(fā)情況下的處理,但是沖突很大。因為每次只有一個人會成功,其他client需要進行重試,即使重試也只能保證一次只有一個人成功,因此TPS很低。當(dāng)設(shè)置的失敗重試次數(shù)小于發(fā)放的紅包數(shù)時,可能導(dǎo)致最后有人沒搶到紅包,實際上還有剩余紅包。
基于悲觀鎖的實現(xiàn)

由于紅包活動表增加樂觀鎖沖突很大,所以可以考慮使用使用悲觀鎖:select * from t_redpack_activity where id = #{id} for update,注意悲觀鎖必須在事務(wù)中才能使用。此時,所有的搶紅包行為變成了串行。此種情況下,悲觀鎖的效率遠大于樂觀鎖。
預(yù)先分配紅包,基于樂觀鎖的實現(xiàn)

可以看到,如果我們將樂觀鎖的維度加在紅包明細上,那么沖突又會降低。因為之前紅包明細是用戶搶到后才創(chuàng)建的,那么現(xiàn)在需要預(yù)先分配紅包,即創(chuàng)建紅包活動時即生成N個紅包,通過狀態(tài)來控制可用/不可用。這樣,當(dāng)多個client搶紅包時,獲取該活動下所有可用的紅包明細,隨機返回其中一條然后再去更新,更新成功則代表用戶搶到了該紅包,失敗則代表出現(xiàn)了沖突,可以循環(huán)進行重試。如此,沖突便被降低了。
基于Redis隊列的實現(xiàn)

和上一個方案類似,不過,用戶發(fā)放紅包時會創(chuàng)建相應(yīng)數(shù)量的紅包,并且加入到Redis隊列中。搶紅包時會將其彈出。Redis隊列很好的契合了我們的需求,每次彈出都不會出現(xiàn)重復(fù)的元素,用完即銷毀。缺陷:搶紅包時一旦從隊列彈出,此時系統(tǒng)崩潰,恢復(fù)后此隊列中的紅包明細信息已丟失,需要人工補償。
基于Redis隊列,異步入庫

這種方案的是搶到紅包后不操作數(shù)據(jù)庫,而是保存持久化信息到Redis中,然后返回成功。通過另外一個線程UserRedpackPersistConsumer,拉取持久化信息進行入庫。需要注意的是,此時的拉取動作如果使用普通的pop仍然會出現(xiàn)crash point的問題,所以考慮到可用性,此處使用Redis的BRPOPLPUSH操作,彈出元素后加入備份到另外一個隊列,保證此處崩潰后可以通過備份隊列自動恢復(fù)。崩潰恢復(fù)線程CrashRecoveryThread通過定時拉取備份信息,去DB中查證是否持久化成功,如果成功則清除此元素,否則進行補償并清除此元素。如果在操作數(shù)據(jù)庫的過程中出現(xiàn)異常會記錄錯誤日志redpack.persist.log,此日志使用單獨的文件和格式,方便進行補償(一般不會觸發(fā))。
QA
Redis掛了怎么辦?
Redis做高可用。
紅包算法使用的什么?
此工程主要展示搶紅包系統(tǒng)的設(shè)計,紅包算法不是重點,所以沒有二倍均值法之類的實現(xiàn)。
當(dāng)然,一個健壯的系統(tǒng)可能還要考慮到方方面面。發(fā)紅包本身如果是數(shù)據(jù)量特別大的情況要還需要做多副本方案。本文只是演示各種方案的優(yōu)缺點,僅供參考。另外,如果采用Redis則需要做高可用。

由于,本文涉及的源碼過多,我就不一一貼出來了,這里放一個百度網(wǎng)盤的連接,大家自行獲?。?code style="overflow-wrap: break-word;margin-right: 2px;margin-left: 2px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(53, 148, 247);background: rgba(59, 170, 250, 0.1);padding-right: 2px;padding-left: 2px;border-radius: 2px;height: 21px;line-height: 22px;">https://pan.baidu.com/s/1h1_4lcoaQO7PzDbpr6vadg?pwd=94xy。如遇資源過期,加我微信:xttblog2,補充資源。
