Skip to content

【面试题】1. 分布式锁在秒杀中怎么用?Redisson、ZooKeeper、etcd 你怎么选? ​

【标准答案】三个方案各有主场。Redisson 最常用: 基于 Redis 的 SETNX + Lua 释放锁,看门狗自动续期防止锁过期,主打低延迟。秒杀场景首选,一个商品对应一把锁,扣完即释放,锁持有时间毫秒级。ZooKeeper 更可靠: 临时顺序节点 + Watcher 机制,客户端挂了节点自动删除,不担心死锁。但在秒杀场景里 ZK 的写性能明显弱于 Redis(ZK 所有写都走 Leader),不适合高并发加锁。etcd 类似 ZK 但更轻量: Go 实现,部署简单,适合 K8s 环境。

选型公式: 纯秒杀扣库存 → Redisson(快);需要严格顺序和强一致性 → ZK(稳);K8s 微服务体系 → etcd(原生)。秒杀场景 90% 选 Redisson,剩下 10% 是特殊需求。


【面试题】2. 缓存穿透、缓存击穿、缓存雪崩,三个场景怎么区分?怎么解决? ​

【标准答案】三个问题名字像,根因完全不同:

缓存穿透:查一个根本不存在的 key。 黑客用随机 ID 狂查缓存,全 miss 打到 DB。解法:布隆过滤器——把所有合法 ID 预先存进去,查之前先问过滤器"这 key 可能存在吗?"返回否直接拒绝。没布隆过滤器也可以用空值缓存(设短 TTL 30 秒)。

缓存击穿:一个热点 key 过期瞬间,海量请求直接打 DB。 比如秒杀商品缓存刚好在活动开始时过期了。解法:互斥锁——第一个请求拿锁去查 DB 重建缓存,其他请求排队等结果,不直接查库。或者永不过期——物理不过期,用异步任务刷新数据。

缓存雪崩:大量 key 同时过期,或 Redis 挂了,所有请求涌向 DB。 解法:过期时间加随机偏移(原 3600 秒 → 3600 ± 300 秒),避免集中过期。Redis 用哨兵/Cluster 做高可用,挂了自动切换。

一句话记: 穿透 = 查不存在的东西(布隆过滤器),击穿 = 一个热点过期(互斥锁),雪崩 = 大面积过期或挂了(过期随机化 + 高可用)。


【面试题】3. 流量削峰与错峰,网关、应用、MQ 三层各做什么? ​

【标准答案】削峰是层层拦河坝,三道防线各自为战:

网关层(第一道,Nginx/OpenResty): 用 Lua 做最轻量的拦截——令牌桶限流(每个用户 1 token/秒)、秒杀时间窗口校验(没到时间全拒绝)、IP 黑名单直接 403。这一层拦 80% 无效请求,成本极低。

应用层(第二道,Sentinel/Guava RateLimiter): 对打到 Java 的请求做精确流控。按秒杀商品 ID 做热点参数限流——比如 iPhone 秒杀接口最多允许 5000 QPS,超了就排队或降级。这层还能做业务级校验:用户资格、防重复提交、风控打分。

MQ 层(第三道,RocketMQ/Kafka): 前两道放行后,创建订单的请求不直接写数据库,而是丢进 MQ。MQ 的队列深度就是"错峰缓冲区"——1 秒涌入 5000 条消息,消费者按自己能扛的节奏(比如 500 条/秒)慢慢消费,不压垮数据库。

核心设计: 网关控制进入量,应用层做蓄洪,MQ 做泄洪。三层削峰下来,10 万 QPS 到数据库只有几百 QPS。


【面试题】4. 多级限流怎么设计?网关令牌桶 + Sentinel + 数据库连接池各配什么策略? ​

【标准答案】限流做三层,防止任何一层被打穿。

第一层:网关令牌桶(Nginx limit_req / OpenResty)。 按 IP 和用户 ID 双维度限流。每个 IP 每秒 20 个请求,每个用户 5 个请求。超了直接返回 HTTP 429,不给应用层添乱。令牌桶的好处是允许突发——配置 burst=10,短时间超过限值也不会立刻拒绝,用排队缓冲。

第二层:Sentinel 应用层。 按接口 + 热点参数做精确限流。下单接口设 8000 QPS,秒杀商品 ID 作为热点参数限 2000 QPS。Sentinel 的滑动窗口比令牌桶更实时,能在秒级调整。触发了走降级逻辑——返回排队编号或直接告诉用户"系统繁忙"。

