Skip to content

【面试题】1. 秒杀系统分库分表怎么做?分片键选什么?以后数据量涨了怎么扩容不丢数据? ​

【标准答案】分库分表不是"表大了就拆",是要提前想清楚:按什么拆、拆多少、以后怎么扩。

分片键选择(最重要): 秒杀订单表选 user_id 做分片键。原因:秒杀最频繁的查询是"我的订单",走 user_id 哈希直接命中单库单表,不会跨库扫。如果用 order_id 做分片键,查"我的订单"就要全库扫——每查一次跨 8 个库,RT 翻 8 倍。用 item_id 做分片键可以快速查"这个商品被谁抢到了",但用户查订单又要跨库。所以分片键选谁取决于最高频查询是谁——秒杀里就是"我的订单",选 user_id。

分多少: 不是越大越好。预估未来 3 年订单量——假设日均 10 万单 × 3 年 ≈ 1 亿单,单表 1000 万数据性能 OK,那就分 16 库 × 1 表 = 16 表,每表约 625 万,余量充足。注意别 16 库 × 16 表(256 张表)过度设计,表太多连接池不够用。

扩容方案: 原始 8 库扩到 16 库,哈希规则从 user_id % 8 变成 user_id % 16,存量数据怎么办?停服迁移不现实。推荐方案——双写 + 渐进迁移: ①新数据同时写老库(user_id%8)和新库(user_id%16);②后台作业分批扫老数据,按新规则搬到新库;③搬完后切读流量到新库,下线老库。全程不停服。ShardingSphere 的 Scaling 功能可以自动化这个流程——配置目标分片数,它帮你做数据迁移和一致性校验。

一句话: 分片键选最高频查询的维度(user_id),余量算 3 年,扩容用双写+渐进迁移,别停服别丢数据。


【面试题】2. 熔断和降级到底什么区别?Sentinel、Hystrix、Resilience4j 在秒杀里怎么选? ​

【标准答案】熔断是"被动保护"——下游挂了,我自动断开不调了。降级是"主动放弃"——我提前准备好 B 计划,出问题就执行。

熔断(Breaker): Sentinel 统计调用下游的慢调用比例或异常比例。比如 1 分钟内 50% 的请求 RT 超过 500ms → 断路器打开 → 接下来 10 秒不调下游,直接走 fallback。10 秒后半开状态,放一个请求试探——请求成功就闭合恢复,失败就继续熔断。核心:断路器保护的是"你自己不被下游拖死"。

降级(Fallback): 熔断触发后走的兜底逻辑。比如扣库存挂了 → 返回"活动火爆,请稍后再试"静态文案;查用户等级挂了 → 返回默认等级"普通用户"。降级追求的是"有损服务"——宁可功能不全,不能全站 500。

三者选型: Sentinel(阿里系)秒杀场景首选——规则从控制台热更新、支持 QPS/线程数/RT 多种熔断策略、和 Spring Cloud Gateway/Nacos 原生打通、Dashboard 可视化。Hystrix 已停维护,线程池隔离思路太重(每个接口一个线程池,100 个接口 100 个线程池,内存爆炸)。Resilience4j 轻量函数式,适合不用 Spring Cloud 的项目,但控制台和动态规则不如 Sentinel。

秒杀里的实战配置: ①下单接口熔断:慢调用比例 > 30% 且 RT > 1s 触发,熔断 30 秒。②扣库存不配熔断——库存接口挂了本身就是故障,熔断它没意义(不如直接限流)。③降级优先级:优惠券 > 积分 > 短信 > 推荐,按顺序降。

一句话: 熔断自保、降级兜底。有阿里的用 Sentinel(秒杀首选),Spring 非阿里用 Resilience4j,Hystrix 已是过去式。


【面试题】3. 超卖到底怎么根治?从 Redis Lua 原子扣到 DB 乐观锁的完整链路怎么保证一个都不多卖? ​

【标准答案】超卖的根因:库存读和写之间有并发间隙。解决思路:让"读+判断+写"变成一个不可分割的原子操作。

第一道——Redis Lua 原子扣(主防线): 不走"先 GET 再 SET"的两步操作,用 Lua 脚本一次完成:local stock = redis.call('get', KEYS[1]); if tonumber(stock) < tonumber(ARGV[1]) then return -1 end; redis.call('decrby', KEYS[1], ARGV[1]); return stock - ARGV[1]。Redis 单线程执行 Lua,天然串行,一台 Redis 单机 5-8 万 QPS 零超卖。这是第一道防线,扛住 99% 的并发。

