高并发下的抢红包设计:微信红包背后的算法与温情

高并发下的抢红包设计:微信红包背后的算法与温情

拼手气红包追求的是随机公平惊喜感,不能简单粗暴地随机,否则会出现第一个人抢走大部分钱的尴尬。

整体核心思路是「缓存扛峰值、异步提性能、最终一致保数据

核心需求边界拆解

先明确场景的核心约束,避免设计偏离:

分类 核心要求
功能需求 发红包、抢红包、拆红包、24h 未领自动原路退款
非功能需求 百万级 QPS 高并发支撑、零超发、毫秒级响应、防重复抢 / 作弊

红包核心算法:二倍均值法(微信同款)

经典的实现是 二倍均值法

🔴 这是红包设计的灵魂,也是 "温情" 的底层逻辑,公式如下:

text 复制代码
单次抢到金额 = 随机区间 (0.01, 剩余金额 ÷ 剩余人数 × 2)

这样既能保证金额随机波动,又能避免最后的人分到 0 元,数学上保证了所有人的期望值相等。

算法特点

  1. 保底 1 分钱,不会出现抢到 0 元的尴尬体验
  2. 每一轮的均值相等,不会出现 "前面人拿大头,后面全是 1 分" 的极端情况,公平性强
  3. 实现简单、计算性能极高,完全适配高并发场景

示例演示(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. 发红包流程(低并发,侧重数据初始化)

  1. 校验红包金额、个数合法性,限制单红包最大个数 / 金额
  2. 生成唯一红包 ID,写入数据库生成红包主记录
  3. 将红包剩余个数、剩余金额同步写入 Redis,设置 24h + 过期时间
  4. 返回红包跳转链接

预分配+入缓存

  • 用户发起支付成功后,后端用 二倍均值法 提前把总金额拆分成 N 个小额。
  • 将这一组金额 push 进 Redis 的 List (RPUSH),相当于红包池就绪。
  • 同时,把红包元信息(发送者、留言、总金额等)异步写入 MySQL,并缓存到 Redis Hash。
  • 关键:金额预分配,抢的时候直接从 List 里 LPOP,极致轻量。

2. 抢红包核心流程(高并发重灾区,性能核心)

核心逻辑:同步链路只做 Redis 操作,所有 DB 操作全异步

  1. 第一层过滤:网关限流、用户黑名单校验,拦截刷接口的恶意请求
  2. 第二层过滤:Redis 用SETNX做用户维度去重,同一个红包单用户只能抢 1 次
  3. 第三层过滤:Redis 原子执行DECR 剩余库存,结果 < 0 直接返回「手慢了」
  4. 异步处理:库存扣减成功后发送 MQ 消息,异步生成红包明细、更新用户账户余额
  5. 同步返回:前端直接展示抢到金额,明细数据后续懒加载

这是并发最高的环节,必须把所有核心判断和操作压缩到一个 Redis Lua 脚本 里,利用 Redis 单线程保证原子性。

这个方案的精髓

  • 原子性判断 + 扣库存 + 去重 在一个 Lua 脚本里完成,无锁无超发。
  • 抗并发:直接操作内存,单机 Redis 轻松扛住 10W+ QPS,热点红包可上 Redis Cluster + 本地缓存。
  • 最终一致:抢完立刻给用户反馈,写入 DB 走 MQ 削峰,保证数据不丢。

🛠️ 扩展思考:如果红包金额极其巨大、拆包耗时,可以引入拆包预分配的二阶段模式,但微信普通红包场景下,实时二倍均值 + Redis List 已经足够。

3. 关键优化手段

  • 原子扣库存防超发:用 Redis 的DECR原子指令替代分布式锁,性能提升一个量级,从根源杜绝超发
  • 热点 Key 拆分:春晚级别的超级热点红包,把单个库存 Key 拆分为多个子 Key 分摊流量,解决单 Key 热点瓶颈
  • 全链路异步化:抢红包成功后的写库、余额变更、消息通知全部走 MQ,同步链路耗时控制在 10ms 以内
  • 分库分表:红包主表、明细表按红包 ID 哈希分库分表,支撑亿级数据存储与查询

数据一致性与兜底方案

  1. 最终一致性保障:MQ 消费失败自动重试,超过阈值进入死信队列人工干预,保证 Redis 扣减和 DB 数据最终对齐
  2. 定时对账兜底:每日定时任务扫描过期红包,校对 Redis 与 DB 的库存、金额偏差,自动执行未领红包退款
  3. 服务降级:峰值时降级非核心接口(比如红包排行榜、好友抢红包记录),优先保障抢红包主链路可用

算法里的 "温情" 设计

✨ 技术之外的用户体验细节:

  1. 二倍均值算法保证公平性,不会让晚抢的人只能拿到 1 分钱,照顾所有参与者的情绪
  2. 24 小时未领取的红包自动原路退回,无任何资金损耗
  3. 保底 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"

相关推荐
Rain的Java大神实战圈1 天前
如何设计对外API接口
场景设计题
Rain的Java大神实战圈5 天前
如何保证接口幂等
场景设计题
Rain的Java大神实战圈5 天前
PC版网站被狂刷怎么处理
场景设计题
Rain的Java大神实战圈7 天前
接口被狂刷怎么处理
场景设计题
Rain的Java大神实战圈8 天前
线上在Docker中跑MySQL会怎样
场景设计题
Rain的Java大神实战圈9 天前
Docker搭建Redis集群完全指南
场景设计题
Rain的Java大神实战圈12 天前
短信接口被狂刷怎么处理
场景设计题
Rain的Java大神实战圈13 天前
如何避免订单重复提交
场景设计题
Rain的Java大神实战圈14 天前
MySQL误删数据如何恢复
场景设计题