高并发下的抢红包设计:微信红包背后的算法与温情
拼手气红包追求的是随机公平 和惊喜感,不能简单粗暴地随机,否则会出现第一个人抢走大部分钱的尴尬。
整体核心思路是「缓存扛峰值、异步提性能、最终一致保数据」
核心需求边界拆解
先明确场景的核心约束,避免设计偏离:
| 分类 | 核心要求 |
|---|---|
| 功能需求 | 发红包、抢红包、拆红包、24h 未领自动原路退款 |
| 非功能需求 | 百万级 QPS 高并发支撑、零超发、毫秒级响应、防重复抢 / 作弊 |
红包核心算法:二倍均值法(微信同款)
经典的实现是 二倍均值法:
🔴 这是红包设计的灵魂,也是 "温情" 的底层逻辑,公式如下:
text
单次抢到金额 = 随机区间 (0.01, 剩余金额 ÷ 剩余人数 × 2)
这样既能保证金额随机波动,又能避免最后的人分到 0 元,数学上保证了所有人的期望值相等。
算法特点
- 保底 1 分钱,不会出现抢到 0 元的尴尬体验
- 每一轮的均值相等,不会出现 "前面人拿大头,后面全是 1 分" 的极端情况,公平性强
- 实现简单、计算性能极高,完全适配高并发场景
示例演示(100 元 10 个红包)
| 轮次 | 剩余人数 | 剩余金额 | 本次随机金额范围 | 单轮均值 |
|---|---|---|---|---|
| 第 1 个 | 10 | 100 元 | 0.01~20 元 | 10 元 |
| 第 2 个 | 9 | 假设剩余 92 元 | 0.01~20.44 元 | 10.22 元 |
| 第 3 个 | 8 | 假设剩余 83 元 | 0.01~20.75 元 | 10.375 元 |
| ... | ... | ... | ... | ... |
| 最后 1 个 | 1 | 剩余全额 | 固定值 | - |
最后一个人直接拿走剩余的钱。整个分配可以用一张流程图概括:
👆 这就是拼手气的核心,简单且高效。
高并发整体架构设计: 快、准、稳
核心原则:流量层层过滤,99% 的请求拦截在缓存层,尽量少打数据库🛡️
抢红包是极致的读多写多场景,必须保证不超发、不少发、抢得顺滑 。核心思路:读写分离、原子化扣减、异步持久化。
text
┌─────────────── 接入层 ───────────────┐
│ Nginx限流 + 网关鉴权 + 恶意请求拦截 │
└───────────────┬─────────────────────┘
┌─────────────── 服务层 ───────────────┐
│ 红包核心服务 + 账户服务 + MQ异步消费 │
└───────────────┬─────────────────────┘
┌─────────────── 缓存层 ───────────────┐
│ Redis库存预扣 + 用户去重 + 热点Key拆分 │
└───────────────┬─────────────────────┘
┌─────────────── 数据层 ───────────────┐
│ 分库分表MySQL + 定时对账兜底任务 │
└─────────────────────────────────────┘
核心流程与高并发优化点
1. 发红包流程(低并发,侧重数据初始化)
- 校验红包金额、个数合法性,限制单红包最大个数 / 金额
- 生成唯一红包 ID,写入数据库生成红包主记录
- 将红包剩余个数、剩余金额同步写入 Redis,设置 24h + 过期时间
- 返回红包跳转链接
预分配+入缓存
- 用户发起支付成功后,后端用 二倍均值法 提前把总金额拆分成 N 个小额。
- 将这一组金额 push 进 Redis 的 List
(RPUSH),相当于红包池就绪。 - 同时,把红包元信息(发送者、留言、总金额等)异步写入 MySQL,并缓存到 Redis Hash。
- 关键:金额预分配,抢的时候直接从 List 里 LPOP,极致轻量。
2. 抢红包核心流程(高并发重灾区,性能核心)
核心逻辑:同步链路只做 Redis 操作,所有 DB 操作全异步
- 第一层过滤:网关限流、用户黑名单校验,拦截刷接口的恶意请求
- 第二层过滤:Redis 用SETNX做用户维度去重,同一个红包单用户只能抢 1 次
- 第三层过滤:Redis 原子执行DECR 剩余库存,结果 < 0 直接返回「手慢了」
- 异步处理:库存扣减成功后发送 MQ 消息,异步生成红包明细、更新用户账户余额
- 同步返回:前端直接展示抢到金额,明细数据后续懒加载
这是并发最高的环节,必须把所有核心判断和操作压缩到一个 Redis Lua 脚本 里,利用 Redis 单线程保证原子性。
这个方案的精髓:
- 原子性 :
判断 + 扣库存 + 去重在一个 Lua 脚本里完成,无锁无超发。 - 抗并发:直接操作内存,单机 Redis 轻松扛住 10W+ QPS,热点红包可上 Redis Cluster + 本地缓存。
- 最终一致:抢完立刻给用户反馈,写入 DB 走 MQ 削峰,保证数据不丢。
🛠️ 扩展思考:如果红包金额极其巨大、拆包耗时,可以引入拆包预分配的二阶段模式,但微信普通红包场景下,实时二倍均值 + Redis List 已经足够。
3. 关键优化手段
- 原子扣库存防超发:用 Redis 的DECR原子指令替代分布式锁,性能提升一个量级,从根源杜绝超发
- 热点 Key 拆分:春晚级别的超级热点红包,把单个库存 Key 拆分为多个子 Key 分摊流量,解决单 Key 热点瓶颈
- 全链路异步化:抢红包成功后的写库、余额变更、消息通知全部走 MQ,同步链路耗时控制在 10ms 以内
- 分库分表:红包主表、明细表按红包 ID 哈希分库分表,支撑亿级数据存储与查询
数据一致性与兜底方案
- 最终一致性保障:MQ 消费失败自动重试,超过阈值进入死信队列人工干预,保证 Redis 扣减和 DB 数据最终对齐
- 定时对账兜底:每日定时任务扫描过期红包,校对 Redis 与 DB 的库存、金额偏差,自动执行未领红包退款
- 服务降级:峰值时降级非核心接口(比如红包排行榜、好友抢红包记录),优先保障抢红包主链路可用
算法里的 "温情" 设计
✨ 技术之外的用户体验细节:
- 二倍均值算法保证公平性,不会让晚抢的人只能拿到 1 分钱,照顾所有参与者的情绪
- 24 小时未领取的红包自动原路退回,无任何资金损耗
- 保底 1 分钱机制,避免出现 "抢了个寂寞" 的负面体验
技术是骨架,情感是血肉
微信红包之所以成为现象级产品,绝不仅是代码写得好,更是把 "钱"包装成了"情"。
- 惊喜感 🎁:拼手气金额的随机波动,激发"赌徒心理",群聊里截屏"手气最佳"成了社交货币,系统还会给手气最佳的人自动带上👑标识,引发下一轮互动。
- 仪式感 🧨:专属红包封面、春节期间的动态封皮、输入金额时的"恭喜发财,大吉大利"默认留言,都在强化"这是心意,不是转账"的认知。
- 低负担 🤝:200元上限的设定非常巧妙,既降低了发送者的心理负担,也让抢到几分钱的人不会尴尬,反而创造"一分也是爱"的群内打趣氛围。
- 即时满足 ⚡:高并发架构保证"点即开"的零等待体验,配合硬币掉落的音效和动画,把"领钱"的快乐瞬间放大。
总结一下:
这个场景设计的答案就是 "算法保证公平,架构承载海量并发,而产品细节注入温度"。单纯用 Redis 实现高并发不难,难的是让每一次点击都既稳定又充满人情味。
核心代码实现(技术亮点)💻
1. 亮点 1:Redis 原子抢红包 Lua 脚本(核心中的核心)
技术亮点:利用 Redis 单线程特性,将「用户抢红包去重 + 库存校验 + 金额扣减」三个操作封装为原子执行,完全摒弃分布式锁,性能提升一个量级,从根源杜绝超发、重复抢问题。
lua
-- 入参:KEYS[1]=红包库存Key KEYS[2]=用户去重Key KEYS[3]=红包剩余金额Key
-- 入参:ARGV[1]=用户ID ARGV[2]=最小金额(1分) ARGV[3]=当前剩余人数
local stockKey = KEYS[1]
local userSetKey = KEYS[2]
local remainAmountKey = KEYS[3]
local userId = ARGV[1]
local minAmount = tonumber(ARGV[2])
local remainCount = tonumber(ARGV[3])
-- 1. 校验用户是否已经抢过(去重)
if redis.call('sismember', userSetKey, userId) == 1 then
return -1 -- -1代表重复抢
end
-- 2. 校验库存是否充足
local stock = tonumber(redis.call('get', stockKey))
if stock <= 0 then
return 0 -- 0代表已抢光
end
-- 3. 二倍均值计算本次抢到的金额(最后一个直接拿剩余金额)
local remainAmount = tonumber(redis.call('get', remainAmountKey))
local currentAmount
if remainCount == 1 then
currentAmount = remainAmount
else
-- 二倍均值公式:随机区间[1分, 剩余金额/剩余人数 * 2]
local maxAmount = math.floor(remainAmount / remainCount * 2)
currentAmount = math.random(minAmount, maxAmount)
end
-- 4. 原子扣减库存、扣减金额、标记用户已抢
redis.call('decr', stockKey)
redis.call('decrby', remainAmountKey, currentAmount)
redis.call('sadd', userSetKey, userId)
-- 返回抢到的金额(单位:分)
return currentAmount
2. 亮点 2:二倍均值红包拆分算法 Java 实现
技术亮点:统一以「分」为整数单位计算,完全规避浮点精度丢失问题,严格遵循微信红包的公平性规则。
java
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* 微信二倍均值红包拆分算法
*/
public class RedPacketAlgorithm {
// 最小金额:1分
private static final int MIN_AMOUNT = 1;
private static final Random RANDOM = new Random();
/**
* 拆分红包
* @param totalAmount 总金额(单位:分)
* @param totalCount 红包总个数
* @return 每个红包的金额列表(单位:分)
*/
public static List<Integer> splitRedPacket(int totalAmount, int totalCount) {
List<Integer> amountList = new ArrayList<>(totalCount);
int remainAmount = totalAmount;
int remainCount = totalCount;
for (int i = 0; i < totalCount - 1; i++) {
// 二倍均值计算上限:剩余金额 / 剩余人数 * 2
int max = (remainAmount / remainCount) * 2;
// 随机区间 [1, max]
int current = RANDOM.nextInt(max - MIN_AMOUNT + 1) + MIN_AMOUNT;
amountList.add(current);
// 更新剩余值
remainAmount -= current;
remainCount--;
}
// 最后一个红包直接拿剩余金额
amountList.add(remainAmount);
return amountList;
}
}
3. 亮点 3:MQ 异步落库消费核心逻辑
技术亮点:同步链路只走缓存,所有 DB 操作全异步削峰,把核心抢红包接口的 RT 控制在 10ms 以内,同时用本地消息表兜底可靠性。
java
// 抢红包成功后发送MQ消息
public void grabRedPacketSuccess(Long redPacketId, Long userId, Integer amount) {
// 组装消息体:红包ID、用户ID、抢到金额、时间
RedPacketGrabMessage message = new RedPacketGrabMessage(redPacketId, userId, amount);
// 异步发送RocketMQ,支持重试、死信队列兜底
rocketMQTemplate.asyncSend("redPacket_grab_topic", message, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
// 发送成功无额外处理
}
@Override
public void onException(Throwable e) {
// 发送失败降级:本地消息表兜底重试
localMessageTable.save(message);
}
});
}
// MQ消费者:异步落库+更新账户余额
@RocketMQMessageListener(topic = "redPacket_grab_topic", consumerGroup = "redPacket_consumer_group")
public class RedPacketGrabConsumer implements RocketMQListener<RedPacketGrabMessage> {
@Override
public void onMessage(RedPacketGrabMessage message) {
// 1. 写入红包明细表
redPacketDetailMapper.insertDetail(message);
// 2. 更新用户账户余额(钱包服务)
accountService.addBalance(message.getUserId(), message.getAmount());
// 3. 发送抢成功通知
notifyService.sendGrabSuccessMsg(message.getUserId(), message.getAmount());
}
}
核心技术难点与对应解决方案 🔧
| 核心技术难点 | 解决方案 | 落地收益 |
|---|---|---|
| 高并发下红包超发、用户重复抢的并发安全问题 | 1. Redis+Lua 脚本原子执行去重、扣库存、扣金额全流程2. 完全摒弃分布式锁,避免锁开销与死锁风险 | 从根源杜绝超发,单节点并发处理能力提升 10 倍以上 |
| 春晚级热点红包的单 Redis Key 性能瓶颈(单 Key QPS 上限约 10W) | 1. 库存分片:将 1 个红包的总库存拆分为 N 个子库存 Key2. 请求按用户 ID 哈希路由到不同子 Key,流量打散3. 子库存抢光后自动迁移到其他分片兜底 | 热点 Key 承载能力线性提升,10 分片可支撑百万级 QPS |
| Redis 缓存与 MySQL 数据库的数据一致性问题 | 1. 采用最终一致性模型,缓存作为主写入口,DB 异步同步2. MQ 消费失败自动重试,超过阈值进入死信队列人工干预3. 每日定时对账任务,校对库存、金额偏差,兜底修正 | 数据一致性达 99.99%,极端异常可通过对账 100% 修复 |
| 峰值流量冲击导致服务雪崩、数据库被打垮 | 1. 多层限流:网关层限流、接口级限流、用户维度限流2. 全链路异步化:非核心逻辑全部 MQ 异步削峰3. 服务降级:峰值关闭排行榜、领取记录等非核心接口 | 数据库压力降低 99%,系统峰值稳定性大幅提升 |
| 亿级红包明细数据的存储与查询性能问题 | 1. 分库分表:按红包 ID 哈希分库分表,分散存储压力2. 冷热数据分离:超过 7 天的历史红包数据归档到冷存储3. 明细查询优先走缓存,DB 只做兜底和对账 | 单表数据量控制在千万级,查询性能稳定在毫秒级 |
归纳起来就是一句话:
用 Redis 抗住读写的瞬时洪峰,用 MQ 平滑落地,用 Lua 保证原子性,用整数运算规避精度,最后加上风控兜底。
公众号"Rain的Java大神之路"
个人博客"www.javadashen.com"