第二道——DB 乐观锁兜底(防 Redis 丢数据): Redis 扣成功 → 订单消息进 MQ → 消费端用乐观锁更新 DB:UPDATE stock SET count = count - ?, version = version + 1 WHERE id = ? AND version = ? AND count >= ?。这里 AND count >= ? 也是关键——即使 Redis 出错多发了扣减消息,DB 里库存不够了 affected_rows=0,不会超卖。

万一 Redis 主从切换丢了几条库存扣减记录怎么办? 主节点挂了还没来得及同步给从节点,从节点提升为新的主节点,库存数回退到扣减前。后面的用户继续扣,导致多卖。解决方案:①Redis 库存不是"数字计数"而是"位图计数"(每扣一个 SETBIT 占一个位),即使主从切换,SETBIT 的位不会丢(已经持久化),总数 BITCOUNT 算出来就准。②更简单的:Redis 只要发生了主从切换,秒杀系统立刻触发全量对账——查 DB 已生成订单数,和 Redis 剩余库存相加,不等于总库存就触发人工介入。

第三道——成品订单数兜底: 终极防线:SELECT COUNT(*) FROM orders WHERE item_id = ? AND status != 'CANCELLED',如果订单数已经等于总库存数,全链路停售拒绝新单。这道防线不在乎 Redis 和 DB 谁对谁错,只看一个事实:订单已经够了,不准再卖。

一句话: Redis Lua 原子化扣减(防并发超卖)→ DB 乐观锁兜底(防 Redis 数据异常)→ 成品订单数上限(终极兜底)。三道防线不是冗余,对应三种不同的故障模式。


【面试题】4. 秒杀活动怎么防刷?验证码、答题、黑名单、设备指纹各自解决什么问题? ​

【标准答案】防刷体系的思路分层:机器人挡在最外面、羊毛党挡在中间层、职业黄牛挡在核心链路。

第一层——图片验证码(挡脚本机器人): 秒杀开始前弹出滑块/图形验证码,拦住 90% 的自动脚本。验证码的意义不是"让人很难通过",是"让脚本比人难 100 倍"。滑块验证对真人 0.5 秒完成,对脚本需要训练 AI 模型 + 代理 IP,成本飙升。

第二层——答题验证(挡羊毛党+黄牛): 秒杀开始时出现一道随机数学题/常识题("37+25=?""秒杀界面的背景色是什么?"),答对后才允许提交订单。答题的目的不是考智商,是让批量脚本增加 OCR 和语义理解的难度——一道随机题 + 5 秒窗口,脚本要 OCR → 语义理解 → 计算 → 提交,延迟直接炸掉。相比之下真人 2 秒搞定。

第三层——IP 限流 + 黑名单(挡批量刷): 同一个 IP 1 秒内请求超过 5 次,直接加入黑名单禁 10 分钟。黑名单分两级:①临时黑名单——Redis SET blacklist:ip:${ip} 1 EX 600,10 分钟后自动解禁。②永久黑名单——数据库记录,人工审核加入,秒杀活动不再放行。注意 IP 限流不能用全局限流(NAT 后多个用户同 IP),搭配设备指纹可区分。

第四层——设备指纹(挡多账号): 前端采集浏览器 Canvas 指纹 + WebGL + 字体列表生成设备唯一 ID,后端 Redis 存 deviceId → userId 列表。同一设备 3 个账号下同一商品 → 标记为可疑设备,强制走滑块验证 + 答题。换 IP 没用,换设备成本高——黄牛不可能买 1000 部手机来刷。

第五层——行为分析(进阶): 用户从进入页面到点击下单的耗时,正常 3-30 秒,脚本 50ms。毫秒级下单直接拒绝。这个靠 Nginx 日志 + Flink 实时计算实现,大厂方案,小团队放最后做。

一句话: 图片验证码挡机器人、答题验证挡脚本军团、IP 限流挡批量、设备指纹挡多账号、行为分析挡高级黄牛。每层筛掉一批,五层之后只剩真人。


【面试题】5. 秒杀场景 Redis 高可用怎么选?哨兵、Cluster、Redisson 各自解决什么问题? ​

【标准答案】三个方案解决的问题不同:哨兵解决"挂了能自动切",Cluster 解决"数据太多单机放不下",Redisson 解决"分布式协调"。

哨兵(Sentinel)——解决高可用: 一主两从 + 三个哨兵节点。主节点挂了,哨兵自动投票选出新主节点,30 秒完成故障转移。秒杀库存 Key 放主节点,从节点分担读(比如售罄标记查询)。哨兵模式 500MB-10GB 数据够用,秒杀库存数据几百 MB,哨兵最匹配。注意:哨兵模式是主从架构,不是分布式——所有数据在每个节点都有一份,不适合大数据量。