第三层:数据库连接池(HikariCP / Druid)。 这是最后防线。连接池最大连接数设 200,等待超时 1 秒。连接池满了说明前面两层限流失效了,maximumPoolSize 就是硬上限。Druid 还能拦截危险 SQL,慢查询超过 1 秒自动 kill。

协调原则: 网关限流 > 应用限流 > 连接池上限,梯度递减。网关放 2000 进来,Sentinel 再卡 500,数据库连 200 都不会满载。每层限流阈值都要在压测时验证过。


【面试题】5. 秒杀防重复提交怎么做?幂等性如何保证? ​

【标准答案】用户连续点三次按钮,只能产生一个订单——这是幂等性的底线。

前端第一道:按钮防抖。 点击后立刻灰掉 2 秒,配合 loading 动画。但前端防抖只能挡住正常用户,脚本直接发 HTTP 请求绕不过。

后端关键防线:token 机制。 用户进入秒杀页面时,后端生成一个一次性 token(UUID)发给前端并存 Redis(key = seckill:token:{userId}:{skuId},value = token,TTL 5 分钟)。用户提交下单时带上 token,后端用 Redis Lua 原子校验:if redis.call('get', KEYS[1]) == ARGV[1] then redis.call('del', KEYS[1]); return 1 else return 0 end。token 用一次就删,第二次请求 token 不匹配,直接拒绝。这保证了一个用户对同一个商品只能成功提交一次。

数据库兜底:唯一索引。 订单表建 user_id + sku_id 联合唯一索引。即使 token 没拦住(Redis 挂了),数据库层面也会拒绝重复插入。不过这个只能兜底,最好在 Redis 层就挡住。

注意: token 不是防重放的,是防重复提交的。重放攻击要依赖 HTTPS + 请求签名。


【面试题】6. 数据库乐观锁和悲观锁有什么区别?秒杀场景用哪个? ​

【标准答案】悲观锁(select for update): 假定一定会冲突,先锁住数据再操作。SELECT * FROM stock WHERE id=1 FOR UPDATE 后,其他事务想查同一行数据都会被阻塞,直到当前事务提交释放锁。优点是简单粗暴一定不会超卖,缺点是锁等待期间所有并发请求串行排队,秒杀场景下 TPS 直接地板——10 个并发就堵死。

乐观锁(version 号): 假定不会冲突,操作时校验版本。UPDATE stock SET count=count-1, version=version+1 WHERE id=1 AND version=? AND count>0。更新时不加锁,但 version 对不上就更新失败(affected_rows=0),应用层重试。适合读多写少、冲突概率低的场景。

秒杀怎么选? 秒杀是典型的高冲突场景,纯乐观锁也不行——大量并发更新同一个库存行,version 冲突概率极高,重试开销大。

实战方案:Redis 挡在前面做原子扣减,数据库乐观锁做兜底。 Redis 把 99% 的并发消化掉,到数据库的并发很低,乐观锁的冲突率就可控了。悲观锁在秒杀里基本不用——除非是金融级别的扣款场景,宁愿慢也不能错。


【面试题】7. 秒杀系统上线后出故障了,你的应急预案是什么? ​

【标准答案】应急不是"出了问题再说",是"提前排练过的肌肉记忆"。

第一级:监控发现 → 判定级别。 成功率掉到 95% 以下、RT 超 2 秒、库存异常三种情况触发 P1 告警(电话 + 群通知)。P2 是降级指标(推荐服务挂了),通知但不电话。

第二级:止损三板斧。 ① 降级开关(5 秒内生效): 提前准备好的配置中心开关(Apollo/Nacos),一键关掉非核心依赖——推荐、积分、优惠券全部熔断,只保下单核心。② 限流收紧: 把 Sentinel 下单接口限流从 8000 调到 3000,优先保证已进来的人能完成。③ 切换备用链路: Redis 主挂了切从,从挂了切本地缓存兜底。每个切换操作都有操作手册,照着点就行。

第三级:问题定位 → 修复。 看慢 SQL 排名、Redis 慢查询日志、MQ 积压量、服务器 CPU/内存曲线。80% 的问题在这四张图里 30 秒能定位到。

第四级:复盘归档。 事故完 24 小时内出复盘报告:时间线、根因、影响面、改进 TODO。下次大促前检查上个事故的改进项做完了没有。

核心原则: 先止损,再排查,最后复盘。不要在 10 万用户卡着不动的时候慢慢查 bug,先关掉出问题的模块恢复业务。


【面试题】8. 面试常考:Redis 分布式锁 vs ZooKeeper 分布式锁,你怎么说服面试官选 Redis? ​

