设计一个抢红包系统
文章目錄
- 1.需求分析
- 2.表結構設計
- 3.基于分布式鎖的實現
- 4.基于樂觀鎖的實現
- 5.基于悲觀鎖的實現
- 6.預先分配紅包,基于樂觀鎖的實現
- 7.基于 Redis 隊列的實現
- 8.基于 Redis 隊列,異步入庫
1.需求分析
常見的紅包系統,由用戶指定金額、紅包總數來完成紅包的創建,然后通過某個入口將紅包下發至目標用戶,用戶看到紅包后,點擊紅包,隨機獲取紅包,最后,用戶可以查看自己搶到的紅包。整個業務流程不復雜,難點在于搶紅包這個行為可能有很高的并發。所以,系統設計的優化點主要關注在搶紅包這個行為上。
- 發紅包:用戶設置紅包總金額、總數量
- 搶紅包:用戶從總紅包中隨機獲得一定金額
搶紅包必須保證高可用,不然用戶會很憤怒。其次,必須保證系統數據一致性不能超發,不然搶到紅包的用戶收不到錢,用戶會很憤怒。最后一點,系統可能會有很高的并發。
2.表結構設計
紅包活動表
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 '紅包總數',`surplus_total` bigint(20) NOT NULL DEFAULT '0' COMMENT '紅包剩余總數',`user_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '用戶編號',`version` bigint(20) NOT NULL DEFAULT '0' COMMENT '版本號',PRIMARY KEY (`id`) ) ENGINE = InnoDBDEFAULT 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 '紅包狀態 1可用 2不可用',`version` bigint(20) NOT NULL DEFAULT '0' COMMENT '版本號',PRIMARY KEY (`id`) ) ENGINE = InnoDBDEFAULT 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 '創建時間',`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',PRIMARY KEY (`id`) ) ENGINE = InnoDBDEFAULT CHARSET = utf8;活動表,就是你發了多少個紅包,并且需要維護剩余金額。明細表是用戶搶到的紅包明細。紅包表是每一個具體的紅包信息。為什么需要三個表呢?事實上如果沒有紅包表也是可以的。但我們的方案預先分配紅包需要使用一張表來記錄紅包的信息,所以設計的時候才有此表。
3.基于分布式鎖的實現
基于分布式鎖的實現最為簡單粗暴,整個搶紅包接口以activityId作為key進行加鎖,保證同一批紅包搶行為都是串行執行。分布式鎖的實現是由spring-integration-redis工程提供,核心類是RedisLockRegistry。鎖通過Redis的lua腳本實現,且實現了阻塞式本地可重入。
4.基于樂觀鎖的實現
第二種方式,為紅包活動表增加樂觀鎖版本控制,當多個線程同時更新同一活動表時,只有一個 clien 會成功。其它失敗的 client 進行循環重試,設置一個最大循環次數即可。此種方案可以實現并發情況下的處理,但是沖突很大。因為每次只有一個人會成功,其他 client 需要進行重試,即使重試也只能保證一次只有一個人成功,因此 TPS 很低。當設置的失敗重試次數小于發放的紅包數時,可能導致最后有人沒搶到紅包,實際上還有剩余紅包。
5.基于悲觀鎖的實現
由于紅包活動表增加樂觀鎖沖突很大,所以可以考慮使用使用悲觀鎖:select * from t_redpack_activity where id = #{id} for update,注意悲觀鎖必須在事務中才能使用。此時,所有的搶紅包行為變成了串行。此種情況下,悲觀鎖的效率遠大于樂觀鎖。
6.預先分配紅包,基于樂觀鎖的實現
可以看到,如果我們將樂觀鎖的維度加在紅包明細上,那么沖突又會降低。因為之前紅包明細是用戶搶到后才創建的,那么現在需要預先分配紅包,即創建紅包活動時即生成 N 個紅包,通過狀態來控制可用/不可用。這樣,當多個 client 搶紅包時,獲取該活動下所有可用的紅包明細,隨機返回其中一條然后再去更新,更新成功則代表用戶搶到了該紅包,失敗則代表出現了沖突,可以循環進行重試。如此,沖突便被降低了。
7.基于 Redis 隊列的實現
和上一個方案類似,不過,用戶發放紅包時會創建相應數量的紅包,并且加入到 Redis 隊列中。搶紅包時會將其彈出。Redis隊列很好的契合了我們的需求,每次彈出都不會出現重復的元素,用完即銷毀。缺陷:搶紅包時一旦從隊列彈出,此時系統崩潰,恢復后此隊列中的紅包明細信息已丟失,需要人工補償。
8.基于 Redis 隊列,異步入庫
這種方案的是搶到紅包后不操作數據庫,而是保存持久化信息到Redis中,然后返回成功。通過另外一個線程UserRedpackPersistConsumer,拉取持久化信息進行入庫。需要注意的是,此時的拉取動作如果使用普通的pop仍然會出現crash point的問題,所以考慮到可用性,此處使用Redis的BRPOPLPUSH操作,彈出元素后加入備份到另外一個隊列,保證此處崩潰后可以通過備份隊列自動恢復。崩潰恢復線程CrashRecoveryThread通過定時拉取備份信息,去 DB 中查證是否持久化成功,如果成功則清除此元素,否則進行補償并清除此元素。如果在操作數據庫的過程中出現異常會記錄錯誤日志redpack.persist.log,此日志使用單獨的文件和格式,方便進行補償(一般不會觸發)。
Redis則需要做高可用。
總結
- 上一篇: 0011__opengl 与 openg
- 下一篇: 一物一码二维码红包系统介绍