Cluster——解决数据分片: 16384 个 Slot 分散在多个主节点,每个主节点只存一部分数据。适合全站 Redis 数据(缓存+session+排行榜)超 10GB 的场景。秒杀库存数据量小不需要 Cluster,但全站缓存用 Cluster 避免单机内存瓶颈。配置要求:至少 3 主 3 从,一个主节点挂了对应的从节点自动顶上。

Redisson——解决分布式锁/集合/队列: 不是独立部署方案,是基于 Redis 的 Java 客户端框架。提供 RedLock 分布式锁、RMap、RQueue、RSemaphore 等高级数据结构。秒杀里用它做库存预热锁、定时对账锁、回滚互斥锁。

秒杀系统实际组合: 秒杀专用 Redis(独立部署)→ 哨兵模式(高可用,数据量小)。全站缓存 Redis → Cluster 模式(海量数据,分片存储)。应用层 → Redisson 客户端(分布式锁 + 高级数据结构)。三者不互斥,各有各的坑位。

为什么秒杀不用 Cluster: 集群模式下的 Lua 脚本要求所有 Key 在同一个 Slot(用 HashTag 强制),跨 Slot 操作会报 CROSSSLOT 错误。而且 Cluster 节点间通信有开销,故障检测比哨兵慢。秒杀库存数据就几百 MB,哨兵够用,没必要引入 Cluster 的复杂度。

一句话: 哨兵管高可用(秒杀专用 Redis)、Cluster 管分片(全站缓存)、Redisson 管分布式协调。量小用哨兵,量大用 Cluster,协调用 Redisson。


【面试题】6. 秒杀里缓存、数据库、消息三者数据怎么保证一致?不一致了怎么发现和修复? ​

【标准答案】秒杀场景下绝对强一致不现实,目标是有序最终一致 + 能发现不一致 + 能自动修复。

核心链路的一致性保障(正向流程): ①Redis 扣库存成功(库存纬度的真理源)→ ②发 RocketMQ 半消息 → ③提交半消息让消费者可见 → ④消费者用乐观锁更新 DB 库存 → ⑤如果乐观锁失败(version 冲突),消息重试 3 次 → ⑥3 次都失败进死信队列,人工介入。这里 Redis 是"事实上的库存",DB 异步跟随,两者有秒级延迟,但最终一定会对齐(乐观锁 + 重试保证)。

怎么发现不一致?(对账机制): 两种对账:①实时对账——每 5 分钟执行 Redis库存 + DB未支付订单数 ≟ 总库存,不等就告警。②全量对账——活动结束后,Redis已扣数(初始库存-剩余库存) + 回滚数 ≟ DB已生成订单数 - 取消订单数 + 回滚记录数。两个等式全部成立才说明一致性没问题。

常见不一致场景及修复: ①Redis 扣成功但 MQ 丢消息 → DB 没扣。修复:对账发现后,手动对 DB 执行 UPDATE stock SET count = count - 差值。②MQ 重复消费导致 DB 多扣 → version 乐观锁自动拦掉了,affected_rows=0,不会多扣。③订单取消回滚,Redis 加回了但 DB 没加回 → 回滚消息有重试,3 次失败走死信。④Redis 主从切换丢库存数据 → Redis 重启对账时重新计算库存 = 总库存 - DB 有效订单数。

设计原则(比具体方案更重要): ①每一刻有一个明确的真理源(扣减阶段是 Redis,对账阶段是 DB 成品订单数)。②所有变更都有流水可追溯(回滚日志、订单状态变更日志)。③对账自动化、告警自动化、修复有 SOP。

一句话: Redis 是先行的真理源,DB 是最终的真理源,MQ 是传递媒介。不一致靠对账发现、靠重试自愈、靠人工兜底。秒杀的一致性不是实时强一致,是有序最终一致。


【面试题】7. 秒杀监控大盘怎么设计?实时 QPS、库存水位、下单成功率分别怎么算、怎么展示? ​

【标准答案】监控大盘不是花里胡哨的图表,是出故障时一眼能看出"哪一层挂了"。

大盘核心指标(三个就够了): ①实时 QPS——网关/应用/Redis/DB 四层各自 QPS,一列展示,哪层掉得凶一眼定位。②库存水位——剩余库存 / 总库存的百分比,低于 10% 变黄,0% 变红。③下单成功率——成功下单数 / 总提交数,低于 95% 橙色告警,低于 90% 红色告警 + 钉钉 + 电话。

