高并发抢红包需从算法、资源隔离、状态流转三方面优化:用二倍均值分配金额确保每人≥0.01元;预生成原子化红包单元并Redis RPOP实现无锁领取;MySQL异步批量落库;按红包ID哈希分片实现水平扩展;过期用Redis有序集合而非定时扫描。

高并发下抢红包不是比谁写得快,而是比谁漏得少、谁锁得准、谁分得匀。单靠数据库行锁或 Redis INCR 拦不住 10 万 QPS 的瞬时请求,必须从算法、资源隔离、状态流转三处下手。
红包金额分配必须用二倍均值,不能用随机截断
常见错误是直接 rand.Intn(remainMoney) 截取,导致最后几人常抢不到或只剩 1 分——违反“每人至少 0.01 元”硬约束。二倍均值算法本质是动态缩放区间:max = (remainMoney - min * remainCount) / remainCount * 2 + min,保证剩余人均有可分配空间。
- 第
i人可抢范围是[min, (remainMoney - min*(remainCount-1)) * 2 / remainCount + min] - 最后一人强制返回
remainMoney,避免浮点误差或整除丢失 - 务必在函数入口调用
rand.Seed(time.Now().UnixNano()),否则多 goroutine 下rand会返回相同序列
Redis 不能只存总金额,要拆成原子化红包单元
把整个红包存成一个 key(如 hb:1001:total)再用 DECRBY 扣减,会在高并发下产生大量失败重试和热点 key。正确做法是预生成 n 个独立红包单元,每个单元含金额与状态:
- 用
LPUSH hb:1001:units "500"推入 100 个已计算好的金额(单位:分) - 抢红包时用
RPOP hb:1001:units原子出栈,成功即得红包,失败说明已抢完 - 配合
EXPIRE hb:1001:units 3600设置过期,避免残留 - 若需记录领取者,额外用
HMSET hb:1001:detail:uid123 uid 123 amount 500 ts 1743996720存明细,不参与核心路径
MySQL 写操作必须避开事务冲突,用“先写缓存+异步落库”
抢到红包后立刻 INSERT INTO hb_detail 会因主键/唯一索引冲突或行锁排队拖慢响应。真实系统中,这步应完全异步:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- 抢成功后只写 Redis(如
SET hb:1001:uid123 "500")并返回客户端 - 另起 goroutine 消费 Redis 中的领取事件,批量写入 MySQL(如每 100 条或 100ms flush 一次)
- MySQL 表结构去除非必要索引,
hb_detail只保留(hb_id, uid, amount, created_at),不建联合唯一键 - 若需强一致性查“某用户是否抢过”,用
EXISTS(SELECT 1 FROM hb_detail WHERE hb_id=? AND uid=?)加SELECT ... FOR UPDATE,但仅限管理后台,不走前台链路
水平扩展必须 SET 化,不能共用一套 Redis/MySQL 实例
当单机扛不住时,简单加机器 + 负载均衡只会让 Redis 连接数、MySQL 锁竞争、慢查询陡增。关键是要做硬件级隔离:
- 按红包 ID 哈希分片,例如
shard_id = hb_id % 64,每个 shard 对应独立 Redis 实例 + 独立 MySQL 库 - 服务启动时通过环境变量加载对应 shard 的连接配置,不跨 shard 查询
- 禁止使用全局计数器(如总领取数),改用各 shard 自行统计 + 上游聚合
- 监控必须按 shard 维度看 QPS、延迟、连接数,否则无法定位是某台 Redis 崩了还是代码写错
最易被忽略的一点:红包过期逻辑不能依赖定时任务扫描全表,而要用 Redis 的 EXPIREAT 配合 KEYS hb:*:units 的模糊匹配根本不可行——生产环境禁用 KEYS。正确方式是发红包时写入一个有序集合 zadd hb:expire:20260407 "1743996720" "1001",再由常驻 goroutine 定时 ZRANGEBYSCORE 拉取过期 ID 清理。这个细节决定系统能不能稳住三天不崩。

















