阿里二面挂了!被问 “抢红包原理”,我只答 “随机算法”,面试官:高并发不用管吗?
前言
昨天帮一位粉丝复盘阿里二面,他说自己最委屈的是倒在了 “微信抢红包原理” 上。
当时他自信满满地甩出了 “二倍均值法” 的随机算法代码,以为能秀一把数学功底。结果面试官冷冷地问了一句:“算法只是皮毛。如果 100 万人同时抢,数据库怎么抗?怎么保证不超发?Redis 挂了怎么兜底?”
他当场懵了——光顾着算钱怎么分,忘了这本质上是一个 “高并发下的资源争抢” 问题。结果直接被判定 “缺乏实战经验,只有学生思维”。
其实 “抢红包” 是大厂面试的 “高并发天王山之战”。面试官考察的不是你怎么算钱,而是你能不能设计一个 “抗住流量洪峰、资金绝对安全” 的系统。

今天结合高并发真实场景,拆透抢红包的 “预先分配 + Redis Lua 原子性 + 异步入库” 核心原理,最后送你一套满分面试模板。
一、 业务场景还原:为什么“抢红包”是高并发的噩梦?
你可能会想:“发个红包不就是转账吗?有什么难的?” 如果只是你给女朋友发 520,MySQL 随便抗。但面试官问的“抢红包”,默认指的是 “春晚摇一摇” 或 “顶流直播间” 这种极端场景。
它有两个违背传统数据库设计的致命特征:
1. 瞬时流量的“洪峰” (Thundering Herd)
平常系统 QPS 只有 100,但在红包发出的一刹那(比如主播喊“3, 2, 1, 开抢”),流量会在 1 秒内从 0 飙升到 20 万 QPS。这种 “脉冲式流量”,任何关系型数据库都扛不住直接的连接冲击。
2. 数据库的“热点行锁”死穴 (Hot Row Problem)
这是最硬核的技术痛点。 假设 10 万人同时抢 1 个红包,如果直接请求数据库,MySQL 的 行锁(Row Lock) 机制会强制这 10 万个线程排队去更新那一行数据。结果: 数据库瞬间死锁,CPU 飙升 100%,系统宕机。
结论: 在秒杀场景下,数据库只能用来“记账”,绝对不能用来“抗压”。这就是为什么我们必须引入 Redis。
二、 核心方案:3 个角色 + “预分配” 策略
在讲流程前,必须先抛弃 “抢的时候再算金额” 的错误思维。高并发下实时计算金额 + 锁数据库 = 系统崩溃。
业界的标准解法是 “二倍均值法 + 预分配”: 在 “发红包” 的那一刻,其实所有红包的金额已经算好,并变成了一条条数据塞进了 Redis。 “抢” 的动作,本质上只是从 Redis 里 “拿走” 一个预先生成好的数字而已。

阶段 1:发红包 —— 资金冻结,流量前置
当你塞入 100 元发 10 个红包时,后台做了三件大事:
1. 资金同步处理(业务核心)
必须在 DB 层先扣除发送者余额。注意:这一步必须是同步的!

