所有抢红包算法必须同时满足这 3 个硬约束,缺一不可:
拿到题你第一反应应该是明确约束,而不是“随机数生成”。
维度 | 要求 |
|---|---|
总金额 | 固定不变,比如 100 元 |
总人数 | 固定不变,比如 10 个人抢 |
公平底线 | 每个人至少分到 0.01 元,绝不能出现负数或0元 |
随机体验 | 金额有大有小,戏剧性强,不能太平均,也不能后面的人没得抢 |
总和正确 | 所有人金额累加 == 总金额,一分不能差 |
错误算法 | 问题本质 | 后果 |
|---|---|---|
随机剩余金额法 | 每次随机 | 先抢的人可能拿 99%,最后几人只能拿 1 分 |
固定均值法 | 所有人拿 | 完全无随机性,失去抢红包的乐趣 |
浮点数直接计算法 | 用 | 精度丢失,出现 |
很多人上来就写:
for (int i = 0; i < n - 1; i++) {
// 随机一个金额,扣减总金额
}结果往往是:前面的人把预算吃光了,最后一个人只有 0 分,或者变成了负数。这在金融场景下是要背责的。
核心思想:每次随机范围限制在 [1, 剩余金额/剩余人数 × 2),保证每次抽取的期望都是平均值。这样既保证了不会一下子抢太多导致后面不够分,也保留了“惊喜感”。
假设剩余金额 remainMoney 元,剩余人数 remainPeople 人。
remainMoney / remainPeople (remainMoney / remainPeople) * 2 随机区间 [0.01, 上限 - 0.01]

优点:O (n) 时间复杂度,实现简单,公平性好
小技巧:最后打乱结果顺序,避免用户觉得 "最后一个人总是拿大的"
核心思想:把总金额看成一条线段,随机切n-1刀,分成n段,每段就是一个红包。
示意图:

优点:绝对公平,所有切割点均匀分布
缺点:需要去重和排序,时间复杂度 O (n log n)
核心思想:发红包时提前算好所有金额存入缓存,用户抢时直接取,无需实时计算。
算法 | 公平性 | 时间复杂度 | 空间复杂度 | 实现难度 | 适用场景 |
|---|---|---|---|---|---|
二倍均值法 | 高 | O(n) | O(n) | 简单 | 90% 以上普通场景 |
线段切割法 | 极高 | O(n log n) | O(n) | 中等 | 对公平性要求极高的场景 |
预分配法 | 高 | O (1)(抢时) | O(n) | 中等 | 春节 / 双十一高并发场景 |
洗牌法 | 极高 | O (m)(m = 总分) | O(m) | 简单 | 小金额小人数测试场景 |
关键注意:所有金额用分作为单位(long类型),彻底避免浮点数精度问题!
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
public class RedPacketGenerator {
/**
* 二倍均值法生成红包(单位:分)
* @param totalAmount 总金额(分)
* @param totalPeople 总人数
* @return 红包金额列表
*/
public static List<Long> generateByDoubleMean(long totalAmount, int totalPeople) {
// 边界校验
if (totalPeople <= 0) {
throw new IllegalArgumentException("人数必须大于0");
}
if (totalAmount < totalPeople) {
throw new IllegalArgumentException("总金额不能小于人数(每人至少1分)");
}
List<Long> packets = new ArrayList<>(totalPeople);
long remainingAmount = totalAmount;
int remainingPeople = totalPeople;
Random random = new Random();
for (int i = 0; i < totalPeople - 1; i++) {
// 随机范围:[1, 剩余金额/剩余人数×2)
long max = remainingAmount / remainingPeople * 2;
long amount = random.nextLong(max - 1) + 1;
packets.add(amount);
remainingAmount -= amount;
remainingPeople--;
}
// 最后一人拿剩余所有
packets.add(remainingAmount);
// 打乱顺序,消除"最后一人特殊"的感知
Collections.shuffle(packets);
return packets;
}
}假设 100 元 10 人,模拟一次结果:
12.58 元 | 3.42 元 | 21.03 元 | 0.88 元 | 15.67 元
8.99 元 | 0.56 元 | 18.22 元 | 7.31 元 | 11.34 元不会出现“最后一个人穷得叮当响”,也没有前面的人垄断预算。
上面的算法在生成红包时用,不是在“抢”的时候实时算。
微信红包的经典做法是:预分配,抢的时候直接拿结果。

