# 红包系统设计

# 方案一:Redis 预分配 + MQ 异步入库
# 红包拆分算法:二倍均值法
为了避免高并发时实时计算产生性能瓶颈,红包金额通常在发红包时就提前计算好,存储在队列中。最常用的拆分算法是二倍均值法。
公式:每次随机金额 =
优点:保证每个人随机到的平均概率是相等的,且不会出现某个人一把把钱抓光的情况
# 高并发架构设计
面对瞬时的高并发流量,绝对不能直接读写数据库(MySQL)。我们需要利用 Redis + 队列 + 数据库异步持久化 的方案
发红包(写操作)
- 用户发了一个 100 元、10 个人的红包
- 后端通过“二倍均值法”生成 10 个随机数字(例如:8, 12, 5, ...)
- 将这 10 个金额放入 Redis 的 List 结构中,同时在 Redis 中设置一个计数器(Counter = 10)和一个红包元数据(TotalAmount = 100)
- 将红包记录写入 MySQL(状态为:进行中)
抢红包(高并发读写)
第一步:先看有没有(利用 Redis 计数器减 1) 使用 DECR 命令把红包个数减 1。如果返回值 < 0,说明红包已经抢光了,直接返回“手慢了”,请求不往下走,保护后续系统
第二步:拿金额(利用 Redis List)使用 LPOP 从 Redis List 中弹出一个金额
第三步:异步入库(利用消息队列 MQ)抢到金额后,不要直接写数据库。把“用户 A 抢到红包 X,金额 Y”的信息发送到消息队列(Kafka/RocketMQ)。消费端异步拉取消息,批量更新 MySQL 中的红包明细表和用户钱包表
# 方案二:分段存储
如果所有线程都去争抢同一条红包记录,数据库会出现严重的行锁竞争(Hotspot)。为了解决这个问题,我们可以引入分段锁的概念(类似于 Java 中 ConcurrentHashMap 的早期实现)
我们在发红包时,在数据库里把一个大红包拆成多个“子红包池”。
-- 红包分段表 (RedEnvelope_Segment)
CREATE TABLE red_envelope_segment (
id INT PRIMARY KEY,
parent_envelope_id INT, -- 主红包ID
segment_index INT, -- 分段序号(例如 0, 1, 2, 3)
remaining_amount DECIMAL(10,2), -- 该段剩余金额
remaining_count INT, -- 该段剩余个数
version INT -- 用于乐观锁
);
# 扣减逻辑:随机分段 + 乐观锁/悲观锁
当用户请求通过应用层队列进入数据库操作时:
随机路由:用户 UID 对分段数取模(例如 UID % 4),路由到指定的 segment_index。这样原本抢同一个红包的流量,被平均分流到了 4 条不同的数据库行记录上,行锁竞争瞬间降低到 1/4
扣减金额(二倍均值实时计算):读取该分段的剩余金额和个数,在内存中计算出本次抽到的金额
更新数据库:
方式 A(乐观锁,适合冲突稍低的段):
UPDATE red_envelope_segment SET remaining_amount = remaining_amount - X, remaining_count = remaining_count - 1, version = version + 1 WHERE id = #{id} AND version = #{version} AND remaining_count > 0;
如果更新失败(说明被别人捷足先登),可以尝试重试(Retry)或者直接路由到下一个 segment
方式 B(悲观锁,适合冲突高的段): 使用 SELECT ... FOR UPDATE 锁住该行,计算金额后直接更新。因为有了前面的应用层排队和分段,这里的锁冲突已经完全在数据库的可承受范围内
某个段先抢完了,但是其他段还有剩余,如何处理?
这是一个很好的边缘场景。我们可以引入“段跳转(Segment Hopping)”机制。当用户路由到 Segment 1 发现 remaining_count == 0 时,代码中不直接返回失败,而是顺延尝试 Segment 2、Segment 3。只有当所有 Segment 都为空时,才真正返回“红包已领完”。因为此时并发已经被过滤得差不多了,这种数据库轮询不会造成太大的压力