面试知识库
困难

红包系统设计#

一句话答案#

二倍均值法随机金额,Redis List 预生成→Lua 原子 LPOP 抢红包→MQ 异步落库转账。

核心要点

二倍均值法: 每次随机 [0.01, 剩余金额/剩余人数*2]

抢红包流程:

  1. 发红包:预生成金额列表存 Redis List
  2. 抢红包:Lua 原子 LPOP → 记录流水
  3. 异步: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=NE₁ = 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 成立。每人期望严格相等,算法公平。

为何后抽的人方差更大:

  • 单次方差 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 异步退回发送者账户,同时更新红包状态为已过期

易错点:

  • ❌ 随机生成然后扣减——可能总额对不上应预生成
  • ❌ 忽略极端情况——最后一个红包可能很大