- **小额红包( 直接扣除余额。
- 大额红包(> 200元): 推荐使用 “冻结” 模式。Why? 如果红包 24 小时没抢完需要退回,直接解冻比走复杂的“退款逆向流程”更安全、更高效。* *
SQL 示例:
UPDATE user_account SET balance = balance - 100, frozen_balance = frozen_balance + 100 WHERE user_id = 1001 AND balance >= 100;
2. 算法拆分(二倍均值法)
服务器用算法将 100 元拆分成 10 个随机金额。
- 【算法细节】 计算时必须保证取值范围在
[0.01, 2*(剩余金额/剩余红包数)]之间。最后一个红包直接分配剩余所有金额,确保总额精确无误且无 0 元红包。
**
**
3. 推入 Redis 队列
- 红包池: 将这 10 个金额,推入一个 Redis List。
- 库存计数: 设置一个计数器。
阶段 2:抢红包 —— Lua 脚本定生死(面试核心)
这是面试官最关注的环节。100 万人点“开”,如何保证只有 10 个人抢到?答案:Redis Lua 脚本。

前端点击“开”,请求打到服务器,服务器执行一段 Lua 脚本。**
**
为什么要用 Lua? 因为 判断余额、扣减库存、弹出金额、记录用户 这几个动作必须是原子性的,中间不能被打断。
⚠️ 生产级大坑预警 1:
Redis Cluster Hash Tag如果你的公司使用的是 Redis Cluster(集群模式),Lua 脚本可能会报错! 因为 Lua 脚本无法跨分片执行。如果 hb_pool(List)和 hb_count(计数器)落在了不同的 Redis 节点上,脚本直接挂掉。**
**

解决方案: 使用 Hash Tag{}。 Key 命名必须统一,例如:hb:{1001}:pool、hb:{1001}:count。 Redis 只会根据 {} 里的 1001 计算 Hash 槽,强制让这组 Key 落在同一个物理节点上。
Lua 脚本逻辑伪代码(含空值防御):
-- 1. 判断用户是否已经抢过(幂等性去重)if redis.call('SISMEMBER', 'hb:{id}:grabbed', userId) == 1 then return nil -- 抢过了end
-- 2. 判断红包是否还有剩余(防御性编程)local count = redis.call('GET', 'hb:{id}:count')-- 增加 nil 判断,防止 Redis 异常导致脚本报错if not count or tonumber(count) <= 0 then return nil -- 抢光了end
-- 3. 核心动作:扣减库存,弹出金额redis.call('DECR', 'hb:{id}:count')local money = redis.call('LPOP', 'hb:{id}:pool')
-- 4. 记录抢夺成功用户redis.call('SADD', 'hb:{id}:grabbed', userId)
return money阶段 3:拆红包入账 —— 异步解耦,最终一致
用户抢到红包(Redis 返回了金额),钱还没进钱包,这时候数据库才开始工作:
- 发送 MQ 消息:服务端拿到 Redis 返回的金额后,立刻发一条 MQ 消息(包含:抢红包者 ID、红包 ID、金额)。
- 异步入库(MQ 消费端):后端服务监听 MQ,执行余额更新和流水插入。

⚠️ 生产级大坑预警 2:
MQ 重复消费(幂等性)如果消费者刚给用户加完钱,还没来得及提交 ACK 就宕机了,MQ 会重发这条消息,导致用户余额被加两次。**
**
解决方案:
- 数据库联合唯一索引(推荐): 建立
unique_key(red_packet_id, user_id)。 - SQL 抗重:
INSERT IGNORE INTO flow ...,利用数据库底层机制挡住重复请求。
三、 关键补充:4 个异常场景怎么处理?(加分项)
懂了主流程只是合格,懂这四个“坑”才是专家。
**Q1:抢了红包,Redis 扣了,但 MQ 丢了(没入账)怎么办?
**
答: 必须采用 “分级对账” 策略。
- 5 分钟小对账(用户体验): 扫描最近 5 分钟内的抢夺记录,快速修复因 MQ 延迟导致的“抢到没到账”问题,减少客诉。
- T+1 全量对账(财务审计): 每天凌晨核对 Redis 里的
grabbed_set和 MySQL 流水表,发现不一致进行兜底补账,确保财务总账做平。

**Q2:同一个用户连点两次,抢了两份怎么办?
**
答: 也就是接口幂等性。在 Lua 脚本中,我们用了 SISMEMBER 维护了一个 Set。只要 Set 里有你的 ID,第二次请求直接被挡回。
**Q3:红包发了一天都没抢完,剩下的钱怎么办?
**
答: 这是一个分布式调度问题。**
**

正确做法: 使用 延迟消息队列(发送时投递一个 24h 后消费的消息)或者 DB 定时任务扫描。 当 24 小时到了,扫描 DB 中该红包是否还有剩余金额,如果有(且之前是冻结状态),直接执行:
UPDATE user_account SET frozen_balance = frozen_balance - 剩余, balance = balance + 剩余 WHERE user_id = xxx;
**Q4:Redis 集群突然宕机了,怎么保证不雪崩?(面试官必问)
**
答: 必须做 降级 + 兜底 两层防护:
- 服务降级: 客户端检测到 Redis 异常时,直接触发熔断,返回“红包已抢完”,绝对不能让流量穿透到 MySQL,否则数据库必死。

- 数据兜底: Redis 开启 RDB+AOF 混合持久化。同时,后台可异步将抢夺记录落入 MySQL 临时表,作为灾备数据。
四、 面试标准答案模板(背下来,怼回去)
下次被问“抢红包”,直接按这个逻辑输出:
“抢红包的核心本质是秒杀系统,我将其设计为 ‘预分配 + 内存原子操作 + 异步落库’ 三层架构。
- 发红包阶段(预分配): 采用二倍均值法将金额预先拆解,存入 Redis List;大额红包采用 ‘资金冻结’ 模式,方便过期退回。
- 抢红包阶段(高并发读写): 利用 Redis 的 Lua 脚本 实现原子性操作,配合 Hash Tag 确保集群兼容性。脚本内增加 ‘空值防御’,一次性完成去重、扣减、弹窗动作,抗住万级并发。
- 入账阶段(削峰填谷): 抢到后发 MQ 消息。消费端通过数据库唯一索引实现幂等,异步执行入账。
针对异常情况,我设计了 ‘5分钟+T+1’ 分级对账 体系保证资金最终一致性,并设计了服务降级策略防止 Redis 宕机雪崩。”
写在最后
面试官问“抢红包”,问的不是那个红色的 UI 怎么画,而是你如何用 Redis 和 MQ 给脆弱的 MySQL 穿上一层防弹衣。
觉得有用的兄弟,点个赞,收藏起来,万一下次面试就用上了呢!
关注公众号【Fox爱分享】,只讲那些书上不写的实战坑。