技术落地方案: 应用层用 Micrometer + Prometheus 埋点,每笔请求上报一次指标到 Prometheus Pushgateway → Prometheus 每 15 秒 scrape 一次 → Grafana 展示。秒杀期间 15 秒间隔太长,加一套实时流——下单成功/失败直接写 Redis Sorted Set(score=时间戳),大盘从 Redis ZRANGEBYSCORE 查出最近 10 秒的数据算实时成功率和 QPS,延迟 1 秒以内,比 Prometheus 快 15 倍。

告警规则设计: ①成功率 < 95% 持续 30 秒 → 钉钉群告警。②成功率 < 90% → 钉钉 + 电话。③库存为 0但仍有 100+ QPS 扣库存请求 → 降级开关没生效告警(说明售罄标记没拦住流量)。④Redis 内存 > 80% 持续 1 分钟 → 告警。⑤DB 连接数 > 连接池 80% → 告警。告警不是越多越好——5 个核心告警够了,多了没人看。

监控大盘的布局设计: 第一行放三个核心数字(QPS、库存%、成功率),字号要大到一眼看到。第二行放四层 QPS 的折线图(网关/应用/Redis/DB 四条线一个图),交叉点就是瓶颈层。第三行放下单 RT 百分位(P50/P99/P999)的时序图。一屏看完,不用滚动。

一句话: 大盘三个核心数字(QPS、库存、成功率)+ 四层折线图定层 + RT 百分位查慢。指标越少越有效,告警越关键越响应。


【面试题】8. Nginx+Lua 前置校验具体能做哪些事?怎么在流量还没进 Java 进程前就拦掉 50% 无效请求? ​

【标准答案】Nginx+Lua(OpenResty)是秒杀的第一道关卡,原则:能在 Nginx 拦的绝不放进 Java 进程。

六大前置校验逻辑(按执行顺序):

①时间窗口校验: local now = ngx.time(); if now < start_time or now > end_time then ngx.exit(403) end。秒杀未开始或已结束的请求直接 403,一条都不进 Java。这是最省资源的校验。

②IP 黑名单: lua-resty-redis 直连 Redis 查 SISMEMBER blacklist:ip ${client_ip},命中返回 403。注意用 redis:set_timeout(100) 设超时 100ms——Redis 连不上时默认放行(宁可漏一个黑名单也不拦正常用户)。

③IP 频率限制: limit_req_zone $binary_remote_addr zone=seckill:20m rate=5r/s。每个 IP 每秒最多 5 个请求,超的返回 429。limit_req 是 Nginx 原生能力,不需要 Lua。

④用户登录校验(JWT Token): 从 Cookie 或 Header 取 Token,用 lua-resty-jwt 验签,解析出 userId。没 Token 或过期直接返回 401。这一步进 Java 就是一次 RPC(调用户中心),Nginx 里验签 0.1ms 省掉。

⑤用户资格初筛(布隆过滤器): 秒杀要求银卡以上会员。用 lua-resty-redis 调 BF.EXISTS seckill:qualified:users ${userId},不存在返回"您不符合参与条件"。布隆过滤器判断不存在就一定不存在,无资格用户被精准拦截。

⑥并发窗口前置控制: 库存只有 1000 件,没必要放 10 万人进 Java。Nginx 层用 redis:incr("seckill:gate:counter:" .. itemId) 做原子计数器,超过库存 × 3 倍(留余量给下单失败重试),直接返回售罄。放 3000 人进应用层就够了,97000 人在 Nginx 层就拿到"已售罄"。

性能要诀: 所有 Redis 调用的超时都设 100ms,连接池预热到 200 个连接。超时宁可漏过也不阻塞 Nginx worker——Nginx 是事件驱动的,一个 worker 阻塞会影响所有请求。

一句话: Nginx+Lua 做时间校验、IP 黑名单、JWT 验签、布隆资格、计数器限流,六刀砍掉 50% 无效流量。原则:能拦的在最外层拦,拦不住才放 Java。


【面试题】9. Seata AT 模式和 Saga 模式在秒杀分布式事务里分别适合什么场景?跟 RocketMQ 事务消息比各有什么优劣? ​

【标准答案】Seata 是解决跨服务分布式事务的框架,秒杀里用得不多但有特定场景。

AT 模式(自动补偿): 基于数据库代理,一阶段自动提交(undo_log 记录回滚数据),二阶段成功删 undo_log,失败根据 undo_log 生成反向 SQL 回滚。不需要写补偿代码,对业务零侵入。适合场景:下单同时扣优惠券、扣积分——三个微服务各管各的数据库,下单成功优惠券和积分也得扣成功。

