SpringBoot2 + Redis + MySQL實(shí)現(xiàn)一個(gè)搶紅包系統(tǒng)
來源:網(wǎng)絡(luò)

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

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

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

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

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

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

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


