后端高频面试题与解答
-
- 第一部分:题目清单(自测用)
- 第二部分:逐题解答
-
- [Q1. TCP 三次握手为什么是三次?](#Q1. TCP 三次握手为什么是三次?)
- [Q2. TIME_WAIT 作用?](#Q2. TIME_WAIT 作用?)
- [Q3. HTTP 常见状态码?](#Q3. HTTP 常见状态码?)
- [Q4. HTTPS 握手过程?](#Q4. HTTPS 握手过程?)
- [Q5. HTTP/2 相比 1.1 提升?](#Q5. HTTP/2 相比 1.1 提升?)
- [Q6. 为什么 MySQL 用 B+ 树?](#Q6. 为什么 MySQL 用 B+ 树?)
- [Q7. 聚簇与非聚簇索引区别?](#Q7. 聚簇与非聚簇索引区别?)
- [Q8. 最左前缀原则?](#Q8. 最左前缀原则?)
- [Q9. 什么情况索引失效?](#Q9. 什么情况索引失效?)
- [Q10. 事务隔离级别与幻读?](#Q10. 事务隔离级别与幻读?)
- [Q11. MVCC 原理?](#Q11. MVCC 原理?)
- [Q12. 行锁升级表锁条件?](#Q12. 行锁升级表锁条件?)
- [Q13. 间隙锁解决什么问题?](#Q13. 间隙锁解决什么问题?)
- [Q14. 如何定位慢 SQL?](#Q14. 如何定位慢 SQL?)
- [Q15. Redis 五种结构及场景?](#Q15. Redis 五种结构及场景?)
- [Q16. RDB 与 AOF 区别?](#Q16. RDB 与 AOF 区别?)
- [Q17. 缓存穿透/击穿/雪崩及方案?](#Q17. 缓存穿透/击穿/雪崩及方案?)
- [Q18. Redis 集群 slot 数?](#Q18. Redis 集群 slot 数?)
- [Q19. IOC 与 DI 区别?](#Q19. IOC 与 DI 区别?)
- [Q20. AOP 实现原理(JDK/CGLIB)?](#Q20. AOP 实现原理(JDK/CGLIB)?)
- [Q21. Bean 生命周期?](#Q21. Bean 生命周期?)
- [Q22. Spring Boot 自动配置原理?](#Q22. Spring Boot 自动配置原理?)
- [Q23. @Transactional 失效场景?](#Q23. @Transactional 失效场景?)
- [Q24. MQ 如何保证不丢消息?](#Q24. MQ 如何保证不丢消息?)
- [Q25. 如何保证消费幂等?](#Q25. 如何保证消费幂等?)
- [Q26. CAP 理论及取舍?](#Q26. CAP 理论及取舍?)
- [Q27. 分布式锁实现与注意事项?](#Q27. 分布式锁实现与注意事项?)
- [Q28. 分库分表怎么分?分布式 ID?](#Q28. 分库分表怎么分?分布式 ID?)
- [Q29. 分布式事务方案?](#Q29. 分布式事务方案?)
- [Q30. 秒杀系统如何设计?](#Q30. 秒杀系统如何设计?)
定位:后端开发岗(Java 技术栈)面试自测与背诵。
配套:《后端面试知识点梳理》(知识点梳理)+《Java面试知识点梳理》(语言/JVM)。
用法:第一部分是纯题目清单,先遮住答案自己答一遍;第二部分逐题对照解答。
第一部分:题目清单(自测用)
- TCP 三次握手为什么是三次?
- TIME_WAIT 作用?
- HTTP 常见状态码?
- HTTPS 握手过程?
- HTTP/2 相比 1.1 提升?
- 为什么 MySQL 用 B+ 树?
- 聚簇与非聚簇索引区别?
- 最左前缀原则?
- 什么情况索引失效?
- 事务隔离级别与幻读?
- MVCC 原理?
- 行锁升级表锁条件?
- 间隙锁解决什么问题?
- 如何定位慢 SQL?
- Redis 五种结构及场景?
- RDB 与 AOF 区别?
- 缓存穿透/击穿/雪崩及方案?
- Redis 集群 slot 数?
- IOC 与 DI 区别?
- AOP 实现原理(JDK/CGLIB)?
- Bean 生命周期?
- Spring Boot 自动配置原理?
- @Transactional 失效场景?
- MQ 如何保证不丢消息?
- 如何保证消费幂等?
- CAP 理论及取舍?
- 分布式锁实现与注意事项?
- 分库分表怎么分?分布式 ID?
- 分布式事务方案?
- 秒杀系统如何设计?
第二部分:逐题解答
Q1. TCP 三次握手为什么是三次?
核心结论:三次是确认双方收发能力都正常、且防止历史重复连接初始化的最小次数。
- 第一次 SYN:客户端证明自己能发,服务端收到证明自己能收。
- 第二次 SYN+ACK:服务端证明自己能发,客户端收到证明自己能收、且自己的 SYN 已被收到。
- 第三次 ACK:服务端据此确认客户端收发均正常,双方进入 ESTABLISHED。
- 两次不够:无法确认客户端的接收能力和服务端的发送结果;四次则冗余,合并成 SYN+ACK 即可。
- 防历史连接:若收到的是迟到的旧 SYN,客户端可在第三次握手时回 RST 中止,避免服务端白白建立无效连接。
Q2. TIME_WAIT 作用?
核心结论:主动关闭方发完最后一个 ACK 后等待 2MSL,保证连接可靠关闭并让旧报文在网络中消散。
- 保证最后的 ACK 能到达:ACK 若丢失,被动方会重发 FIN,处于 TIME_WAIT 的主动方能重发 ACK;否则对端只能异常关闭。
- 防止旧连接的延迟报文污染复用同一四元组的新连接:2MSL 足够让残留报文 TTL 耗尽。
- 出现在主动关闭方,高并发短连接服务器上会堆积大量 TIME_WAIT,占用端口与内存。
- 优化:
tcp_tw_reuse(端口复用)、改长连接、net.ipv4.ip_local_port_range调大;慎用tcp_tw_recycle与SO_LINGER。
Q3. HTTP 常见状态码?
核心结论:按首位数字分五类,面试重点在 3xx 缓存、4xx 客户端错误与 5xx 服务端错误的区分。
| 分类 | 常见码 | 含义 |
|---|---|---|
| 1xx | 101 | 协议切换(WebSocket 升级) |
| 2xx | 200 / 201 / 204 | 成功 / 已创建 / 成功但无响应体 |
| 3xx | 301 / 302 / 304 | 永久重定向 / 临时重定向 / 未修改(协商缓存命中) |
| 4xx | 400 / 401 / 403 / 404 / 405 / 429 | 参数错 / 未认证 / 无权限 / 不存在 / 方法不允许 / 限流 |
| 5xx | 500 / 502 / 503 / 504 | 服务端异常 / 网关错误 / 服务不可用(过载或停机)/ 网关超时 |
- 401 与 403:前者是"你没登录",后者是"登录了但没权限"。
- 502 与 504:都是上游问题,502 是网关拿到无效响应,504 是上游超时没响应。
Q4. HTTPS 握手过程?
核心结论:HTTPS = HTTP + TLS,先用非对称加密安全协商出会话密钥,之后全程用对称加密传输数据。
text
1. ClientHello :支持的 TLS 版本、加密套件列表、随机数 R1
2. ServerHello :选定版本与套件、随机数 R2,下发 CA 证书(含公钥)
3. 客户端校验证书(签发链 / 域名 / 有效期 / 吊销)→ 用公钥加密 Pre-Master Secret 发送
4. 双方用 R1 + R2 + Pre-Master 计算出同一把对称会话密钥
5. 互发 ChangeCipherSpec + Finished 校验,握手结束,后续对称加密通信
- 非对称加密只用于握手,是为了解决"密钥怎么安全送达";数据传输用对称加密是出于性能。
- CA 证书用于防中间人:客户端用内置根证书验证服务端身份。
- TLS 1.3 简化为 1-RTT(会话复用可 0-RTT),移除了 RSA 密钥交换、静态 DH 等不安全套件。
Q5. HTTP/2 相比 1.1 提升?
核心结论:以多路复用解决应用层队头阻塞,配合二进制分帧与头部压缩,显著提升单连接吞吐。
- 二进制分帧:报文拆成 HEADERS / DATA 帧,解析高效、无歧义。
- 多路复用:一个 TCP 连接上并发交错多个 Stream,突破 1.1 的"每连接串行 + 浏览器 6 连接"限制。
- HPACK 头部压缩:静态表 + 动态表 + 哈夫曼编码,消除重复 Header。
- 服务端推送:主动推送关联资源(实践中用得少,已被 HTTP/3 弱化)。
- 局限:TCP 层的队头阻塞仍在(丢包会阻塞所有流),彻底解决要靠 HTTP/3 的 QUIC(基于 UDP)。
Q6. 为什么 MySQL 用 B+ 树?
核心结论:B+ 树矮胖、非叶节点只存键、叶子是有序链表,磁盘 IO 次数少且天然适合范围查询。
- 对比二叉搜索树:树高过大,一次比较一次 IO,且可能退化成链表。
- 对比 B 树:B 树非叶节点也存数据,单页能容纳的键更少、树更高;B+ 树非叶只存键,扇出大,千万级数据仅 3~4 层。
- 对比 Hash:等值查询 O(1),但不支持范围查询与排序。
- 叶子通过双向链表相连:范围扫描顺着链表走即可,无需反复回溯树。
- 补充:InnoDB 一页默认 16KB,是按页整体读写的,这正是为 B+ 树的节点设计的。
Q7. 聚簇与非聚簇索引区别?
核心结论:聚簇索引的叶子节点存整行数据,非聚簇(二级)索引的叶子存主键值,因此二级索引查询需要回表。
| 对比项 | 聚簇索引 | 二级索引(非聚簇) |
|---|---|---|
| 叶子内容 | 整行数据 | 索引列 + 主键值 |
| 数量 | 一张表只有一个 | 可有多个 |
| 查询代价 | 直接命中数据 | 需按主键回表再查一次 |
| 典型 | InnoDB 主键索引 | MyISAM 全部索引(叶子存地址) |
- InnoDB 中主键即聚簇键;无显式主键则选第一个唯一非空索引,都没有则生成 6 字节隐式
row_id。 - 回表:二级索引定位到主键后,再回聚簇索引取整行,多一次 B+ 树查找。
- 覆盖索引 :查询字段全在二级索引中,无需回表,
EXPLAIN的Extra显示Using index。 - 实践提示:主键尽量短且自增,二级索引叶子都冗余主键,太长会放大索引体积。
Q8. 最左前缀原则?
核心结论 :联合索引 (a,b,c) 需按从左到右连续匹配,跳过最左列或遇到范围查询,其后的列就不再用于索引定位。
- 完全生效:
a=1、a=1 and b=2、a=1 and b=2 and c=3。 - 部分生效:
a=1 and c=3(只用 a);a>1 and b=2(b 仅作回表后的过滤,即索引下推)。 - 失效:
where b=2(跳过 a,通常全表扫描)。 - 条件书写顺序无关,优化器会自动重排,关键是
where里有没有最左列。 - 建索引建议:区分度高的列靠左,同时兼顾排序/分组(
order by/group by也遵循最左前缀)。
Q9. 什么情况索引失效?
核心结论:一切破坏索引有序性或确定性的写法,都会让优化器放弃走索引而退化为全表扫描。
- 对索引列做函数、运算或隐式类型转换:
where date(create_time)='2026-01-01'、where id+1=5、字符串列传了数字。 - 前导模糊查询:
like '%abc'(like 'abc%'仍可用)。 - 违反最左前缀;
or连接了非索引列(可用union all改写)。 !=、<>、not in、is not null等,选择率过低时优化器直接选全表。- 优化器代价判断:命中行数占比很大时,回表代价高于全表扫,也会弃用索引;一切以
EXPLAIN为准。
Q10. 事务隔离级别与幻读?
核心结论:四种隔离级别逐级解决脏读、不可重复读、幻读;MySQL InnoDB 默认可重复读,靠 MVCC + Next-Key Lock 基本消灭幻读。
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交 | 有 | 有 | 有 |
| 读已提交 RC | 无 | 有 | 有 |
| 可重复读 RR(MySQL 默认) | 无 | 无 | 基本无 |
| 串行化 | 无 | 无 | 无 |
- 脏读:读到别人未提交的数据。不可重复读:同一行两次读值不同(被
update)。幻读:同一条件两次读行数 不同(被insert)。 - RR 下快照读靠 MVCC 保证一致性;当前读靠 Next-Key Lock(记录锁 + 间隙锁)禁止区间插入。
- 级别越高并发越低,实际业务多在 RC(互联网高并发,锁粒度小)与 RR(默认)之间选择。
Q11. MVCC 原理?
核心结论:MVCC 靠 undo log 版本链 + ReadView 实现"不加锁的一致性读",让读写互不阻塞。
- 每行有两个隐藏列:
DB_TRX_ID(最后修改它的事务 ID)、DB_ROLL_PTR(指向 undo log 的回滚指针)。 - 每次更新把旧值写入 undo log,通过回滚指针串成一条版本链。
- ReadView 记录生成瞬间的活跃事务列表(
m_ids)、最小/最大事务 ID;读取时沿版本链找第一个对该 ReadView 可见的版本。 - RC :每条语句生成新的 ReadView,故能读到别的事务刚提交的数据。RR:事务首个快照读生成一次 ReadView 并全程复用,故可重复读。
- 快照读(普通
select)走 MVCC;当前读(select ... for update、update、delete)走锁。
Q12. 行锁升级表锁条件?
核心结论:InnoDB 行锁是基于索引实现的,一旦 SQL 用不上索引,就只能锁住扫描过的全部记录,效果等同锁表。
- 无可用索引:
update t set name='x' where phone='138...'而phone无索引 → 全表扫描并逐行加锁。 - 索引失效:对索引列做函数运算、隐式类型转换(字符串列传数字)导致走不了索引。
- 优化器主动放弃索引:命中比例过高时改走全表扫描,连带锁全表。
- 注意区分:RR 下有索引的范围更新会加 Next-Key 间隙锁,锁住大范围区间,观感上像表锁,但本质仍是行锁 + 间隙锁。
- 排查:
show engine innodb status查锁与等待事务;用EXPLAIN确认走索引、避免大事务长时间持锁。
Q13. 间隙锁解决什么问题?
核心结论:间隙锁锁住两条记录之间的"空隙",禁止在区间内插入新记录,专门用来解决幻读。
- 组成:Next-Key Lock = 记录锁(Record Lock)+ 间隙锁(Gap Lock),区间为左开右闭。
- 只在 RR 及以上生效,RC 隔离级别没有间隙锁,这也是 RC 下仍可能出现幻读的原因。
- 触发条件:当前读 (
select ... for update、update、delete)且走索引的范围条件;唯一索引的等值命中会退化为纯行锁。 - 代价:降低并发、容易死锁(两个事务互相持有对方需要的间隙),因此应尽量缩小加锁范围、用等值条件替代范围条件。
Q14. 如何定位慢 SQL?
核心结论 :慢日志发现 → EXPLAIN 定位 → 按索引、SQL、数据量、架构四层依次优化。
- 发现:开启慢查询日志
slow_query_log=ON、long_query_time=1,用mysqldumpslow/pt-query-digest聚合出 TOP SQL。 - 分析:
EXPLAIN重点看type(ALL最差,目标ref/range)、key(实际用到的索引)、rows(预估扫描行数)、Extra(出现Using filesort、Using temporary需警惕)。 - 优化:补/改索引、避免
SELECT *、拆分大事务、深分页LIMIT 100000,10改为子查询或游标分页。 - 仍不达标再考虑读写分离、分库分表、缓存前置。
Q15. Redis 五种结构及场景?
核心结论:String / Hash / List / Set / ZSet,选型的本质是看"要按什么维度读写、要不要排序去重"。
| 结构 | 典型场景 | 常用命令 |
|---|---|---|
| String | 缓存 JSON、计数器、分布式锁、限流 | SET / INCR / SET NX EX |
| Hash | 对象属性(购物车、用户资料),可单字段读写 | HSET / HGETALL |
| List | 队列、最新列表、简单消息流 | LPUSH / RPOP / LRANGE |
| Set | 去重、标签、交并集(共同好友) | SADD / SINTER |
| ZSet | 排行榜、延迟队列(score 存执行时间) | ZADD / ZRANGEBYSCORE |
- 底层编码:String → SDS;Hash → ziplist / hashtable;List → quicklist;Set → intset / hashtable;ZSet → ziplist / skiplist。
- 大 key 风险:单个集合元素过多会阻塞主线程,需拆分或用
ZSCAN渐进遍历。
Q16. RDB 与 AOF 区别?
核心结论:RDB 是定时内存快照,体积小恢复快但会丢数据;AOF 是命令日志,更安全但文件大,生产通常两者并用。
| 对比项 | RDB | AOF |
|---|---|---|
| 内容 | 某时刻的二进制快照 | 逐条写命令日志 |
| 文件大小 | 小 | 大(需 BGREWRITEAOF 重写瘦身) |
| 恢复速度 | 快 | 慢 |
| 数据安全性 | 丢最后一次快照之后的写入 | 取决于 appendfsync,everysec 最多丢 1 秒 |
| 性能影响 | fork 子进程,瞬间抖动 |
写盘开销持续 |
appendfsync三档:always(最安全最慢)/everysec(默认,折中)/no(由操作系统决定)。- Redis 4.0+ 支持混合持久化:AOF 重写时把 RDB 内容写入 AOF 文件头部,兼顾恢复速度与安全。
- 实践:主库开启持久化、从库做冷备;不要用 AOF 文件手工改数据来"修"线上问题。
Q17. 缓存穿透/击穿/雪崩及方案?
核心结论:三者都是"请求绕过了缓存打到 DB",区别在于触发范围:穿透是不存在的 key,击穿是单个热点 key,雪崩是大批 key。
| 问题 | 原因 | 方案 |
|---|---|---|
| 穿透 | 查询根本不存在的 key,缓存永不命中 | 空值缓存(短 TTL)+ 布隆过滤器 + 参数校验 |
| 击穿 | 热点 key 过期瞬间高并发同时重建 | 互斥锁(只放一个线程回源)/ 逻辑过期(后台异步刷新) |
| 雪崩 | 大量 key 同时过期或 Redis 整体宕机 | TTL 加随机值 + 多级缓存 + 限流降级 + 哨兵/Cluster 高可用 |
- 互斥锁实现:
SET lock:key uuid NX EX 10,抢到才查 DB 重建,其余线程短暂等待后重试读缓存。 - 一致性:更新 DB 后删除缓存,配合延时双删或订阅 binlog(Canal)异步删除。
- 兜底:缓存层不可用时,宁可限流返回降级数据,也不要让 DB 被冲垮。
Q18. Redis 集群 slot 数?
核心结论 :Redis Cluster 固定 16384 个哈希槽,key 经 CRC16(key) % 16384 映射到槽,槽再分配给各主节点。
- 为什么是 16384:集群节点数通常不超过 1000,槽数足够均分;心跳报文用 bitmap 携带槽归属信息,16384 bit = 2KB,而 65536 需 8KB,会浪费带宽。
- 扩缩容:通过槽在线迁移完成(
redis-cli --cluster reshard),迁移期间单个 key 操作会短暂阻塞,大 key 影响明显。 - 多 key 操作(
MGET、事务、Lua)要求所有 key 落在同一个槽,否则报CROSSSLOT;用 hash tag 解决:user:{123}:name与order:{123}:1只按{}内的123计算槽位。
Q19. IOC 与 DI 区别?
核心结论:IOC 是设计思想(把对象创建和依赖装配的控制权交给容器),DI 是它的具体实现手段(容器把依赖注入进去)。
- 传统写法中对象自己
new依赖,耦合高;IOC 后由 Spring 容器统一管理 Bean 的创建、装配与生命周期。 - 注入方式三种:构造器注入 (推荐,可注入
final、便于单测、能提前暴露循环依赖)、Setter 注入、字段@Autowired(不推荐,隐藏依赖且不利于测试)。 - 容器:
BeanFactory(基础工厂)/ApplicationContext(增强:事件、国际化、AOP、资源加载)。 - 循环依赖:三级缓存(一级成品、二级半成品、三级工厂)解决单例 Setter 注入的循环依赖;构造器注入的循环依赖无法解决 ,需
@Lazy或调整设计。
Q20. AOP 实现原理(JDK/CGLIB)?
核心结论:Spring AOP 基于运行时动态代理------有接口用 JDK Proxy,无接口用 CGLIB 生成子类(Spring Boot 2.x 起默认 CGLIB)。
| 对比项 | JDK 动态代理 | CGLIB |
|---|---|---|
| 机制 | 实现同一接口 | 继承目标类生成子类 |
| 限制 | 目标必须实现接口 | 不能代理 final 类 / final 方法 |
| 性能 | 调用快,创建慢 | 创建慢,调用略快(现代 JVM 差距已很小) |
- 核心概念:切点 Pointcut、通知(Before / After / AfterReturning / AfterThrowing / Around)、切面 Advisor;采用运行时织入。
- 调用链:代理对象 → 拦截器责任链 → 目标方法。
@Transactional就是典型的环绕通知:代理开启事务 → 执行目标方法 → 提交或回滚。自调用(this.xxx)不经过代理,事务因此失效。- AspectJ 是编译期 / 类加载期织入,能力更强(可拦截字段、构造器),但需额外编译器或 agent。
Q21. Bean 生命周期?
核心结论:实例化 → 属性填充 → Aware 回调 → 前置处理 → 初始化 → 后置处理 → 使用 → 销毁。
text
1. 实例化(构造器创建对象)
2. 属性赋值 populateBean(依赖注入)
3. Aware 回调:BeanNameAware / BeanFactoryAware / ApplicationContextAware
4. BeanPostProcessor#postProcessBeforeInitialization
5. 初始化:@PostConstruct → InitializingBean#afterPropertiesSet → init-method
6. BeanPostProcessor#postProcessAfterInitialization(AOP 代理对象在此生成)
7. 就绪,放入单例池(singletonObjects)
8. 销毁:@PreDestroy → DisposableBean#destroy → destroy-method
- 作用域:
singleton(默认,容器启动时创建)/prototype(每次获取新建)/request/session。 prototype的 Bean 容器只负责创建,不负责销毁,资源需自行释放。- 关键考点:AOP 代理是在第 6 步生成的,三级缓存解决的循环依赖也发生在实例化之后、初始化之前。
Q22. Spring Boot 自动配置原理?
核心结论 :@EnableAutoConfiguration 扫描 classpath 下的自动配置类,按 @Conditional 条件装配 Bean,实现"引入 starter 即生效"。
@SpringBootApplication=@SpringBootConfiguration+@ComponentScan+@EnableAutoConfiguration。- 配置清单位置:Spring Boot 2.7 之前是
META-INF/spring.factories,之后是META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports。 - 条件注解:
@ConditionalOnClass(classpath 中存在某类才配)、@ConditionalOnMissingBean(用户没自己定义才配,故自定义 Bean 可覆盖默认)、@ConditionalOnProperty。 - 顺序控制:
@AutoConfigureBefore/@AutoConfigureAfter/@AutoConfigureOrder。 - 调试手段:
debug=true看自动配置报告(CONDITIONS EVALUATION REPORT),或在启动注解上用exclude排除某个自动配置类。
Q23. @Transactional 失效场景?
核心结论:事务靠 AOP 代理生效,凡是绕过代理、异常被吞或配置不当的场景,事务都不会回滚。
- 方法不是
public(JDK/CGLIB 代理限制);同类内部自调用 (this.xxx()未走代理)→ 注入自身代理或用AopContext.currentProxy()。 - 异常被
try-catch吞掉未抛出;或抛出受检异常但默认只回滚RuntimeException/Error→ 需配@Transactional(rollbackFor = Exception.class)。 - 数据库引擎不支持事务(MyISAM)、对象不是 Spring 管理的 Bean、方法是
final/static(无法被代理)。 - 传播行为用错(如
NOT_SUPPORTED、NEVER);事务过大导致锁持有过久,应拆小事务 + 异步化。
Q24. MQ 如何保证不丢消息?
核心结论:按"生产端 → Broker → 消费端"三段分别保证,任一段掉链子都会丢消息。
- 生产端 :开启
confirm确认与return回调,失败重试;更稳妥的是本地消息表------消息与业务数据同库同事务落表,定时任务扫描失败消息重投。 - Broker :队列与消息持久化(durable)、RocketMQ 同步刷盘 + 主从同步复制;Kafka 多副本
acks=all且min.insync.replicas >= 2。 - 消费端:关闭自动 ACK,业务处理成功后手动 ACK;失败进入重试队列,多次失败转死信队列人工兜底。
- 兜底:定期对账(生产/消费条数比对)+ 消息轨迹追踪,发现丢失可补偿。
Q25. 如何保证消费幂等?
核心结论:MQ 只能保证"至少一次投递",重复消费不可避免,幂等必须由消费端自己保证。
- 数据库唯一键 :以
msgId或业务单号建唯一索引,重复插入直接失败即视为已处理(最简单可靠)。 - 状态机校验 :订单状态流转做前置判断(
待支付 → 已支付),重复消息因不满足前置状态而直接忽略。 - Redis 去重 :
SET msgId 1 NX EX 86400,抢到才消费;注意业务失败时要删除该标记,否则这条消息永远无法重试。 - 乐观锁 :带版本号更新
update ... set stock=stock-1, version=version+1 where id=? and version=?,天然幂等。
Q26. CAP 理论及取舍?
核心结论:一致性 C、可用性 A、分区容错 P 三者不可兼得;分布式环境下网络分区必然发生,P 必须保留,所以实际是在 C 与 A 之间二选一。
- CP:分区时宁可拒绝服务也不能返回旧数据 → ZooKeeper / etcd(选主期间不可用)、Redis 主从强一致切换。
- AP:分区时继续响应,可能读到不一致数据 → Eureka、Cassandra、多数 NoSQL。
- BASE 理论:基本可用(Basically Available)、软状态(Soft state)、最终一致(Eventually consistent),是 AP 的工程化落地,靠消息队列异步补偿、对账来收敛不一致。
- 单机系统不存在分区,可以同时满足 CA;一旦分布式,CA 不可兼得。
Q27. 分布式锁实现与注意事项?
核心结论 :Redis 用 SET key value NX EX 原子加锁,释放时必须用 Lua 校验 value 再删;生产推荐 Redisson 的看门狗自动续期。
lua
-- 加锁(原子:不存在才设值 + 设过期时间,两步不可拆)
SET lock:order:123 uuid NX EX 30
-- 释放:比对 value 与删除必须原子执行
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
- 必须有过期时间,防止持锁方宕机后死锁;过期时间要大于业务最大执行时间。
- value 用唯一值(UUID / 线程 ID),防止误删别人加的锁;业务未执行完由看门狗定时续期(Redisson 默认锁 30s,每 10s 续一次)。
- 单点风险:主从异步复制时主宕机可能丢锁;强一致场景可改用 ZooKeeper 的临时顺序节点(会话断开自动释放、天然可阻塞可重入),或使用有争议的 RedLock。
- 可重入:Redis 锁需用 Hash 结构记录重入次数(Redisson 已实现)。
Q28. 分库分表怎么分?分布式 ID?
核心结论:先垂直拆分按业务解耦,再水平拆分解决单表数据量瓶颈;水平拆分后必须有全局唯一 ID。
-
垂直拆分:按业务拆库(订单库、用户库)、按字段拆表(把大字段如详情、图片拆出去)。
-
水平拆分 :取模(
user_id % N,数据均匀但扩容要迁移)、范围/按时间(易扩容但易热点)、一致性哈希(扩容迁移量小)。 -
分片键选择 :高频查询条件 + 高基数 + 让关联数据尽量同片(订单与订单明细都按
user_id拆),避免跨片 join 和跨片分页。 -
分布式 ID ------ 雪花算法 :
text0 | 000...000(41位 毫秒时间戳) | 5位 dataCenterId + 5位 workerId | 12位 序列号趋势递增、本地生成无网络开销、单节点每毫秒可生成 4096 个。注意两点:时钟回拨 (记录上次时间戳,回拨则等待或抛错)与机器 ID 分配(用 ZK / DB 统一分配)。
-
替代方案:号段模式(美团 Leaf-segment,DB 批量取号本地发放)、Redis
INCR、数据库步长自增;UUID无序,作为 InnoDB 主键会造成页分裂,不适合。
Q29. 分布式事务方案?
核心结论:强一致用 2PC/XA,高并发业务主流是最终一致(事务消息 / 本地消息表),TCC 用于金融等强隔离场景。
- 2PC / XA:协调者两阶段提交,强一致但同步阻塞、协调者单点、锁持有时间长,性能差,适合短事务。
- TCC :Try 预留资源(冻结库存)→ Confirm 确认 / Cancel 释放;业务侵入大,必须处理空回滚、幂等、悬挂三个经典问题。
- 事务消息(RocketMQ,主流) :发 half 消息 → 执行本地事务 → Commit/Rollback;Broker 长时间未收到确认则回查本地事务状态;消费端保证幂等。吞吐高、最终一致,是"下单扣库存"的常用方案。
- 本地消息表:业务数据与消息在同一个本地事务中落库,定时任务扫描投递 + 重试;实现简单,对业务有侵入。
- Seata :AT 模式(基于
undo_log自动生成反向 SQL,对业务几乎无侵入)/ TCC / Saga 三种模式可选。
Q30. 秒杀系统如何设计?
核心结论:核心思路是层层拦截,把绝大多数请求挡在数据库之前------流量分层、缓存前置、异步削峰、兜底降级。
1)流量分层
- 前端/APP:按钮置灰防重复提交、验证码/答题削峰拉长请求时间;详情页静态化 + CDN,动静分离。
- 网关层:令牌桶/漏桶限流、黑名单、用户维度限购(一人一单)。
- 服务层:Redis 预扣库存,扣减成功才发消息,失败直接返回"已售罄"。
2)缓存前置
-
活动开始前把库存、商品信息预热到 Redis;商品详情走"本地缓存(Caffeine)+ Redis"多级缓存,避免缓存击穿。
-
扣库存用 Lua 脚本原子执行 (判断库存 → 扣减 → 记录已购用户),杜绝超卖:
luaif tonumber(redis.call('get', KEYS[1])) > 0 then redis.call('decr', KEYS[1]) return 1 -- 扣减成功 end return 0 -- 已售罄
3)异步削峰
- 扣减成功后投递 MQ,异步创建订单、扣 DB 库存(乐观锁
update sku set stock=stock-1 where id=? and stock>0),消费失败进死信队列重试。 - 流量与处理能力解耦,DB 只承担与库存等量的写压力。
4)兜底降级
- DB 层限流与连接池隔离;售罄标记前置(命中即返回,不再查库);非核心链路(积分、通知、推荐)熔断降级。
- 定时任务对账补偿:处理"Redis 扣了但订单创建失败"的少卖。
其他 :接口幂等(user_id + sku_id 唯一键保证一人一单)、风控防刷、Redis 集群(Cluster + 哨兵)保证高可用,上线前压测确定容量并预留水位。