Skip to content

Commit def49ee

Browse files
committed
场景设计-抢红包系统
1 parent a324a10 commit def49ee

File tree

3 files changed

+269
-0
lines changed

3 files changed

+269
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@
252252
- [如何用 Redis 统计用户访问量?](docs/advance/system-design/10-pdd-visit-statistics)
253253
- [实时订阅推送设计](docs/advance/system-design/11-realtime-subscribe-push.md)
254254
- [权限系统设计方案](docs/advance/system-design/13-permission-system.md)
255+
- [如何设计一个抢红包系统](docs/advance/system-design/15-red-packet.md)
255256

256257
# 安全
257258

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# 如何设计一个抢红包系统
2+
3+
## 前言
4+
5+
本篇分享如何设计一个抢红包系统,希望对大家有所帮助。主要展示抢红包系统的设计,红包算法不是重点,所以没有二倍均值法之类的实现。
6+
7+
## 需求分析
8+
9+
常见的红包系统,由用户指定金额、红包总数来完成红包的创建,然后通过某个入口将红包下发至目标用户,用户看到红包后,点击红包,随机获取红包,最后,用户可以查看自己抢到的红包。整个业务流程不复杂,难点在于抢红包这个行为可能有很高的并发。所以,系统设计的优化点主要关注在抢红包这个行为上。
10+
11+
由于查看红包过于简单,所以本文不讨论。那么系统用例就只剩下`发、抢`两种。
12+
13+
1. 发红包:用户设置红包总金额、总数量
14+
2. 抢红包:用户从总红包中随机获得一定金额
15+
16+
没什么好说的,相信大家的微信红包没少抢,一想都明白。看起来业务很简单,却其实还有点小麻烦。首先,抢红包必须保证高可用,不然用户会很愤怒。其次,必须保证系统数据一致性不能超发,不然抢到红包的用户收不到钱,用户会很愤怒。最后一点,系统可能会有很高的并发。
17+
18+
OK,分析完直接进行详细设计。所以简简单单只有两个接口:
19+
20+
1. 发红包
21+
2. 抢红包
22+
23+
## 表结构设计
24+
25+
这里直接给出建表语句:
26+
27+
红包活动表:
28+
29+
```sql
30+
CREATE TABLE `t_redpack_activity`
31+
(
32+
`id` bigint(20) NOT NULL COMMENT '主键',
33+
`total_amount` decimal(10, 2) NOT NULL DEFAULT '0.00' COMMENT '总金额',
34+
`surplus_amount` decimal(10, 2) NOT NULL DEFAULT '0.00' COMMENT '剩余金额',
35+
`total` bigint(20) NOT NULL DEFAULT '0' COMMENT '红包总数',
36+
`surplus_total` bigint(20) NOT NULL DEFAULT '0' COMMENT '红包剩余总数',
37+
`user_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '用户编号',
38+
`version` bigint(20) NOT NULL DEFAULT '0' COMMENT '版本号',
39+
PRIMARY KEY (`id`)
40+
) ENGINE = InnoDB
41+
DEFAULT CHARSET = utf8;
42+
复制代码
43+
```
44+
45+
红包表:
46+
47+
```sql
48+
CREATE TABLE `t_redpack`
49+
(
50+
`id` bigint(20) NOT NULL COMMENT '主键',
51+
`activity_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '红包活动ID',
52+
`amount` decimal(10, 2) NOT NULL DEFAULT '0.00' COMMENT '金额',
53+
`status` TINYINT(4) NOT NULL DEFAULT 0 COMMENT '红包状态 1可用 2不可用',
54+
`version` bigint(20) NOT NULL DEFAULT '0' COMMENT '版本号',
55+
PRIMARY KEY (`id`)
56+
) ENGINE = InnoDB
57+
DEFAULT CHARSET = utf8;
58+
复制代码
59+
```
60+
61+
明细表:
62+
63+
```sql
64+
CREATE TABLE `t_redpack_detail`
65+
(
66+
`id` bigint(20) NOT NULL COMMENT '主键',
67+
`amount` decimal(10, 2) NOT NULL DEFAULT '0.00' COMMENT '金额',
68+
`user_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '用户编号',
69+
`redpack_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '红包编号',
70+
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
71+
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
72+
PRIMARY KEY (`id`)
73+
) ENGINE = InnoDB
74+
DEFAULT CHARSET = utf8;
75+
复制代码
76+
```
77+
78+
活动表,就是你发了多少个红包,并且需要维护剩余金额。明细表是用户抢到的红包明细。红包表是每一个具体的红包信息。为什么需要三个表呢?事实上如果没有红包表也是可以的。但我们的方案`预先分配红包`需要使用一张表来记录红包的信息,所以设计的时候才有此表。
79+
80+
OK,分析完表结构其实方案已经七七八八差不多了。请接着看下面的方案,从简单到复杂的过度。
81+
82+
## 基于分布式锁的实现
83+
84+
![](http://img.topjavaer.cn/img/抢红包1.png)
85+
86+
基于分布式锁的实现最为简单粗暴,整个抢红包接口以`activityId`作为`key`进行加锁,保证同一批红包抢行为都是串行执行。分布式锁的实现是由`spring-integration-redis`工程提供,核心类是`RedisLockRegistry`。锁通过`Redis``lua`脚本实现,且实现了阻塞式本地可重入。
87+
88+
## 基于乐观锁的实现
89+
90+
![](http://img.topjavaer.cn/img/抢红包2.png)
91+
92+
第二种方式,为红包活动表增加乐观锁版本控制,当多个线程同时更新同一活动表时,只有一个 clien 会成功。其它失败的 client 进行循环重试,设置一个最大循环次数即可。此种方案可以实现并发情况下的处理,但是冲突很大。因为每次只有一个人会成功,其他 client 需要进行重试,即使重试也只能保证一次只有一个人成功,因此 TPS 很低。当设置的失败重试次数小于发放的红包数时,可能导致最后有人没抢到红包,实际上还有剩余红包。
93+
94+
## 基于悲观锁的实现
95+
96+
![](http://img.topjavaer.cn/img/抢红包3.png)
97+
98+
由于红包活动表增加乐观锁冲突很大,所以可以考虑使用使用悲观锁:`select * from t_redpack_activity where id = #{id} for update`,注意悲观锁必须在事务中才能使用。此时,所有的抢红包行为变成了串行。此种情况下,悲观锁的效率远大于乐观锁。
99+
100+
## 预先分配红包,基于乐观锁的实现
101+
102+
![](http://img.topjavaer.cn/img/抢红包4.png)
103+
104+
可以看到,如果我们将乐观锁的维度加在红包明细上,那么冲突又会降低。因为之前红包明细是用户抢到后才创建的,那么现在需要预先分配红包,即创建红包活动时即生成 N 个红包,通过状态来控制可用/不可用。这样,当多个 client 抢红包时,获取该活动下所有可用的红包明细,随机返回其中一条然后再去更新,更新成功则代表用户抢到了该红包,失败则代表出现了冲突,可以循环进行重试。如此,冲突便被降低了。
105+
106+
## 基于 Redis 队列的实现
107+
108+
![](http://img.topjavaer.cn/img/抢红包5.png)
109+
110+
和上一个方案类似,不过,用户发放红包时会创建相应数量的红包,并且加入到 Redis 队列中。抢红包时会将其弹出。`Redis`队列很好的契合了我们的需求,每次弹出都不会出现重复的元素,用完即销毁。缺陷:抢红包时一旦从队列弹出,此时系统崩溃,恢复后此队列中的红包明细信息已丢失,需要人工补偿。
111+
112+
## 基于 Redis 队列,异步入库
113+
114+
![](http://img.topjavaer.cn/img/抢红包6.png)
115+
116+
这种方案的是抢到红包后不操作数据库,而是保存持久化信息到`Redis`中,然后返回成功。通过另外一个线程`UserRedpackPersistConsumer`,拉取持久化信息进行入库。需要注意的是,此时的拉取动作如果使用普通的`pop`仍然会出现`crash point`的问题,所以考虑到可用性,此处使用`Redis``BRPOPLPUSH`操作,弹出元素后加入备份到另外一个队列,保证此处崩溃后可以通过备份队列自动恢复。崩溃恢复线程`CrashRecoveryThread`通过定时拉取备份信息,去 DB 中查证是否持久化成功,如果成功则清除此元素,否则进行补偿并清除此元素。如果在操作数据库的过程中出现异常会记录错误日志`redpack.persist.log`,此日志使用单独的文件和格式,方便进行补偿(一般不会触发)。
117+
118+
## 后语
119+
120+
当然,一个健壮的系统可能还要考虑到方方面面。发红包本身如果是数据量特别大的情况要还需要做多副本方案。本文只是演示各种方案的优缺点,仅供参考。另外,如果采用`Redis`则需要做高可用。
121+
122+
123+
124+
> 参考链接:https://juejin.cn/post/6925947709517987848
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
---
2+
sidebar: heading
3+
---
4+
5+
6+
7+
分享几点我自己的刷题经验,看看我是如何在最短时间内搞定数据结构与算法,达到应付面试的程度的。
8+
9+
主要有以下3点技巧:
10+
11+
1. **按题目分类来刷**
12+
2. **难度要循序渐进**
13+
3. **做好总结**
14+
15+
## 按题目分类刷题
16+
17+
LeetCode上面的题目都有进行分类,建议在一个时间段只刷同一类型的题目,可以更全面的认识这一类型的数据结构or算法,以加深对此类题型的理解。就好比练功夫,前期把一些基本招式都熟悉掌握,后面再串通这些招式,融会贯通。
18+
19+
我个人也是比较习惯按照分类来刷题,自我感觉效果还可以。
20+
21+
我将LeetCode题目进行了整理分类,大家可以参考下(以下出现的题型都是需要掌握的):
22+
23+
**数组操作**
24+
25+
- LeetCode54 螺旋矩阵
26+
- LeetCode76 最小覆盖子串
27+
- LeetCode75 颜色分类
28+
- LeetCode73 矩阵置零
29+
- LeetCode384 打乱数组
30+
- LeetCode581 最短无序连续子数组
31+
- LeetCode945 使数组唯一的最小增量
32+
33+
**链表操作**
34+
35+
- LeetCode206 反转链表
36+
- LeetCode19 删除链表的倒数第N个节点
37+
- LeetCode25 k个一组翻转链表
38+
- LeetCode141 环形链表
39+
- LeetCode142 环形链表Ⅱ
40+
- LeetCode61 旋转链表
41+
- LeetCode138 复制带随机指针的链表
42+
- LeetCode160 相交链表
43+
- LeetCode707 设计链表
44+
45+
****
46+
47+
- LeetCode20 有效的括号
48+
- LeetCode32 最长有效括号
49+
- LeetCode155 最小栈
50+
- LeetCode224 基本计算器
51+
- LeetCode232 用栈实现队列
52+
- LeetCode316 去除重复字母
53+
54+
**树的遍历**
55+
56+
- LeetCode94 二叉树的中序遍历
57+
- LeetCode102 二叉树的层次遍历
58+
- LeetCode110 平衡二叉树
59+
- LeetCode144 二叉树的前序遍历
60+
- LeetCode145 二叉树的后序遍历
61+
62+
**二叉搜索树**
63+
64+
- LeetCode98 验证二叉搜索树
65+
- LeetCode450 删除二叉搜索树中的节点
66+
- LeetCode701 二叉搜索树中的插入操作
67+
68+
**递归**
69+
70+
- LeetCode21 合并两个有序链表
71+
- LeetCode101 对称二叉树
72+
- LeetCode104 二叉树的最大深度
73+
- LeetCode226 翻转二叉树
74+
- LeetCode236 二叉树的最近公共祖先
75+
76+
**双指针/滑动窗口**
77+
78+
- LeetCode3 无重复字符的最长子串
79+
- LeetCode11 盛最多水的容器
80+
- LeetCode15 三数之和
81+
- LeetCode16 最接近的三数之和
82+
- LeetCode26 删除排序数组中的重复项
83+
- LeetCode42 接雨水
84+
- LeetCode121 买卖股票的最佳时机
85+
- LeetCode209 长度最小的子数组
86+
87+
**快慢指针遍历**
88+
89+
- LeetCode141 环形链表
90+
- LeetCode202 快乐数
91+
- LeetCode876 链表的中间结点
92+
93+
**动态规划**
94+
95+
- LeetCode5 最长回文子串
96+
- LeetCode53 最大子序和
97+
- LeetCode62 不同路径
98+
- LeetCode64 最小路径和
99+
- LeetCode70 爬楼梯
100+
- LeetCode118 杨辉三角
101+
- LeetCode300 最长上升子序列
102+
- LeetCode1143 最长公共子序列
103+
104+
**回溯算法**
105+
106+
- LeetCode10 正则表达式匹配
107+
- LeetCode22 括号生成
108+
- LeetCode40 组合总和2
109+
- LeetCode46 全排列
110+
111+
**贪心算法**
112+
113+
- LeetCode 11. 盛最多水的容器
114+
- LeetCode 406. 根据身高重建队列
115+
- LeetCode 55. 跳跃游戏
116+
- LeetCode 122. 买卖股票的最佳时机 II
117+
- LeetCode 309. 最佳买卖股票时机含冷冻期
118+
- LeetCode 714. 买卖股票的最佳时机含手续费
119+
120+
**并查集**
121+
122+
- LeetCode200 岛屿的个数
123+
- LeetCode547 省份数量
124+
125+
**位运算**
126+
127+
- LeetCode52 N皇后Ⅱ
128+
- LeetCode338 比特位计数
129+
- LeetCode191 位1的个数
130+
- LeetCode231 2的幂
131+
132+
## 难度要循序渐进
133+
134+
这一点是针对初学者来说的,切记一上来就干hard级别的题目,会让你怀疑人生的。。。
135+
136+
正确的做法是**循序渐进**,从容易到中等,再过渡到困难级别。不过国内大厂考察算法,一般都是中等难度,困难级别的应该很少考察。
137+
138+
## 做好总结
139+
140+
**多做总结!多做总结!多做总结!**
141+
142+
**做好总结很重要**,特别是对于没思路的题目,看了其他大佬的解法之后,多思考有哪些题目也是类似解法,这种题目的关键解题步骤,把自己的理解写下来,方便自己日后查看。
143+
144+
虽然总结可能会花费你半个钟甚至更多的时间,但是不总结的话,下次你遇到这个题目,可能会花更多的时间去思考、解答。

0 commit comments

Comments
 (0)