但 AT 模式在秒杀里的坑: ①锁持有时间长——一阶段提交到二阶段决议之间,被更新的行有全局锁,秒杀高并发下锁冲突爆炸。②依赖数据库 undo_log 表——秒杀的订单表已经扛很大压力了,再加全局事务的 undo_log 写操作,火上浇油。结论:秒杀主链路不用 AT 模式。

Saga 模式(长事务补偿): 每个参与者实现正向(扣库存)+ 反向(回滚库存)两个接口。协调器按顺序调正向,哪个失败就逆序调反向补偿。适合长流程——比如秒杀下单 + 支付 + 发货,三个动作跨几个小时。Saga 不锁资源,靠补偿保证最终一致。缺点是补偿逻辑自己写,复杂。

和 RocketMQ 事务消息的对比: 如果只是"扣库存 + 发消息"的原子绑定,RocketMQ 事务消息是最优解——无锁、异步、一对一的原子性。Seata AT/Saga 解决的是"多个数据库事务之间的一致性",RocketMQ 事务消息解决的是"一个本地事务 + 一条消息的一致性"。秒杀主链路是后者——库存扣减了要发创建订单的消息,一个本地事务 + 一条消息,RocketMQ 够用。

实际选型结论: 秒杀核心链路(扣库存→发消息→消费建单)→ RocketMQ 事务消息。跨服务的实时强一致(扣库存+扣优惠券+扣积分)→ Seata AT(接受性能损耗)。长流程异步(下单→支付→履约)→ Saga。大部分秒杀场景 RocketMQ 事务消息 + 本地消息表就搞定了,Seata 是重型武器。

一句话: RocketMQ 事务消息解"发消息 + 做操作"的原子性,Seata AT 解"跨库事务"的原子性(秒杀里代价太大),Saga 解"长流程"的最终一致。秒杀主链路不用 Seata,太重。


【面试题】10. 面试常考对比:哨兵和 Cluster 在秒杀场景下到底怎么选?能不能混用? ​

【标准答案】面试官问这个,不是听你背定义,是看你有没有"量体裁衣"的判断——根据数据量和可用性要求选架构。

哨兵(Sentinel)特点: 主从架构,所有节点存全量数据。一主多从,主写从读,主挂了哨兵自动选新主。优点:部署简单(3-5 节点搞定)、Lua 脚本无限制、故障转移 30 秒完成。缺点:数据量受单机内存限制,写入只能走主节点——单机写瓶颈。秒杀库存数据几百 MB,哨兵完美匹配。

Cluster 特点: 分布式架构,数据按 Slot 分片,每个节点只存一部分。多主多从,读写分散到各主节点。优点:数据量无上限(加节点就扩容)、写入分散到多主(总写吞吐是各节点之和)。缺点:部署复杂(至少 6 节点)、Lua 脚本跨 Slot 报错(必须用 HashTag 约束到同一 Slot)、节点间通信有开销、故障转移比哨兵慢(节点间还需 gossip 同步状态)。

秒杀场景下的选型决策树: ①全站缓存数据 < 10GB 且秒杀库存数据需 Lua 原子操作 → 哨兵。②全站缓存数据 > 10GB → Cluster。③秒杀库存数据单独一套 Redis → 哨兵(独立部署、物理隔离),全站缓存另外一套 → Cluster。这就是"混用"的正确姿势——秒杀热数据用哨兵(高性能、低延迟、Lua 自由),全站长尾数据用 Cluster(大容量、可扩容)。

能不能一套 Cluster 搞定所有? 能,但不推荐。秒杀的热点 Lua 脚本在 Cluster 里要用 HashTag 把所有 Key 绑到一个 Slot,极端情况下 HotSpot Slot 的 QPS 全打在一个节点上,Cluster 的分片优势归零——其他节点看戏,热点节点打满。物理隔离的独立哨兵实例至少保证秒杀崩了不影响全站。

故障切换对比: 哨兵选主靠多数哨兵投票(Raft-like),判定客观下线后 30 秒内切完。Cluster 靠 gossip 协议传播节点状态,一个节点认为另一个挂了 → 广播给其他节点 → 超过半数确认 → 提升从节点,整体 30-60 秒。秒杀期间 30 秒故障意味着漏掉一轮完整的秒杀。所以秒杀 Redis 必须配哨兵,用独立的硬件、独立的网卡、独立的监控。

一句话: 数据小(<10GB)+ 要 Lua 原子操作 → 哨兵。数据大(>10GB)+ 要水平扩展 → Cluster。秒杀系统最佳实践:热数据哨兵(物理隔离) + 全站 Cluster,两套共存。