【标准答案】面试官问这个是想看你有没有做过"取舍",不是一个标准答案。

Redis 锁的优势: 性能高,单机 10 万 QPS 加锁释锁;部署成本低,秒杀本来就有 Redis,不用额外引入 ZK 集群;延迟低,比起 ZK 的写 Leader 操作少一层。

Redis 锁的硬伤: 分布式场景下不安全。主从切换时,主节点宕机但锁还没同步到从节点,新主节点认为锁不存在,两个客户端同时拿到锁——这就是"主从脑裂"问题。Redisson 的看门狗续期只能防业务处理慢导致锁提前释放,防不了主从切换丢锁。

ZK 锁的优势: 强一致性。临时顺序节点,客户端会话断开自动删除,不担心死锁。基于 ZAB 协议,不存在脑裂丢锁问题。适用场景: 扣款、库存转移等必须"一定不会重复"的场景。

说服面试官的关键一句: "秒杀场景锁持有时间是毫秒级,主从切换概率远小于锁持有期间发生故障的概率,加上有数据库乐观锁兜底,Redis 锁的微小不安全在实践中可接受。ZK 锁更适合金融交易、库存划拨这种锁持有时间长的强一致场景。"

加分话术: 可以提 RedLock 算法试图解决 Redis 锁的安全问题,但业界有争议(Martin Kleppmann 的批评),实操中 Redisson 单实例 + 乐观锁兜底更常见。


【面试题】9. 库存扣减三种方案:纯 Redis、纯 DB、Redis + DB 混合,区别是什么? ​

【标准答案】方案选型看的是你愿不愿意为一致性让步。

方案一:纯 Redis 扣减。 库存全部在 Redis,Lua 脚本原子扣,返回扣减结果。最快,单 Redis 轻松 5 万 QPS。缺点是 Redis 挂了库存全丢,数据不可恢复。适合"丢一两件库存也能接受的"低风险秒杀。

方案二:纯 DB 扣减。 库存只在数据库,UPDATE stock SET count=count-1 WHERE id=? AND count>0。最安全,ACID 保证。缺点:单行更新 TPS 瓶颈在 2000 左右,并发高了锁等待严重。适合小规模秒杀(日活几千人)。

方案三(推荐):Redis 先扣 + DB 异步兜底。 Redis 是最快的真相源,Lua 扣成功后发 MQ,数据库异步更新。对账任务定时比对两边数据。兼顾性能和安全性——Redis 带来毫秒级扣减体验,DB 提供持久化和对账兜底。缺点是对账有 5 分钟延迟,极端情况下 Redis 回滚了但 MQ 消息丢了。

你的选择取决于场景: 1000 件 iPhone 秒杀 → 必须纯 DB 或混合方案(绝不能多卖);100 万张优惠券秒杀 → 纯 Redis 够用,丢几张无所谓。面试时先反问场景再做选择,比直接给答案分更高。


【面试题】10. 秒杀全链路从用户点按钮到订单生成,分几层?每层的核心职责是什么? ​

【标准答案】秒杀全链路分七层,层层削峰、层层校验:

第一层:CDN + 静态资源层。 商品图片、CSS/JS 全在 CDN,用户打开页面不访问源站。动态数据用 Ajax 异步刷新。

第二层:网关层(Nginx/OpenResty)。 IP 限流、秒杀时间窗口校验、URL 合法性校验。无效请求在这一层直接 403/429。

第三层:WEB 应用层(Spring Boot)。 接收请求,组装参数,调用下游服务。前端削峰(按钮防抖)配合,控制进入量。

第四层:风控 + 资格校验层。 用户身份验证、购买资格(布隆过滤器 + Bitmap)、频控(每人限购 N 件)、风控打分。没资格的在这一层截住。

第五层:库存扣减层(Redis Lua)。 核心环节——原子扣库存。扣成功继续,扣失败返回售罄。毫秒级完成。

第六层:MQ 削峰层(RocketMQ)。 扣减成功的请求变成消息写入队列,异步通知订单服务。库存服务和订单服务在这里解耦。

第七层:订单持久化层(MySQL)。 从 MQ 消费消息,创建订单,乐观锁兜底。这是数据最终的落脚点,ACID 保证不丢数据。

核心原则:越靠前越轻、越靠后越重。 前端毫秒级响应,中间件削峰缓冲,数据库慢慢持久化。每一层挂了下一层还有兜底,不会单点故障全链路崩。面试时画出这七层,比列技术栈有说服力得多。