抢红包的一些场景
前言
逢年过节,微信群里抢红包已经成了一种”传统活动”。看似简单的”点一下就能抢到钱”的背后,实际上涉及了并发控制、一致性、性能等多个技术问题。一个完整的抢红包流程,远没有看上去那么简单。
本文从一个最简单的红包模型开始,逐步深入,聊聊抢红包背后的技术方案演进。
红包的基本模型
发红包
发红包时,用户需要指定总金额和个数。系统需要将这 N 个红包的金额分配好,让每个抢到的人随机获得一定金额。
拆分的核心是二倍均值法:
每次拆分的金额 = 随机区间 [0.01, 剩余金额 / 剩余个数 × 2]
举个例子,发 100 元,10 个人:
- 第 1 个人抢:金额范围 [0.01, 100/10×2=20],平均 10 元
- 第 2 个人抢:剩余 90 元左右,金额范围 [0.01, 90/9×2=20],平均 10 元
- …
- 最后一个人:拿剩下的全部
这种方式的优点是公平——每个人抢到的金额期望值相等,不会出现前面的人抢太多后面的人没得抢的情况。
1 | public static List<BigDecimal> splitRedPacket(BigDecimal total, int count) { |
抢红包流程
抢红包的核心流程分为两步:
flowchart LR
A[用户点击抢红包] --> B{红包是否存在
且未被抢完?}
B -->|否| C[返回 已抢完]
B -->|是| D[原子扣减红包余额]
D --> E{扣减成功?}
E -->|否| C
E -->|是| F[分配金额给用户]
F --> G[返回 抢到 x 元]
这里的核心约束是:同一个红包不能被抢超过它设定的次数,同一个用户不能重复抢同一个红包。这看起来简单,但在高并发场景下,要做到正确且高性能,并不容易。
Redis 方案
高并发场景下,直接操作数据库会成为瓶颈。Redis 凭借其单线程模型和丰富的数据结构,成为抢红包场景的首选方案。
数据结构设计
1 | // 红包预拆分 - 发红包时一次性拆好 |
Lua 脚本保证原子性
抢红包操作涉及多个步骤:判断是否已抢完 → 判断是否已抢过 → 弹出一个金额 → 记录用户。这些步骤需要用 Lua 脚本封装成原子操作:
1 | -- 参数: KEYS[1]=红包ID, KEYS[2]=用户ID |
flowchart TD
A[用户请求] --> B[Redis执行Lua脚本]
B --> C{"LLEN 还有余额?"}
C -->|否| D[返回 已抢完]
C -->|是| E{"SISMEMBER
已抢过?"}
E -->|是| F[返回 重复抢]
E -->|否| G[LPOP 弹出一个金额]
G --> H{SADD 记录用户}
H --> I[返回金额]
I --> J["异步落库
(消息队列)"]
Redis 方案将抢红包操作控制在了内存中,单个 Lua 脚本的原子性保证了数据一致,性能上单机可以达到每秒数万次的处理能力。
数据库方案
如果不依赖 Redis,纯数据库方案应该怎么做?
表结构
1 | -- 红包主表 |
乐观锁扣减
1 |
|
1 | -- 乐观锁更新的 SQL |
数据库方案的瓶颈
数据库方案在高并发下有几个明显的问题:
- 行锁竞争:所有用户抢同一个红包,
UPDATE会锁定该行,其他请求排队等待 - 连接池压力:等待锁的请求会占用数据库连接,可能导致连接池耗尽
- 事务开销:每抢一次红包都需要开启和提交事务
压测数据下,数据库单机能支撑的抢红包 QPS 大概在几百到一千左右,远低于 Redis 方案。
异步架构
对于类似春晚红包这种千万级并发的场景,仅靠 Redis 也不够,需要引入异步架构来削峰。
flowchart LR
subgraph 接入层[接入层]
Nginx["Nginx/LVS
负载均衡"]
Waf["WAF
防刷"]
end
subgraph 缓存层[缓存层]
Redis["Redis Cluster
预拆分余额
Lua原子操作"]
end
subgraph 消息队列[消息队列]
MQ["Kafka/RocketMQ
削峰填谷"]
end
subgraph 持久层[持久层]
DB["MySQL
最终落库"]
end
Nginx --> Waf
Waf --> Redis
Redis --> MQ
MQ --> DB
这种架构的特点是:
- 接入层:通过负载均衡分流,配合 WAF 过滤机器刷单
- 缓存层:Redis 做核心的抢红包操作,Lua 脚本保证原子性
- 消息队列:抢成功后将明细写入 MQ,异步落库,不阻塞主流程
- 持久层:消费 MQ 完成最终的数据库持久化
扩展问题
红包过期退回
超时未抢完的红包,需要将剩余金额退回给发红包的人。可以用定时任务扫描”已过期但未退回”的红包,或者用 Redis 的 TTL + 过期回调来自动触发。
用户限流
防刷是红包系统的必修课。常见的策略有:
- 每个用户对单个红包限抢一次(SISMEMBER 判断)
- 同一用户抢红包频率控制(滑动窗口限流)
- IP 级别限流(Nginx 级别)
最终一致性
异步架构下,可能会出现用户抢成功了但落库失败的情况。这时候需要补偿机制——记录流水日志,通过定时对账发现不一致的数据,进行补偿处理。
总结
抢红包看似简单,实际上从发红包时的金额拆分到抢红包时的并发控制,每个环节都有不同的技术方案可以选择:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 纯数据库 | 实现简单,数据强一致 | 并发低,行锁瓶颈 | 小范围活动 |
| Redis + Lua | 并发高,原子性有保证 | 增加运维复杂度 | 中等规模抢红包 |
| 异步架构 | 可支撑千万级并发 | 架构复杂,需要补偿机制 | 大型活动如春晚 |
做技术选型时,不要为了炫技而引入复杂的架构。如果你的业务场景只是几百人抢红包,数据库乐观锁就足够了。架构是服务于业务的,不是反向的。
文章作者:米兰
原始链接:https://blog.milanchen.site/posts/grap-red-packet.html
版权声明:转载请声明出处