🧧 前言:为什么“抢红包”这么难?
你以为抢红包只是Random()一下吗?
如果是 100 块钱发给 10 个人:
- 第一个人
Random(0, 100),随到了 99 元。 - 剩下 9 个人分 1 块钱?这游戏还能玩?
核心难点在于:
- 随机性:每个人抢到的金额要随机,但要符合正态分布(大部分人手气差不多,少数人运气爆棚)。
- 公平性:越早抢和越晚抢,获取金额的“数学期望”必须相等。
- 高并发:春节除夕夜,几亿人同时点,数据库直接炸裂。
- 不超发:绝对不能出现 100 块钱分出了 101 块的情况(资损)。
今天,我们重点讲业界最通用的算法——二倍均值法,以及如何用Redis Lua脚本落地。
🧮 核心算法:二倍均值法 (Double Mean Method)
如果让你设计,你可能想过“预先生成好 10 个随机数放到数组里”。这没问题,但如果红包金额很大、人数很多,存储成本较高。
二倍均值法是一种实时计算算法。
1. 算法公式
假设剩余金额为M,剩余人数为N。
每次抢到的金额X=Random(0.01, (M / N) * 2)
2. 原理推导
假设 100 元发给 10 人。
第 1 人:
均值 = 100 / 10 = 10 元。
范围 = [0.01, 20]。
假设抢到5 元。
第 2 人:
剩余金额 95 元,剩余 9 人。
均值 = 95 / 9 ≈ 10.55 元。
范围 = [0.01, 21.1]。
假设抢到15 元。
…
最后 1 人:直接拿走剩余所有金额。
数学证明:
每次抢夺的期望值始终等于剩余人均金额。这保证了无论你第几个来,理论上抢到的钱平均值是一样的。
💻 代码实现 (Java 版)
算法逻辑本身很简单,注意处理最小单位(1分钱)。
publicclassRedPacketUtil{/** * 二倍均值法计算红包 * @param restMoney 剩余金额 (单位:分) * @param restPeople 剩余人数 * @return 本次抢到的金额 */publicstaticintsplitRedPacket(intrestMoney,intrestPeople){// 1. 如果是最后一人,拿走所有if(restPeople==1){returnrestMoney;}// 2. 二倍均值法的核心公式// 范围:[1, (剩余金额/剩余人数) * 2)// 注意:Random 是左闭右开,所以要 -1 留给剩下的人至少 1 分钱intmax=(restMoney/restPeople)*2;// 随机生成金额,至少 1 分钱intamount=(int)(Math.random()*max);if(amount<=0)amount=1;// 3. 兜底逻辑:不能让剩下的人没钱分// 如果本次抢太多,导致剩下的人分不到 1 分钱,要强行截断if(restMoney-amount<restPeople-1){amount=restMoney-(restPeople-1);}returnamount;}}🏗️ 高并发架构:Redis + Lua 脚本
算法有了,怎么抗住 10万 QPS?
绝对不能用 MySQL 的行锁(悲观锁),必死无疑。
必须使用Redis进行内存计算,并配合Lua 脚本保证原子性(Atomic)。
1. 架构流程图
2. 核心 Lua 脚本
Redis 的 Lua 脚本是原子执行的,中间不会被插入其他命令。我们把“查询剩余金额”、“计算二倍均值”、“扣减库存”这三步合为一步。
-- keys[1]: 红包信息的 Key (Hash结构)-- argv[1]: 用户 IDlocalkey=KEYS[1]localuserId=ARGV[1]-- 1. 校验是否抢过 (幂等性)ifredis.call('hexists',key..':records',userId)==1thenreturn-1-- 已经抢过了end-- 2. 获取剩余金额和人数localrestMoney=tonumber(redis.call('hget',key,'money'))localrestPeople=tonumber(redis.call('hget',key,'count'))-- 3. 校验库存ifrestPeople<=0thenreturn0-- 抢光了end-- 4. 执行二倍均值法算法 (简化版)localamount=0ifrestPeople==1thenamount=restMoneyelse-- Lua 的随机数localmax=math.floor((restMoney/restPeople)*2)amount=math.random(1,max)end-- 5. 扣减 Redis 库存redis.call('hincrby',key,'money',-amount)redis.call('hincrby',key,'count',-1)-- 记录领取记录redis.call('hset',key..':records',userId,amount)returnamount📉 方案优缺点分析
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 预分配法(发红包时生成所有金额存 List) | 抢红包速度极快 (LPOP),逻辑简单 | 占用内存大,发红包时耗时 | 标准红包,人数确定 |
| 二倍均值法(实时计算) | 内存占用极小,不需要预存列表 | 每次需要计算,Redis Lua 逻辑稍复杂 | 超大额/超多人红包 |
📝 总结
设计一个抢红包系统,不仅仅是写个Random。
- 算法层:用二倍均值法保证数学期望的公平。
- 存储层:用Redis Atomicity解决超卖。
- 持久层:用MQ 异步解耦保护 MySQL。
这一套组合拳打下来,面试官基本就被你折服了。
博主留言:
想获取“Redis Lua 脚本完整版”和“SpringBoot 抢红包项目源码”吗?
在评论区回复“红包”,我打包发给你!