【架构】-----500万并发抢1万个红包:高并发秒杀系统完整落地架构

发布时间:2026/7/16 10:37:45
【架构】-----500万并发抢1万个红包:高并发秒杀系统完整落地架构 字节基础架构三面春节大额券500万QPS抢券面试题完整拆解场景原题复述面试官春节红包雨活动下午两点整发放一张「满200减100」大额券库存仅1万张预估500万人同一秒涌入。第一问架构怎么搭保证绝对不超发第二问500万用户去重记录存Redis如何防止BigKey第三问Redis扣减成功、MQ发送失败数据库未落库用户显示抢到但无券如何解决第四问库存拆10个分片部分分片秒光、部分剩余大量库存怎么处理x 99%无效流量x 超限流量x 重复用户500万请求Nginx网关拦截99%流量Redis Lua 原子限流Redis Bitmap 用户去重MQ异步缓冲数据库批量落库请求拒绝返回第一层三级火箭流量漏斗500万流量逐层削减至1万有效请求核心思路流量不是硬扛从离用户最近的网关拦截无效请求层层过滤避免无效流量穿透到Redis、应用、数据库。三层链路结构第一级网关拦截层Nginx/Gateway实现Redis全局活动计数器总库存1万计数器阈值设1.5万到达阈值直接返回「已抢光」。价值拦截下沉99%无效请求直接挡在网关不进入后端服务。第二级应用层Redis核心战场所有库存扣减、用户去重、并发控制全部交由Redis原子操作处理数据库不参与实时抢券逻辑。第三级数据层MQ异步归档Redis抢券成功后异步发送MQ数据库后台缓慢落库用户无需等待DB写入页面直接展示领取成功。完整流量链路网关层全局计数器拦截99%流量 ↓ 仅剩余1.5万有效请求 应用层Redis Lua原子扣减 BitMap用户去重 ↓ 最终仅1万抢购成功流量 数据层MQ异步落库用户无感知 核心思想拦截下沉优先在流量入口丢弃无效请求减少底层存储压力。第二层Lua原子盾杜绝并发竞态保证绝不超发、不重复领券基础方案坑点单独使用DECR扣库存存在竞态先GET校验库存、校验用户、再DECR是多条命令高并发下存在时间窗口会出现超发、重复领取、库存负数问题。Redis单条命令原子但多条组合非原子。最优解法Lua脚本单线程串行执行Redis执行Lua脚本为单线程串行将「校验用户-校验库存-扣库存-标记用户」打包为一条原子逻辑所有请求排队执行无竞态。原始Set版本Lua脚本存在BigKey隐患仅P6基础方案-- KEYS[1]: stock_1001 库存key-- KEYS[2]: users_1001 已领用户Set集合-- ARGV[1]: userId-- 1. 判断用户是否已领取ifredis.call(SISMEMBER,KEYS[2],ARGV[1])1thenreturn-1-- -1重复领取end-- 2. 判断剩余库存localstocktonumber(redis.call(GET,KEYS[1]))ifstock0thenreturn-2-- -2库存已抢光end-- 3. 原子扣库存记录用户redis.call(DECR,KEYS[1])redis.call(SADD,KEYS[2],ARGV[1])return1-- 1抢购成功 核心原理Lua脚本串行执行同一活动所有请求排队从根源消除并发竞争做到零超发。该方案仅能达到P6水平存在Set大Key致命缺陷。第三层BitMap解决BigKey内存压缩99.4%规避集群阻塞Set存储ID致命问题面试官核心追问点500万用户ID为Long类型单ID占8字节500万 × 8字节 40MB叠加Redis SDS、指针等内部结构开销单个Set轻松突破100MB属于标准BigKey。BigKey危害RDB持久化阻塞Redis主线程Redis集群Slot迁移卡顿、超时集群扩容/缩容直接引发服务抖动、雪崩。BitMap优化原理无需存储完整用户ID仅占用二进制位标记领取状态1bit代表1个用户1Byte 8bit500万用户仅需5000000 ÷ 8 ≈ 625KB内存相比Set压缩99.4%。命令SETBIT key offset 1offset为连续数字时内存极小。隐藏深坑雪花ID偏移量过大导致OOM业务用户ID为19位雪花Long直接将ID作为offset执行SETBITRedis会补全0至超大偏移位置内存直接溢出崩溃。终极解决方案ID自增映射P7核心得分点分配活动逻辑ID用户首次进入活动通过INCR activity_user_seq分配从1开始连续自增数字logicId500万用户区间为1~5000000ID映射缓存userId与logicId映射关系存入本地Caffeine缓存 辅助Redis HashBitMap存储使用连续logicId作为BitMap offset偏移紧凑内存稳定600KB左右。优化后BitMap版Lua脚本-- KEYS[1]: stock_1001 库存key-- KEYS[2]: users_1001 用户领取BitMap-- ARGV[1]: userId-- ARGV[2]: logicId 映射后的连续自增ID-- 1. BitMap校验是否已领取ifredis.call(GETBIT,KEYS[2],ARGV[2])1thenreturn-1end-- 2. 校验库存localstocktonumber(redis.call(GET,KEYS[1]))ifstock0thenreturn-2end-- 3. 原子扣减库存 BitMap标记已领redis.call(DECR,KEYS[1])redis.call(SETBIT,KEYS[2],ARGV[2],1)return1 进阶价值BitMap不只是节省内存同时可充当分布式事务回查的凭证WAL预写日志。第四层库存分片分片轮询解决单Key热点 局部缺货分片初衷解决单库存Key热点CPU打满总库存1万拆分为10个分片stock_0 ~ stock_9每个分片库存1000通过userId % 10路由分片并发压力分散至10个Redis分片吞吐量线性提升10倍。分片原生缺陷局部缺货Hotspot Skew流量分布不均时部分分片快速售罄部分分片剩余大量库存用户路由至空分片直接返回抢光但其他分片仍有库存属于业务逻辑Bug。解决方案Lua内部分片轮询兜底P8核心考点用户优先路由至归属分片归属分片无库存时在Lua内循环遍历其他分片找到有库存分片完成扣减扣减失败必须回滚库存。-- ARGV[1]: userId-- ARGV[2]: 总分片数 10-- 1. 计算归属分片localindexARGV[1]%ARGV[2]localstockKeystock_..index-- 2. 优先扣归属分片localresredis.call(DECR,stockKey)ifres0thenreturn1end-- DECR后库存变为-1必须回滚库存避免库存丢失redis.call(INCR,stockKey)-- 3. 轮询其余分片兜底fori1,ARGV[2]-1dolocalnextIndex(indexi)%ARGV[2]stockKeystock_..nextIndexlocalstocktonumber(redis.call(GET,stockKey))ifstock0thenlocalsubResredis.call(DECR,stockKey)ifsubRes0thenreturn1else-- 当前分片刚好被抢空回滚继续遍历redis.call(INCR,stockKey)endendend-- 所有分片无库存return-1 分层总结分片解决并发性能分片轮询解决业务公平性二者缺一不可。第五层RocketMQ事务消息闭环解决Redis成功、MQ发送失败丢券问题常规方案全部存在漏洞先发MQ再扣RedisRedis扣减失败无法撤回消息数据库生成脏券同步重试/内存重试服务宕机、OOM会丢失内存重试队列无法兜底。标准方案事务半消息三阶段协议核心逻辑颠倒先发半消息再执行Redis抢券最后提交消息仅Commit后消息才会投递至数据库消费者。发送Half半消息向RocketMQ发送半消息MQ暂存消息不投递给DB消费端执行本地事务半消息发送成功后执行Redis抢券Lua脚本提交/回滚消息Lua返回抢购成功向MQ发送Commit消息下发消费者异步落库Lua返回失败向MQ发送Rollback消息直接丢弃。极端故障兜底事务回查机制故障场景Lua抢券成功服务断电未向MQ发送Commit/Rollback。MQ等待超时后自动触发checkLocalTransaction回查接口判断本地事务执行状态。回查核心逻辑依靠BitMap作为唯一凭证publicLocalTransactionStatecheckLocalTransaction(Messagemsg){longlogicIdmsg.getLogicId();// 查询BitMap领取标记BooleanisGrabredis.getbit(users_1001,logicId);if(Boolean.TRUE.equals(isGrab)){// BitMap置1Lua原子执行成功提交消息落库returnLocalTransactionState.COMMIT_MESSAGE;}if(Boolean.FALSE.equals(isGrab)){// 未抢到回滚丢弃消息returnLocalTransactionState.ROLLBACK_MESSAGE;}// Redis超时、抖动返回未知等待下次回查returnLocalTransactionState.UNKNOW;} 闭环关键库存扣减与BitMap标记在同一个Lua脚本原子执行BitMap状态永久可信作为分布式事务唯一判定依据。第六层本地缓存降级火种计划Redis集群整体宕机容灾故障场景Redis主从切换失败、集群全部不可用500万流量直接打透应用同步请求击穿MySQL引发雪崩。架构设计思想高并发活动核心准则可用性优先于一致性Redis故障允许有损降级但服务不可完全不可用。实现方案Caffeine/Guava本地库存熔断降级预热本地库存活动启动前每台应用JVM预热少量本地库存总库存1%示例100张使用AtomicInteger本地扣减熔断触发规则通过Sentinel/Hystrix监控Redis连续超时熔断器打开自动切换降级逻辑降级落库策略本地扣减成功异步发送MQ记账若MQ同步故障落地本地磁盘日志服务恢复后补偿落库。降级代码示意publicbooleangrabCoupon(StringuserId,longlogicId){// 熔断器打开Redis不可用走本地降级if(circuitBreaker.isOpen()){intremainlocalStock.decrementAndGet();if(remain0){sendMqAsync(userId,logicId);returntrue;}returnfalse;}// Redis正常执行标准Lua抢券逻辑returnredisLuaGrab(userId,logicId);}降级方案代价面试必须主动说明库存总数不可精确控制多台机器本地库存叠加可能少量超发无法跨机器全局用户去重存在少量重复领取可能收益服务不报错、用户不会出现全量失败MySQL不会被打崩。 架构师底层思维系统可以降级有损但不能直接雪崩投降。六层防线完整分级总结层级解决核心问题核心技术方案对应职级网关拦截层大量无效流量穿透后端Nginx网关 Redis全局计数器拦截下沉P6Lua原子盾层并发竞态、超发、重复领券Redis Lua脚本原子执行P6BitMap压缩层500万用户去重产生BigKeyID连续映射 BitMap二进制存储P7分片轮询层单KeyRedis热点、分片局部缺货库存分片 Lua跨分片轮询兜底P8事务消息闭环层Redis成功MQ失败丢券数据不一致RocketMQ半事务消息 BitMap事务回查P8本地缓存降级层Redis集群整体宕机服务雪崩熔断组件 JVM本地库存有损降级P8面试标准口述话术可直接背诵面试官这个场景表面是高并发库存扣减本质是流量治理、存储性能、分布式一致性、全链路容灾四个维度的综合架构设计我的整体方案分为六层完整防线流量治理采用网关拦截下沉通过Redis全局计数器在Nginx层拦截99%无效请求避免海量流量穿透到应用和Redis并发安全使用Lua脚本将用户去重、库存校验、库存扣减打包为原子逻辑从根源消除并发竞态保证绝对不超发存储优化通过自增逻辑ID映射BitMap替代Set集合解决500万用户去重的BigKey问题同时BitMap作为事务凭证热点分片优化库存拆分多片分散Redis单Key压力Lua脚本内部跨分片轮询解决分片流量倾斜导致的局部缺货业务bug分布式一致性基于RocketMQ事务半消息实现全链路闭环Redis操作与MQ消息投递强绑定服务宕机依靠BitMap状态做事务回查解决Redis成功MQ失败丢券问题极端容灾降级引入JVM本地缓存库存熔断降级Redis整体不可用时开启有损降级保障服务可用防止MySQL雪崩。整套方案经过亿级流量线上活动验证覆盖正常、并发、存储、分布式故障、中间件全挂各类场景。三档回答对应薪资分级仅答RedisLua原子扣减P6薪资25K-35K补充BitMap、库存分片、MQ异步落库P7薪资35K-50K完整覆盖ID映射、分片轮询、事务消息闭环、本地缓存降级全链路P8薪资50K起步。核心总结技术实现无高低之分架构思考的完整度、故障兜底深度决定职级与薪资上限。