后端高频面试题与解答

后端高频面试题与解答

    • 第一部分:题目清单(自测用)
    • 第二部分:逐题解答
      • [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)。

用法:第一部分是纯题目清单,先遮住答案自己答一遍;第二部分逐题对照解答。


第一部分:题目清单(自测用)

  1. TCP 三次握手为什么是三次?
  2. TIME_WAIT 作用?
  3. HTTP 常见状态码?
  4. HTTPS 握手过程?
  5. HTTP/2 相比 1.1 提升?
  6. 为什么 MySQL 用 B+ 树?
  7. 聚簇与非聚簇索引区别?
  8. 最左前缀原则?
  9. 什么情况索引失效?
  10. 事务隔离级别与幻读?
  11. MVCC 原理?
  12. 行锁升级表锁条件?
  13. 间隙锁解决什么问题?
  14. 如何定位慢 SQL?
  15. Redis 五种结构及场景?
  16. RDB 与 AOF 区别?
  17. 缓存穿透/击穿/雪崩及方案?
  18. Redis 集群 slot 数?
  19. IOC 与 DI 区别?
  20. AOP 实现原理(JDK/CGLIB)?
  21. Bean 生命周期?
  22. Spring Boot 自动配置原理?
  23. @Transactional 失效场景?
  24. MQ 如何保证不丢消息?
  25. 如何保证消费幂等?
  26. CAP 理论及取舍?
  27. 分布式锁实现与注意事项?
  28. 分库分表怎么分?分布式 ID?
  29. 分布式事务方案?
  30. 秒杀系统如何设计?

第二部分:逐题解答

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_recycleSO_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+ 树查找。
  • 覆盖索引 :查询字段全在二级索引中,无需回表,EXPLAINExtra 显示 Using index
  • 实践提示:主键尽量短且自增,二级索引叶子都冗余主键,太长会放大索引体积。

Q8. 最左前缀原则?

核心结论 :联合索引 (a,b,c) 需按从左到右连续匹配,跳过最左列或遇到范围查询,其后的列就不再用于索引定位。

  • 完全生效:a=1a=1 and b=2a=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 inis 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 updateupdatedelete)走锁。

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 updateupdatedelete)且走索引的范围条件;唯一索引的等值命中会退化为纯行锁。
  • 代价:降低并发、容易死锁(两个事务互相持有对方需要的间隙),因此应尽量缩小加锁范围、用等值条件替代范围条件。

Q14. 如何定位慢 SQL?

核心结论 :慢日志发现 → EXPLAIN 定位 → 按索引、SQL、数据量、架构四层依次优化。

  • 发现:开启慢查询日志 slow_query_log=ONlong_query_time=1,用 mysqldumpslow / pt-query-digest 聚合出 TOP SQL。
  • 分析:EXPLAIN 重点看 typeALL 最差,目标 ref/range)、key(实际用到的索引)、rows(预估扫描行数)、Extra(出现 Using filesortUsing 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 重写瘦身)
恢复速度
数据安全性 丢最后一次快照之后的写入 取决于 appendfsynceverysec 最多丢 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}:nameorder:{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_SUPPORTEDNEVER);事务过大导致锁持有过久,应拆小事务 + 异步化。

Q24. MQ 如何保证不丢消息?

核心结论:按"生产端 → Broker → 消费端"三段分别保证,任一段掉链子都会丢消息。

  • 生产端 :开启 confirm 确认与 return 回调,失败重试;更稳妥的是本地消息表------消息与业务数据同库同事务落表,定时任务扫描失败消息重投。
  • Broker :队列与消息持久化(durable)、RocketMQ 同步刷盘 + 主从同步复制;Kafka 多副本 acks=allmin.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 ------ 雪花算法

    text 复制代码
    0 | 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 脚本原子执行 (判断库存 → 扣减 → 记录已购用户),杜绝超卖:

    lua 复制代码
    if 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 + 哨兵)保证高可用,上线前压测确定容量并预留水位。

相关推荐
我命由我123453 小时前
Photoshop - Photoshop 把两个 PSD 文件合并
学习·ui·职场和发展·产品运营·产品经理·学习方法·photoshop
数智启示录5 小时前
PostgreSQL 计划缓存实战(第 10 篇):预编译 SQL 前五次都快,第六次为什么可能变慢
经验分享·sql·缓存·postgresql·面试
CoderYanger7 小时前
A.每日一题:1140. 石子游戏 II
java·程序人生·算法·leetcode·游戏·职场和发展·深度优先
windliang7 小时前
Agent Loop 的 while 循环什么时候开始不够用
人工智能·面试·开源
不要打扰7568 小时前
2027国网提前批宣讲、面试、预报名、内部笔试时间一览
面试·职场和发展
YHHLAI8 小时前
TypeScript 高级工具类型面试指南
ubuntu·面试·typescript
退休倒计时10 小时前
【每日五题】leetcode TypeScript
算法·leetcode·职场和发展·typescript
众人皆醒我独醉11 小时前
Kubernetes GPU 调度与管理的完整机制——从节点上架到 Pod 拿到 GPU
面试·kubernetes·gpu
程序员-Benothing12 小时前
MySQL 中的 Log Buffer 是什么?它有什么作用?
数据库·mysql·面试