【面试题】1. 秒杀系统从0到1,全链路架构怎么画?核心模块分几层?
【标准答案】秒杀架构不是把单体拆成微服务就完了,是按"流量递减"分层设计。
标准六层架构(自顶向下): ①CDN/动静分离层——HTML、JS、图片走CDN,源站只收Ajax动态请求;②网关层(OpenResty/Spring Cloud Gateway)——IP限流、黑名单拦截、时间窗口校验,拦掉50%无效流量;③应用层——资格校验、风控、库存扣减Redis Lua、下单异步化;④消息队列层——RocketMQ解耦,削峰填谷,订单异步消费;⑤数据层——库存卫Redis、订单落MySQL、搜索走ES;⑥监控运维层——全链路QPS/成功率/库存大盘,告警规则。
核心设计原则: 每一层只做自己该做的事,不做重复校验。网关不做业务,MQ不做实时响应承诺,Redis不持久化订单。坏了一个模块,降级开关切断,主链路继续跑。秒杀架构的本质是"防御性架构"——假设每个环节都会挂,给每个环节准备B计划。
【面试题】2. 流量削峰和错峰有什么区别?网关、应用、MQ三层怎么分工配合?
【标准答案】削峰是把尖峰压平,错峰是把同一时刻的流量错开到不同时刻。两个词不是一个意思。
削峰——流量整形(网关层): 令牌桶算法控制每秒放行数量,比如秒杀集群能扛2万 QPS,网关令牌桶就设1.8万,超过的直接排队或拒绝。Nginx的 limit_req_zone 配 rate=18000r/s,超出的返回429。这是"硬削",把尖峰砍平到系统容量线以下。
错峰——时间分散(应用层): 秒杀开始瞬间所有请求同时到达,应用层加随机延迟——返回前端"请等待,正在排队中",前端倒计时3-5秒再重试。不同用户看到不同倒计时,请求被错到不同时刻。再配合答题验证码——答完题的1秒内才能发起下单,又是一个时间窗口错开。
MQ是第三种——填谷: 削峰削掉和错峰错掉的不是丢了,是被MQ兜住了。订单请求进MQ队列,消费者按自己的速率慢慢消费,不平白失效。网关削峰(令牌桶卡容量上限)+ 应用错峰(前端倒计时打散请求)+ MQ填谷(异步消化积压),三层配合把秒杀QPS从"瞬间尖刺"变成"平稳矩形"。
【面试题】3. 库存扣减方案怎么选?纯Redis扣减、纯DB扣减、Redis+DB混合方案各自的优劣?
【标准答案】三种方案不是越复杂越好,要看秒杀量级。
纯Redis扣减(扛高并发主力): Lua脚本原子扣,单机5-8万QPS,RT 1ms以内。秒杀99%的流量靠Redis扛。缺点:Redis挂了或主从切换丢数据,库存可能多扣或少扣。适合日均百万级QPS的秒杀场景。
纯DB扣减(简单可靠): UPDATE stock SET count = count - ? WHERE id = ? AND count >= ? 走行锁,强一致,不会超卖。缺点:单机MySQL扛2000 QPS就到头了,秒杀10000人同时下单直接崩。适合小型活动、几百人抢的东西。
Redis + DB混合(推荐): Redis做第一道扣减(速度快,挡流量),DB做第二道兜底(乐观锁防超卖)。Redis扣成功 → 订单消息进MQ → 消费端用乐观锁更新DB库存。如果乐观锁更新失败(affected_rows=0),说明Redis和DB不一致了,触发回滚和对账。这就是"Redis扛量,DB保底"的经典组合。
一句话: 日均QPS低于1000纯DB足够,1000-50000用Redis+DB混合,5万以上Redis扛主、DB纯异步兜底。量级决定架构。
【面试题】4. 分布式锁在秒杀里到底该怎么用?Redisson、ZooKeeper、etcd怎么选?
【标准答案】分布式锁在秒杀里的核心用途不是扣库存(库存用Lua原子扣),是保护"不能并发执行的操作"。
三个典型场景: ①库存预热——只有一个实例能执行预热脚本把库存从DB加载到Redis,避免多实例同时预热覆盖数据;②定时对账——只有一个实例跑对账任务;③库存回滚——只有一个实例执行回滚逻辑,防止重复回滚。
Redisson(首选,前提有Redis): 基于Redis的RedLock算法,自动续期看门狗(watchdog),锁到期自动释放。RLock lock = redisson.getLock("seckill:warmup:lock"); lock.lock(30, TimeUnit.SECONDS),看门狗每10秒续期一次,防止业务没执行完锁就过期。优点是快(Redis内存操作),缺点是Redis主从切换时可能锁丢失(概率极低,秒杀预热丢一次锁可以接受)。
ZooKeeper(强一致场景): 临时顺序节点 + Watch机制,CP强一致,主挂了锁也不丢。但比Redis慢10倍以上,秒杀预热这种场景杀鸡用牛刀。适合金融交易、资金结算这类绝对不能丢锁的场景。
etcd(云原生推荐): 和ZooKeeper类似走Raft协议,但API更现代(gRPC),K8s生态无缝。并发性能比ZooKeeper好一些,但依然不如Redis。
一句话: 秒杀用Redisson(快、够用),资金用ZooKeeper(强一致),K8s生态用etcd。分布式锁在秒杀里是配角,不是主角——库存扣减不靠锁。
【面试题】5. 多级限流体系怎么设计?网关令牌桶、应用Sentinel、数据库连接池各管什么?
【标准答案】限流不是单点拦一道就完了,要层层设防,每层粒度不同。
第一层——网关令牌桶(Nginx/OpenResty): 粒度最粗,按全站或IP限流。limit_req_zone $binary_remote_addr zone=seckill:20m rate=5r/s,每个IP每秒最多5个请求。这一层把爬虫、脚本、刷量流量全挡在JVM外面。令牌桶的妙处在于允许突发——桶里攒的令牌可以应对短时尖刺,长期超过rate才拒绝。
第二层——应用Sentinel(热点参数限流): 粒度最细,按秒杀商品ID限流。同一个商品ID扣库存接口最多2000 QPS,多出来的走降级返回"正在排队"。Sentinel的滑动窗口是秒级精度,比网关的令牌桶更能感知瞬时流量波动。而且支持流控规则从控制台热更新,不用重启应用。
第三层——数据库连接池(Druid/HikariCP): 最后一层物理限制。连接池最大连接数50,maxWait 设1000ms。1000ms内拿不到连接抛异常,而不是让线程无限等。这一层不是主动限流,是被动兜底——前面两层如果都失效了,连接池保证数据库不会被冲垮。
三者关系(不是三国,是接力): 网关拦IP(99%非法流量),Sentinel拦接口(保护关键资源),连接池拦残留(物理兜底)。任何一层单独用都有盲区,三层组合无死角。面试时画这个图:流量从左到右 → 网关砍 50% → Sentinel砍 30% → 连接池砍最后 5%,到数据库的只有 15% 有效流量。
【面试题】6. 幂等性怎么设计?秒杀下单怎么防止用户多点几次按钮就重复下单?
【标准答案】幂等性一句话:同一个操作无论执行多少次,结果和一次一样。
第一道——前端防抖: 按钮点击后立即 disabled,显示"提交中...",2秒内不可再次点击。防50%的误触,但不防脚本绕前端。
第二道——下单Token机制(核心): 用户进入秒杀页面时,后端生成一个唯一token,存Redis:SET seckill:token:user123:item456 "token_abc123" EX 600。前端提交下单时带这个token,后端用Lua一次性校验并删除:local token = redis.call('get', KEYS[1]); if token == ARGV[1] then redis.call('del', KEYS[1]); return 1 else return 0 end。token只能消费一次,第二次请求发现Redis里token没了或变了,直接返回"订单已提交,请勿重复操作"。不管用户点多少次按钮,只有第一次扣库存和创建订单会执行。
第三道——数据库唯一索引兜底: 订单表加联合唯一索引 UNIQUE KEY uk_user_item (user_id, item_id)。前端、token都失效了(极低概率),数据库最后一道防线报 DuplicateKeyException,捕获后查已有订单状态返回"已下单"。
一句话: 前端防手抖,Token防重复提交,唯一索引防数据重复写入。三道防线确保"一个人对一个商品只能成一次单"。
【面试题】7. 动静分离怎么做?CDN在秒杀里省了多少服务器?
【标准答案】动静分离是秒杀省钱第一招——把不变的页面内容提前推到CDN,源站只处理动态请求。
什么是"动"什么是"静": 商品图片、详情描述HTML、CSS/JS文件这些是静态内容,秒杀开始前1小时全部推到CDN,秒杀期间完全不回源。只有库存查询、下单提交这些必须实时计算的是动态请求,才回源站。一个秒杀页面的体积中,90%是静态资源,只有10%是动态接口。
CDN怎么配: 秒杀页面的HTML也做CDN缓存,Cache-Control设 max-age=60(1分钟),秒杀开始前先预热到全国各CDN节点。但页面里有库存数字怎么办?库存数字用Ajax异步请求单独获取,页面骨架走CDN,数据缺口走动态接口。这叫"页面骨架静态化,数据动态填充"。
省了多少服务器: 假设 100 万人同时开秒杀页面,如果不做CDN,页面500KB,需要100万×500KB=500GB的带宽和源站承载能力。一般需要50-100台Web服务器。做了CDN,静态资源全走CDN,源站只收Ajax请求和下单请求,有效流量降到原来的10%。50台服务器省到5台,成本砍90%。
大促前的预热动作: 秒杀前1小时,用脚本把商品详情页、活动页、静态资源列表逐个请求一遍(模拟用户访问),让CDN边缘节点提前拉取缓存。不做预热,秒杀开始时第一波请求全部回源——"缓存击穿"的CDN版本。
【面试题】8. 秒杀怎么压测?JMeter能扛多少并发?全链路压测和单接口压测有什么区别?
【标准答案】压测不是测"系统最高能跑多少QPS",是测"在预期的秒杀QPS下,系统表现是否稳定"。
压测前先算目标QPS: 假设秒杀商品库存1000件,预期参与人数10万,转化率5%,下单峰值QPS预估=10万×5%÷10秒窗口=500 QPS。但秒杀开抢的前3秒是峰值中的峰值,加3倍安全系数=1500 QPS。这就是压测目标,不是想当然的10万QPS。
JMeter实操(单机极限): JMeter单机跑HTTP请求压测大概能到3000-5000 QPS(取决于响应体大小和机器配置)。超出就要做分布式压测——多台JMeter slave同时压,master汇总结果。秒杀压测至少准备5台压测机,每台跑500 QPS,合计2500 QPS够用。关键配置:线程组设Ramp-up为0(瞬间并发),定时器设固定吞吐量,别用高斯随机定时器(秒杀不是随机的)。
全链路 vs 单接口的区别: 单接口压测只压/seckill/submit一个接口,单纯看这个接口能扛多少QPS。全链路压测要模拟真实用户行为——先请求商品详情,再请求库存查询,再提交下单,链路完整。全链路压测暴露的是系统串联后的短板(比如扣库存快但MQ发送慢,整体RT被拖慢),单接口压测看不到这个。秒杀上线前,全链路压测必须跑够3轮,每轮持续至少5分钟。
压测必须观察的指标: QPS达成率(实际/目标)、P99 RT(< 100ms及格,< 50ms优秀)、错误率(< 0.1%)、Redis CPU(< 70%)、数据库连接数(< 池大小80%)。压测不是为了压挂,是为了验证系统在目标QPS下稳如老狗。
【面试题】9. 下单链路怎么优化?异步化、并行化、减少RPC各怎么做?
【标准答案】下单链路的优化目标:用户从点提交到看到"下单成功",RT控制在200ms以内。
异步化(拆主次): 下单链路里哪些是"必须同步完成"的?扣库存、创建订单。哪些可以"异步搞定"?发短信、送积分、发优惠券、写用户行为日志。做法:扣库存成功 → 发MQ消息 → 立即返回"下单成功"。短信、积分这些异步消费MQ消息完成,用户不需要等。主链路从原来的8步串行砍到2步,RT从500ms降到80ms。
并行化(无依赖的并发): 主链路里必须同步做的事也有优化空间。比如风控校验和库存扣减没有先后依赖(风控只管人不管库存),用 CompletableFuture 同时发起:CompletableFuture.allOf(riskCheck(), deductStock())。两个操作各50ms,并行后整体50ms,而不是串行的100ms。前提是两者没有数据依赖,有依赖不能并行。
减少RPC(合并请求): 一次下单可能调5个微服务:用户服务(查实名)、风控服务(查风险)、库存服务(扣库存)、优惠券服务(核销券)、订单服务(创建订单)。5次RPC每次20ms=100ms。优化方案:把用户+风控查询合并成一个接口——"查询用户资格和风控等级",1次RPC拿到两个结果。库存和优惠券也可以通过批量接口串起来,整体砍掉2-3次RPC。
缓存预热减少RPC: 用户资格数据、优惠券数据提前预热到本地Caffeine缓存,下单时本地读,0次RPC。
一句话: 异步砍非核心步骤,并行没依赖的操作,合并能一起查的RPC,预热的放本地不RPC。四招下去,下单RT从500ms压到60ms不是事。
【面试题】10. 分布式ID怎么生成?雪花算法在秒杀里的坑和美团Leaf怎么解的?
【标准答案】秒杀订单不能靠数据库自增ID——分库分表后自增会冲突,而且提前暴露订单量(黄牛通过ID差算出销量)。
雪花算法(Snowflake)标准结构: 64位长整型,1位不用 + 41位毫秒时间戳 + 10位机器ID + 12位序列号。单机每毫秒能生成4096个ID,QPS约409万。秒杀够用了,但有三个坑。
坑一:时钟回拨。 NTP时间同步导致服务器时钟往回跳了几毫秒,雪花算法就会生成重复ID。美团Leaf的解法:启动时记录当前时间到本地文件,每次生成ID前对比文件时间和当前时间,如果发现回拨,等(自旋)到时间恢复。回拨超过5秒,抛异常人工介入。
坑二:机器ID怎么分配? 10位机器ID理论上要手动配1024个不同的workId,根本配不过来。Leaf方案:用ZK的临时顺序节点自动分配——每个实例启动时在ZK创建临时顺序节点 /leaf/worker-{seq},序列号就是workId。实例挂了临时节点自动删除,新实例拿到空闲的ID。
坑三:序列号用完了。 同一毫秒内4096个序列号用完,雪花算法会等到下一毫秒。秒杀峰值同毫秒可能超过4096个请求,解决方案是用 号段模式(Segment)——Leaf的另一种模式:提前从数据库批量取一段ID(比如1-1000),用完了再去取下一段,避免每毫秒的4096限制。
一句话: 雪花算法适用于QPS < 400万的场景,Leaf的segment模式适用于更高并发,时钟回拨靠本地文件+自旋解决,机器ID靠ZK自动分配。