【面试题】1. 面试官问"秒杀怎么保证不超卖?"你怎么用一句话回答,然后展开?
【标准答案】一句话:Redis Lua 原子扣减做主力,数据库乐观锁兜底,两道防线保证不多卖一件。
展开: Redis Lua 脚本一次性完成"读库存 → 判够不够 → 扣减"三步,Redis 单线程执行 Lua 保证原子性。脚本核心逻辑:local stock = redis.call('get', KEYS[1]); if tonumber(stock) >= tonumber(ARGV[1]) then redis.call('decrby', KEYS[1], ARGV[1]); return 1 else return 0 end。扣成功返回 1,失败返回 0,没有中间态。
为什么还要数据库乐观锁? Redis 虽然是主力但可能丢数据——主从切换瞬间主节点宕机,扣减记录没同步到从节点。所以数据库用 UPDATE stock SET count=count-?, version=version+1 WHERE id=? AND version=? AND count>=? 做第二道防线。affected_rows=0 说明有人抢先或库存不够,回滚 Redis。
防超卖三层: Redis Lua(毫秒级,99.99% 场景搞定)→ DB 乐观锁(兜底,Redis 出问题拦截)→ 对账脚本(事后发现差异报警)。前两道在线防,第三道离线查,三重保险一件不多卖。
【面试题】2. Kafka、RocketMQ、RabbitMQ 在秒杀系统里怎么选?各自适合什么场景?
【标准答案】三个 MQ 都做过秒杀,关键看团队的场景和诉求。
RocketMQ(秒杀首选): 阿里出品,为电商而生。事务消息天然支持"扣库存 + 发消息"原子操作,消费者支持按 tag 过滤(秒杀订单 tag、普通订单 tag 分开消费),延迟消息支持 18 个级别(15 分钟未支付自动取消)。缺点是社区不如 Kafka 活跃,非 Java 生态支持弱。
Kafka(大数据场景): 吞吐王,单机百万 QPS。顺序写磁盘 + 零拷贝,秒杀日志、用户行为埋点、实时流计算的一把好手。缺点是消息可靠性低——默认异步刷盘可能丢消息,没有事务消息支持,适合"丢几条无所谓"的日志场景。做秒杀订单消息需要对可靠性做额外保障。
RabbitMQ(中小团队): 部署简单、管理界面友好,Erlang 实现稳定性好。死信队列做延迟消息,ack 机制灵活。缺点是吞吐明显弱于 Kafka 和 RocketMQ,集群模式性能有天花板,秒杀高峰期 5 万 QPS 就比较吃力了。
结论: 订单消息 → RocketMQ(事务消息兜底),日志/埋点 → Kafka(高吞吐),小团队快速上线 → RabbitMQ。三个可以一起用,各司其职。
【面试题】3. Sentinel 熔断降级在秒杀里怎么配?跟 Hystrix 比有什么优势?
【标准答案】熔断降级的核心——当依赖服务出问题,快速失败比慢慢等更好。
Sentinel 实战配置: 秒杀链路里至少有 5 个需要熔断的点:Redis 库存服务、MQ 发送、订单服务、风控服务、优惠券服务。每个都配两个规则:①慢调用比例熔断——RT 超过 500ms 的请求比例 > 50% 时触发熔断,10 秒后半开试探;②异常比例熔断——异常率 > 20% 触发。非核心服务(优惠券、积分)熔断后直接返回降级结果,不影响下单主链路。
Sentinel 比 Hystrix 好在哪? 第一,Sentinel 支持控制台实时调整规则,不需要重启应用,Hystrix 改配置要重新部署。第二,Sentinel 的滑动窗口更精细,支持秒级数据,Hystrix 是固定窗口(10 秒)。第三,Sentinel 支持热点参数限流——同一个秒杀商品 ID 的请求单独限流,Hystrix 没有这能力。第四,Hystrix 已停维护,Sentinel 还在活跃迭代。
关键设计: 降级逻辑要提前写好,不能熔断了返回 null。比如优惠券熔断,返回"优惠券系统繁忙,订单仍可正常提交",而不是直接报错。秒杀场景下,非核心功能熔断的底线是——不影响用户下单。
【面试题】4. 秒杀恶意请求怎么防?黄牛脚本、Bot、撞库存你怎么处理?
【标准答案】秒杀风控不是要不要做的问题,是做几层的问题。
第一层:验证码 + 答题。 滑块验证码在前端拦住 90% 的脚本。秒杀开始前弹出"拖动滑块到缺口",脚本过不去。更高阶的加答题——"请问 15+27=?",人类 0.5 秒答出来,脚本要 OCR 识别加计算,提升攻击成本。
第二层:频控 + 行为分析。 同一个 IP 每秒超过 10 次请求直接丢进黑名单(Nginx 层处理最快)。同一个设备 ID 一秒内发 5 次下单请求,直接标记可疑,后续请求走图形验证码。正常人类不可能一秒点 5 次按钮。
第三层:IP 画像 + 代理识别。 接入 IP 风险库,机房 IP、代理 IP、Tor 节点直接拒绝。正常人不会从数据中心 IP 来抢手机。
第四层:设备指纹 + 人机识别。 收集浏览器指纹(Canvas、WebGL、字体列表),同一设备注册了 100 个账号的全是黄牛。下单时校验设备指纹和账号的关联密度,密度超阈值触发二次验证。
第五层:收货地址聚合分析。 事后分析:同一个收货地址下了 30 单、同一个手机号绑了 50 个账号,批量标记恶意订单并回滚库存。
一句话: 前端拦脚本(验证码),网关拦频率(IP 限流),后端拦异常(行为分析),事后拦批量(地址聚合)。四层拦截,黄牛的 ROI 打下来自然不来了。
【面试题】5. 秒杀订单表数据爆炸,分库分表怎么落地?分片键怎么选?
【标准答案】秒杀一天产生的订单可能比平时一个月还多,单表 5000 万行查询就崩。
分片键选择是第一要务。 秒杀订单表分片键选 user_id,查询用户订单时单分片就能查。如果选 order_id,查用户订单要扫全部分片(读扩散)。核心原则:让 80% 的查询落在单分片上。
ShardingSphere 落地三步: ①配数据源——spring.shardingsphere.datasource.names=ds0,ds1,...,ds7,8 个库。②配分片规则——t_order 按 user_id % 8 分库,每个库 1 张表够用。③配广播表——商品表、配置表每个库都放一份完整副本,Join 查询不出库。
分片后的坑: ①跨分片查询变慢——统计"今天总订单数"要扫 8 个库,解决方案是用 ES 做聚合统计。②分片键不能改——user_id 选了就不能改,改了数据找不着。③分布式 ID 生成——雪花算法生成全局唯一 order_id,不依赖于自增 ID。④扩容难——从 8 库扩到 16 库要停服迁移,因此初期预估 2-3 年订单量,一次分够。
一句话: user_id 分库 + 雪花 ID + ES 聚合,分库分表的三板斧。
【面试题】6. Redis 哨兵和 Cluster 有什么区别?秒杀场景用哪个?
【标准答案】两者解决的问题不同,说清楚"不是升级关系"就加一半分了。
哨兵模式: 本质是主从 + 自动故障转移。一主多从,哨兵节点监控主节点健康,主挂掉自动选一个从提升为新主。数据全量存储在每个节点,瓶颈在单机内存和写 QPS(只有主能写)。适合数据量不大(< 几十 GB)、写 QPS < 5 万的场景。
Cluster 模式: 数据分片存储,16384 个 Slot 分布在不同主节点上。每个主节点只存一部分数据,支持水平扩展。写了数据自动按 key 的 CRC16 路由到对应 Slot。适合数据量大(TB 级)、需要水平扩展的场景。缺点是多 Key 操作受限——mset、事务、Lua 脚本操作的 Key 必须在同一个 Slot,需要 HashTag 强制路由。
秒杀用哪个? 秒杀库存数据很小(几个商品),单 Redis 扛 5 万 QPS 够用,哨兵足矣。但如果秒杀只是系统的一部分,还有用户信息、购买记录、统计缓存等 TB 级数据,整体架构应该用 Cluster。秒杀的热点库存用 HashTag 固定到一个 Slot 或单独一套高配哨兵 Redis。
一句话: 库存扣减用哨兵(快、简单),全站缓存用 Cluster(能扩容)。不冲突,可以一起用。
【面试题】7. 秒杀数据一致性怎么保证?缓存、DB、消息三者怎么对齐?
【标准答案】秒杀有三个真相源——Redis 库存、MQ 消息、DB 订单,三者不一致就是事故。
写入链路: 扣库存(Redis Lua)→ 发事务消息(RocketMQ)→ 消费创建订单(DB)。事务消息保证"扣成功 → 消息一定发出",这是第一步对齐。
异常路径: 消息发成功但 DB 写失败(比如网络闪断),消费者重试。重试还是失败 → 写入死信队列 → 人工处理。同时,Redis 执行回滚(incrby 加回库存),记录回滚日志。这样就保证:订单没入库的库存一定回了。
对账是兜底: 写链路再完美也可能出问题——Redis 回滚时刚好宕机、MQ 消息在 Broker 里丢了。所以每 5 分钟跑一次对账:Redis扣减总数 = DB已创建订单数 + 回滚记录数。不等就告警,人工查差异。
三种不一致的优先级: ①多卖(Redis 扣了但 DB 订单没跟上)→ 最严重,必须回滚 Redis 库存;②少卖(DB 有订单但 Redis 多扣了)→ 用户体验差但经济上损失小,人工补库存;③消息丢失 → 查 MQ 的 trace 日志,重发丢失的消息。
一句话: 事务消息保主链路,死信队列兜异常,对账脚本查差异。实时校验 + 离线对账,双轨对齐。
【面试题】8. 秒杀监控大盘要展示什么?实时 QPS、库存、成功率这三块你怎么设计?
【标准答案】秒杀监控大盘不是用来炫技的,是用来 30 秒判断"系统是不是还在正常工作"。
实时 QPS 面板: ①总 QPS 曲线——30 秒粒度,展示全链路每秒请求数,水位线标红色(45% 容量告警,70% 紧急扩容);②分层 QPS——Nginx → 应用 → Redis → DB 每层的 QPS 单独画,一眼看出瓶颈在哪一层;③接口级 QPS——下单、扣库存、查商品各接口的 QPS 分开,突然某接口异常一眼定位。
库存面板: ①实时剩余库存数字(大字号),每 1 秒刷新;②库存消耗速率曲线——斜率变平了说明流量下来了,斜率突然陡峭说明有人在刷;③分仓库库存——北京仓还剩多少、上海仓还剩多少。
成功率面板: ①全链路成功率(订单创建成功数 / 下单请求数);②各环节成功率——资格校验通过率、扣库存成功率、订单创建成功率,链路上哪个环节成功率掉下来就是在哪卡了;③P99 RT 曲线——RT 飙升同时成功率下降 = 系统过载,立刻限流收紧。
告警规则: 成功率 < 95% → P2 告警群通知;成功率 < 90% → P1 电话;库存差异 > 5 → P0 立即排查。告警不是越密越好,3 条核心规则比 30 条没人看的规则有用一百倍。
【面试题】9. 缓存和数据库双写一致性怎么保证?Cache Aside 在秒杀里怎么用?
【标准答案】双写一致性四个字总结:先写库,再删缓存。
Cache Aside 标准模式: 读的时候先查缓存,命中返回;miss 查 DB,写入缓存后返回。写的时候先更新数据库,然后删除缓存(不是更新缓存)。为什么删而不是更新?因为并发写的时候,A 更新 DB 后 B 先更新缓存,A 再覆盖缓存——数据就反了。删缓存没有这个问题,下次读自然重建。
秒杀场景的变通: 标准 Cache Aside 在秒杀里有个坑——热点商品刚被删缓存,瞬间几千个请求同时 miss 全冲 DB。解决方案:①热点数据不删缓存,异步刷新。写数据库后发一条 MQ 消息,消费端读最新 DB 数据再写回 Redis,延迟最多 100ms。②双删策略——写前先删一次(清旧数据),写后再删一次(防并发写脏数据),两次删中间隔 500ms。
库存这个特殊场景: 库存不走 Cache Aside。Redis 是库存的真相源,DB 是异步同步的副本——和常规缓存方向反了。库存一致性靠 Redis Lua 原子扣 + 对账保证,不用 Cache Aside。
一句话: 常规数据 Cache Aside(先写库再删缓存),热点数据异步刷新(MQ更新缓存),库存直接以 Redis 为准(不走这个模式)。
【面试题】10. 面试常考:哨兵 vs Cluster、RDB vs AOF、Pipeline vs 批量命令,你怎么一句话讲清区别?
【标准答案】这三组对比是 Redis 面试的固定节目,一句话定乾坤。
哨兵 vs Cluster: 哨兵解决的是"主挂了谁来接班"(高可用),Cluster 解决的是"数据太多一台机器放不下"(可扩展)。哨兵是主从的增强版,Cluster 是分片的增强版。秒杀库存小用哨兵,全站缓存大用 Cluster,不互斥可并存。
RDB vs AOF: RDB 是快照——每隔 N 分钟把全量内存数据写到磁盘 dump.rdb,恢复快但丢最近 N 分钟的数据。AOF 是日志——每条写命令追加到文件,数据安全但恢复慢。秒杀库存数据量小,用 AOF everysec 模式,最多丢 1 秒数据够用。RDB 适合做定期全量备份,AOF 做增量持久化。
Pipeline vs 批量命令(mset/mget): Pipeline 是把多条命令打包一次发过去,服务端逐条执行逐条返回结果,减少 RTT 但不原子。mset/mget 是一条命令操作多个 key,单命令原子执行。秒杀扣库存必须用 Lua(原子),不能用 Pipeline(非原子)。Pipeline 适合批量查询——一次查 100 个商品信息,快 100 倍但不需要原子性。
加分话术: 面试官问"你用过哪些 Redis 高级特性",这三组对比挨个讲一遍,展示你的 Redis 认知是立体的,不是只背了 SET/GET。