中 困难
红包系统设计#
一句话答案#
二倍均值法随机金额,Redis List 预生成→Lua 原子 LPOP 抢红包→MQ 异步落库转账。
核心要点
二倍均值法: 每次随机 [0.01, 剩余金额/剩余人数*2]
抢红包流程:
- 发红包:预生成金额列表存 Redis List
- 抢红包:Lua 原子 LPOP → 记录流水
- 异步:MQ → 落库转账
防并发: Redis 单线程 + Lua 原子操作
二倍均值法数学原理深挖#
算法: 第 k 个人抽取时,设剩余金额 M、剩余人数 n,则从区间 [0.01, M/n × 2] 均匀随机取一个值(忽略 0.01 下界做理论推导)。
为何系数恰好是 2 —— 期望相等的证明:
- 单次抽取服从均匀分布
U(0, 2M/n),其期望E = (0 + 2M/n) / 2 = M/n。 - 即每次抽取的期望,恰好等于”当前剩余金额平均分给剩余每个人”的份额。系数取 2 是唯一能让上界中点正好落在均值
M/n的取值。 - 用归纳法证明每个人的无条件期望都等于
总额T / 总人数N:- 第 1 人:
M=T, n=N,E₁ = T/N。✓ - 关键引理:第 1 人抽完后,剩余金额的期望
E[M₂] = T − T/N = T·(N−1)/N,剩余人数N−1,则剩余每人平均份额的期望E[M₂]/(N−1) = T/N,与第一轮相同。 - 第 2 人期望
E₂ = E[ M₂/(N−1) ] = T/N(利用全期望公式,对 M₂ 取期望)。✓ - 递推下去,每一轮”剩余金额/剩余人数”的期望恒为
T/N,故 Eₖ = T/N 对所有 k 成立。每人期望严格相等,算法公平。
- 第 1 人:
为何后抽的人方差更大:
- 单次方差
Var = (2M/n)²/12 = M²/(3n²),与当前剩余金额M正相关。 - 越往后抽,已被前面的人抽走的金额本身是随机的,导致剩余
M的不确定性层层累积;尤其最后一人直接领取全部剩余(n=1时区间退化),它继承了前面所有随机性的总和,波动最剧烈。这就是”手气最佳常出现在最后几个”的数学根源——不是更公平,是方差大、容易出极值。
为何必须预生成保证总额精确:
- 若抢的时候实时随机,浮点累加与”最后一人领剩余”的边界处理极易让总和对不上原始总额(多发或少发一分钱都是资损)。
- 预生成:发红包时一次性把 N 个金额按上述算法算好、强制令其和等于精确总额(最后一份用减法兜底而非随机),存入 Redis List。抢时只是
LPOP弹出一个已确定的值,不再做任何随机运算——总额绝对精确,且抢的链路退化为 O(1) 弹出,扛得住高并发。
面试回答(2分钟版)
红包系统的设计我分三个阶段来说。发红包阶段,用二倍均值法预生成所有红包的金额列表:每次随机范围是 [0.01, 剩余金额/剩余人数*2],这样能保证每个人的期望值相等且金额分布比较均匀。生成好的金额列表存到 Redis List 里。抢红包阶段,用 Lua 脚本在 Redis 里做原子的 LPOP 操作,因为 Redis 是单线程执行 Lua 的,天然保证不会出现超发问题,也不需要额外的分布式锁。抢到红包后记录流水。落账阶段,通过 MQ 异步将红包流水写入数据库并完成转账,这里用异步是因为转账涉及账户余额变更,比较重,不应该放在高并发的抢红包链路上。核心设计思想是预生成而不是实时计算:金额在发红包时就算好了存好了,抢的时候只是弹出一个值,这样既简单又能保证总额精确。过期未领完的红包通过定时任务退回。
追问与易错
追问方向:
- “二倍均值法公平吗?”→ 数学上每个人期望值相等(都是总金额/总人数),但方差随顺序递增,越靠后的人金额波动越大;实际体验上基本公平,微信红包也是类似算法
- “高并发怎么保证不超发?”→ Redis Lua 脚本原子执行 LPOP(判断剩余+弹出金额一气呵成),Redis 单线程天然串行化不存在竞态条件,无需额外分布式锁
- “过期未领完怎么处理?”→ 红包 code 在 Redis 设 TTL(如 24 小时),定时任务扫描过期红包,将剩余金额通过 MQ 异步退回发送者账户,同时更新红包状态为已过期
易错点:
- ❌ 随机生成然后扣减——可能总额对不上应预生成
- ❌ 忽略极端情况——最后一个红包可能很大