【面试题】1. 说说秒杀系统的核心难点是什么?你是怎么解决的?
【标准答案】秒杀有四个核心难点:
第一,高并发读。 商品详情页在秒杀开始前几秒会被疯狂刷新,QPS 可能是平时的 100 倍。我的方案是:CDN + 本地缓存 + 动静分离。静态资源全走 CDN,动态数据(库存、价格)用 Redis 缓存,页面做静态化,不查数据库。
第二,高并发写。 秒杀开始一瞬间,几万人同时点"抢购",全部打到库存扣减上。我用的是 Redis 预扣库存 + 数据库异步扣减。Redis 先扛住并发,数据库慢慢写,中间用 MQ 异步解耦。
第三,超卖。 100 件商品,绝对不能卖出 101 件。我用 Redis Lua 脚本做原子扣减 + 数据库乐观锁兜底,双重保险。
第四,黄牛与风控。 脚本秒杀、恶意刷单。我在网关层做了 IP 限流 + 用户限流,下单链路加了验证码、答题、防重复提交,风控侧用规则引擎识别异常用户。
【面试题】2. 你们项目的流量削峰是怎么做的?为什么不能硬扛?
【标准答案】硬扛是最蠢的做法,秒杀那 0.1 秒的流量是平时的 500 倍,按峰值配置服务器,一年有 364 天在浪费钱。
我们做了三层削峰:
第一层:前端削峰。 秒杀按钮到时间才亮,提前 CDN 缓存静态页面,用户拉不到最新数据就不会频繁刷新,浏览器端做了 5 秒内只允许一次请求。
第二层:网关限流。 Nginx 层做连接数限制 + 令牌桶限流,超过阈值的直接返回"活动太火爆",不往后透传。每个用户每分钟最多请求 20 次秒杀接口。
第三层:MQ 异步削峰。 请求到了应用层,不直接操作数据库,而是丢进消息队列。数据库按自己的处理能力消费,请求峰值被拉平成一条平滑的曲线。1 秒进来 10 万请求,DB 用 10 秒慢慢处理。
【面试题】3. Redis 预扣库存和数据库扣库存,谁先谁后?为什么?
【标准答案】Redis 先扣,数据库后扣。思路叫"先抗后稳"。
流程是: 用户下单 → Redis Lua 原子扣减 → 扣减成功 → 发 MQ 消息 → 异步写数据库 → 扣减失败 → 回滚 Redis。
为什么 Redis 先? 因为 Redis 单线程 + 内存操作,单机 10 万 QPS 很轻松,MySQL 单机最多扛几百的写 QPS。让 Redis 当第一道闸口,扛住 99% 的无效请求——库存没了就直接返回,不用到数据库。
那 Redis 挂了怎么办? 用 Redis Cluster 或哨兵保证高可用,库存初始化写 Redis 的同时也写一份到本地缓存做降级。另外数据库乐观锁做最后一道防线——就算 Redis 扣过了,数据库落库时 version 对不上也插不进去。
【面试题】4. 秒杀场景下,分布式锁用 Redis 还是 ZooKeeper?为什么?
【标准答案】秒杀场景首选 Redis 分布式锁,原因三个:
第一,性能。 Redis 是内存操作,加锁解锁都是微秒级。ZooKeeper 基于 CP 协议,加锁需要半数节点确认,毫秒级延迟在秒杀场景不可接受。
第二,简单。 我们用 Redisson 封装的分布式锁,自动续期、可重入、支持公平锁,一行代码搞定。ZK 需要处理临时节点的创建和删除,代码复杂。
第三,够用。 秒杀的锁是短时间、高并发的场景。Redis 在绝大部分情况下已经足够保证互斥。ZK 更适合长事务、强一致性的场景,比如分布式配置变更、分布式队列。
但要注意坑: Redis 锁必须设过期时间,防止死锁。释放时要判断是不是自己的锁(用 UUID + Lua 脚本实现),不能误删别人的锁。Redisson 已经帮你做了这些。
【面试题】5. 缓存雪崩、击穿、穿透有什么区别?秒杀系统怎么防?
【标准答案】三个词经常搞混,一句话区分:
缓存穿透: 查一个不存在的数据。比如用户请求 id=-1 的商品,缓存没命中,每次都穿透到数据库。解法:布隆过滤器 + 空值缓存。布隆过滤器先判断 key 是否可能存在,不存在直接返回。已经确认不存在的 key,缓存一个 null,过期时间设短一点。
缓存击穿: 一个热点 key 突然过期,大量请求同时打到数据库。秒杀的库存 key 最典型。解法:热点 key 永不过期,在代码里主动异步刷新;或者用互斥锁,只让一个线程回源查数据库,其他线程等待。
缓存雪崩: 大量 key 同时过期,缓存层整体失效,所有请求压到数据库。解法:过期时间加随机值,比如基础时间 60 秒 + 随机 0~30 秒,避免同时过期。另外用 Redis Cluster 或主从保证缓存层本身高可用。
【面试题】6. 下单接口防重复提交你怎么实现?
【标准答案】核心思路是"唯一标识 + 状态机"。
第一步:全局唯一 token。 用户进入下单页时,后端生成一个 token(UUID),存 Redis,key 是 order_token:userId:skuId,value 是请求体 MD5,过期时间 5 分钟。前端提交时必须携带这个 token。
第二步:Redis 原子校验。 下单接口拿到 token,用 Lua 脚本做两步原子操作:先 GET 判断 token 存不存在,存在就 DEL 掉并返回成功,不存在就返回重复提交。
第三步:数据库唯一约束兜底。 订单表加联合唯一索引,比如 user_id + sku_id + 秒杀场次。就算 Redis 被穿透了,数据库也插不进重复记录。
第四步:前端配合。 按钮点击后立刻置灰,1 秒内不允许再次点击,减少无效请求打到后端。
【面试题】7. 秒杀中消息队列消息丢失怎么办?你怎么保证不丢?
【标准答案】消息丢失分三种场景,逐一解决:
生产者丢消息: 用 RabbitMQ 的事务或 Confirm 机制。我们用异步 Confirm 模式,生产者发送消息后注册回调,Broker 确认收到才删本地缓存。没确认的就重发。
MQ 自己丢消息: RabbitMQ 默认消息存内存,重启就丢。我们把队列和消息都设为持久化(durable=true),Delivery Mode 设为 2(持久化消息)。Kafka 则是置 acks=all 加 replication-factor=3,所有副本写完了才返回成功。
消费者丢消息: 消费者拿到消息先处理业务,成功了再手动 ACK。设置成手动确认模式,绝对不能自动 ACK。如果处理失败,消息会重新入队,由其他消费者或下一次重试处理。
极端兜底: 每天定时跑对账任务,对比 Redis 扣减记录和数据库订单,有差异就补偿。
【面试题】8. 多级限流怎么做?网关层和应用层限流有什么区别?
【标准答案】秒杀系统至少做三级限流:
第一级:网关层(Nginx/API Gateway)。 用令牌桶或漏桶算法控制总入口流量。比如 Nginx 的 limit_req 限制每个 IP 每秒 10 个请求,多余的直接返回 429。这层保护的是整个系统不被冲垮,流量大到这层就开始丢弃。
第二级:应用层(Sentinel/Hystrix)。 用 Sentinel 做接口级限流、热点参数限流。比如秒杀下单接口 QPS 限制 5000,超过就快速失败。还能针对用户 ID 做热点限流,同一个用户 1 秒内最多触发一次下单。
第三级:数据库层。 连接池限制最大连接数,Druid 的 maxActive=100。当连接池满了,新的请求排队等待,等不到就超时报错。
网关层和应用层区别: 网关层是粗粒度的,按 IP/域名/路径限流,适合拦截 DDoS 和爬虫。应用层是细粒度的,可以按业务维度(用户 ID、商品 ID)限流,能做更精准的流量整形。网关是先发制人,应用层是精准手术。
【面试题】9. 秒杀扣库存,用数据库乐观锁和悲观锁各有什么优缺点?
【标准答案】
乐观锁(推荐): 用 version 号或时间戳。update stock set count=count-1, version=version+1 where id=1 and version=#{oldVersion}。优点是不锁行,并发高,不会死锁。缺点是并发极高时会大量回滚重试,浪费 CPU。秒杀场景一般用乐观锁,因为写冲突是短时间的,失败后快速返回就行。
悲观锁: select ... for update 锁住行,事务提交才释放。优点是绝对不超卖,不会重试。缺点是并发能力极差,行锁会阻塞其他所有请求,秒杀场景下相当于把数据库变成了单线程。
选型结论: 秒杀用乐观锁做主方案,但要配合 Redis 前置拦截,把无效请求挡在数据库外面。流量削到数据库时已经很少了,乐观锁的重试代价可接受。悲观锁适合金融扣款(强一致、不能出错),不适合秒杀。
【面试题】10. 你做过的项目里,遇到过什么秒杀线上故障?怎么排查和恢复的?
【标准答案】讲一个真实的:有一次秒杀开始后,前 3 秒正常,第 4 秒开始所有用户看到"系统繁忙"。
排查过程: 第一步看监控,发现 Redis 的 CPU 从 30% 瞬间飙到 95%,连接数满了。第二步看慢查询日志,发现 Lua 脚本执行时间从 1ms 涨到了 500ms。第三步定位到原因:Redis 里有个大 key —— 用户资格校验的 hash,存了 300 万条数据,hget 操作越来越慢,拖累了整个实例。
恢复方案: 第一,立刻把资格校验拆分到独立 Redis 实例,秒杀主流程走原来的 Redis。第二,资格数据改成布隆过滤器 + 延迟查询,90% 的不合法用户直接过滤,不需要查 hash。第三,加了定期的大 key 扫描和告警。
教训: 不要在一个 Redis 实例上混合热数据和大 key,热 key 要单独隔离。Redis 不是垃圾桶,大的离线数据不应该放进去。大 key 问题要在压测阶段发现,不能等线上爆炸。