LPOP 原子弹出,天然线程安全。 春节抢红包是典型的读多写少、瞬时高并发场景,核心优化思路是把计算提前,抢时只做原子读:
List结构 LPOP命令(原子操作)直接弹出红包,无需加锁,避免超发 Set存储已抢用户 ID,抢前先判断 如果面试官继续追问,你可以聊这些方向:
方向 | 关键点 |
|---|---|
精度问题 | 用 |
随机种子 | 避免伪随机,可用 |
二倍均值的公平性 | 数值不会太大也不会太小,但不是绝对“完全随机”,也可提“线段分割法”作为对比 |
跨春节瞬间洪峰 | 提前预热缓存、限流、削峰、最终一致性设计 |
数据一致性 | 扣款和入账可用事务消息,红包状态机严谨设计 |
总结:如果是我来设计,普通业务场景直接用二倍均值法,简单高效;春节这种亿级并发场景,用预分配 + Redis 原子操作的方案,兼顾公平性和性能 ✅
面试官您好,我再补充一下带明确技术亮点的核心实现代码和这个场景下最容易被追问的技术难点及落地方案。
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
public class RedPacketGenerator {
// 微信红包默认最大单包200元=20000分,支持业务配置
private static final long DEFAULT_MAX_PACKET = 20000L;
/**
* 生产级二倍均值法生成红包
* @param totalAmount 总金额(单位:分,必须≥1)
* @param totalPeople 总人数(必须≥1)
* @param maxPacket 单包最大金额(单位:分,必须≥1)
* @return 打乱后的红包金额列表
*/
public static List<Long> generateRedPacket(long totalAmount, int totalPeople, long maxPacket) {
// ✅ 技术亮点1:前置严格边界校验,提前拦截所有非法参数
// 避免后续计算出现异常,同时符合业务规则
if (totalPeople <= 0) {
throw new IllegalArgumentException("抢红包人数必须大于0");
}
if (totalAmount < totalPeople) {
throw new IllegalArgumentException("总金额不能小于人数(每人至少1分)");
}
if (maxPacket < 1) {
throw new IllegalArgumentException("单包最大金额不能小于1分");
}
if (totalAmount > totalPeople * maxPacket) {
throw new IllegalArgumentException("总金额不能超过人数×单包最大金额");
}
List<Long> packets = new ArrayList<>(totalPeople);
long remainingAmount = totalAmount;
int remainingPeople = totalPeople;
// ✅ 技术亮点2:使用ThreadLocalRandom替代Random
// 优势:线程安全无锁,多线程环境下性能比Random高10倍以上,无CAS竞争
ThreadLocalRandom random = ThreadLocalRandom.current();
for (int i = 0; i < totalPeople - 1; i++) {
// ✅ 技术亮点3:双重限制随机范围,既保证公平又避免超大红包
// 限制1:二倍均值限制,保证每人抢到金额的数学期望完全相等
long doubleMeanMax = remainingAmount / remainingPeople * 2;
// 限制2:业务最大单包限制,符合微信等主流产品的规则
long currentMax = Math.min(doubleMeanMax, maxPacket);
// 随机范围:[1, currentMax),保证每人至少抢到1分
long amount = random.nextLong(1, currentMax);
packets.add(amount);
remainingAmount -= amount;
remainingPeople--;
}
// 最后一人拿剩余金额,极端情况兜底校验
long lastAmount = remainingAmount;
if (lastAmount > maxPacket) {
// 概率极低的极端情况:如果最后一人超过上限,重新分配
return generateRedPacket(totalAmount, totalPeople, maxPacket);
}
packets.add(lastAmount);
// ✅ 技术亮点4:结果洗牌,彻底消除用户感知问题
// 数学上不影响公平性,但能避免用户觉得"最后一个人总是拿大红包"
Collections.shuffle(packets);
return packets;
}
// 重载方法,使用默认最大单包200元
public static List<Long> generateRedPacket(long totalAmount, int totalPeople) {
return generateRedPacket(totalAmount, totalPeople, DEFAULT_MAX_PACKET);
}
}🧠 亮点解读
分 做整数运算,彻底避免 double 浮点数累计误差 SecureRandom 产生不可预测的随机序列,红包生成更难被钻空子 核心思想:发红包时预生成所有金额存入 Redis,抢红包时用Lua 脚本原子执行全流程,无需分布式锁,性能拉满。
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.util.Arrays;
import java.util.List;
public class RedisRedPacketService {
private final RedisTemplate<String, Long> redisTemplate;
// ✅ 技术亮点5:Lua脚本原子化操作
// 将"判断是否已抢+弹出红包+标记已抢"三个命令打包成原子操作
// 性能比分布式锁高100倍以上,无死锁风险,彻底解决并发超发问题
private static final String GRAB_RED_PACKET_LUA =
"local userId = KEYS[1] " +
"local packetKey = KEYS[2] " +
"local grabbedKey = packetKey .. ':grabbed' " +
// 1. 原子判断用户是否已经抢过
"if redis.call('SISMEMBER', grabbedKey, userId) == 1 then " +
" return -1 " + // 返回码:-1=已抢过
"end " +
// 2. 原子弹出一个红包
"local amount = redis.call('LPOP', packetKey) " +
"if amount == false then " +
" return 0 " + // 返回码:0=红包已抢完
"end " +
// 3. 原子标记用户已抢
"redis.call('SADD', grabbedKey, userId) " +
"return amount"; // 返回码:正数=抢到的金额(分)
public RedisRedPacketService(RedisTemplate<String, Long> redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 发红包:预生成红包并存入Redis
* @param redPacketId 红包唯一ID
* @param totalAmount 总金额(分)
* @param totalPeople 总人数
* @param expireSeconds 过期时间(秒)
*/
public void sendRedPacket(String redPacketId, long totalAmount, int totalPeople, long expireSeconds) {
// 1. 预生成所有红包金额(计算提前,抢时不做任何计算)
List<Long> packets = RedPacketGenerator.generateRedPacket(totalAmount, totalPeople);
// 2. 存入Redis List结构,支持O(1)弹出
String packetKey = "redpacket:" + redPacketId;
redisTemplate.opsForList().rightPushAll(packetKey, packets);
// 3. 设置过期时间,过期自动清理
redisTemplate.expire(packetKey, expireSeconds);
// 4. 已抢用户集合也设置相同过期时间
String grabbedKey = packetKey + ":grabbed";
redisTemplate.expire(grabbedKey, expireSeconds);
}
/**
* 抢红包:纯原子操作,无锁,支持百万级QPS
* @param userId 用户ID
* @param redPacketId 红包ID
* @return 抢到的金额(分),-1=已抢过,0=已抢完
*/
public long grabRedPacket(String userId, String redPacketId) {
DefaultRedisScript<Long> script = new DefaultRedisScript<>(GRAB_RED_PACKET_LUA, Long.class);
return redisTemplate.execute(script, Arrays.asList(userId, "redpacket:" + redPacketId));
}
}🧠 亮点解读
LPOP 命令在 Redis 单线程模型下天然线程安全,一行代码解决并发竞争 // Lua 脚本一次性完成:检查红包状态 + 塞入预分配列表 + 标记已发
String lua = """
local key_pool = KEYS[1]
local key_status = KEYS[2]
local status = redis.call('GET', key_status)
if status ~= 'INIT' then
return -1 -- 已发过,不允许重复入队
end
for i = 1, #ARGV do
redis.call('RPUSH', key_pool, ARGV[i])
end
redis.call('SET', key_status, 'READY')
return 1
""";
Long result = (Long) jedis.eval(lua,
Arrays.asList(poolKey, statusKey),
amountListAsStrings);🧠 亮点解读
我整理了这个场景下算法 + 工程两大层面的 8 个核心难点,以及对应的工业级解决方案:
技术层面 | 核心难点 | 问题本质 | 工业级解决方案 | 技术亮点 |
|---|---|---|---|---|
算法层面 | 公平性与随机性平衡 | 简单随机会导致先抢 / 后抢占便宜,用户体验极差 | 二倍均值法 + 结果洗牌 | 保证每人抢到金额的数学期望完全相等,同时消除顺序感知 |
算法层面 | 极端值控制 | 无限制随机会出现 "一人独吞 99% 金额" 的极端情况 | 双重范围限制(二倍均值 + 业务最大单包) | 既符合数学公平性,又满足产品业务规则 |
算法层面 | 浮点数精度灾难 | 使用 double/float 计算会出现 0.000001 元、总额不符等资金问题 | 所有金额以分为单位,用 long 类型存储计算 | 彻底解决精度问题,无任何资金误差 |
工程层面 | 高并发超发 / 少发 | 多线程同时抢时,非原子操作导致超发(多发红包)或少发(用户没抢到但扣了库存) | Redis Lua 脚本原子执行 "判断 + 抢 + 标记" 全流程 | 无需分布式锁,性能高且绝对安全,支持百万级 QPS |
工程层面 | 热点 Key 雪崩 | 明星 / 大 V 发的红包会成为热点 Key,导致单 Redis 节点 CPU 打满宕机 | 红包分片存储:将一个红包拆成 N 个小 List 分散到不同 Redis 节点 | 分散热点压力,支持千万级并发抢红包 |
工程层面 | 重复抢问题 | 同一用户多次点击、网络重试会导致重复抢同一个红包 | Redis Set 存储已抢用户 ID,在 Lua 脚本中原子判断 | 无锁判断,性能高且不会出现重复领取 |
工程层面 | 过期红包资金退回 | 未抢完的红包需要自动退回原账户,不能出现资金沉淀 | Redis Key 过期事件监听 + 定时任务兜底扫描 | 双重保证,资金 100% 安全,无遗漏 |
工程层面 | 分布式幂等性 | 网络重试、服务超时会导致重复发红包、重复扣款 | 每个请求携带唯一 requestId,基于 Redis 做幂等校验 | 避免任何重复资金操作,保证系统一致性 |
安全层面 | 作弊防御 (外挂脚本秒抢、撞库预测随机) | 黑产通过自动化脚本实现毫秒级抢红包,或通过分析随机数种子预测红包金额,破坏公平性 |
| 多层防御体系,不影响正常用户体验的同时,拦截 99% 以上的作弊行为 |
总结:春节抢红包算法的核心不是 "随机" 本身,而是在保证资金绝对安全的前提下,兼顾公平性、产品体验和亿级并发性能。普通业务场景用优化版二倍均值法,春节这种高并发场景用 "预分配 + Redis Lua 脚本 + 分片" 的方案。
【别走,交个朋友】
我是Rain ,一个喜欢把复杂技术讲透、让代码落地的实践者。
如果你厌倦了四处收集碎片化的八股文,想看看一线项目里真实的架构决策和踩坑记录,欢迎来我的公众号「Rain的Java大神之路」坐坐。
在这里,我会把每一次技术复盘、每一个项目的设计源码,毫无保留地分享给你。
🔥技术这条路很酷,我们结伴同行。
🚀Keep Coding, Keep Loving —— 与所有 Java 同路人共勉。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。