面灵AI→

蚂蚁集团后端开发面经合集 03:交易一致性与高并发设计

轮次
多轮面试合集
时间
2026-09
来源
牛客网

《面试题目》

面经 01 · 后端开发岗(2026-01-29)

  1. 除了对账,还有哪些方式保证交易状态一致性?
  2. 如何处理超时/失败单据的重试,且避免重复扣款?
  3. 若己方标记交易成功、渠道侧失败,如何发现并处置?
  4. 项目中的性能优化手段有哪些?
  5. 多线程对账时,线程数如何设置?考量因素是什么?
  6. 线程安全问题的成因,举例说明。
  7. 保证线程安全的方法有哪些?
  8. 死锁的成因及解决办法。
  9. 如何保证 Redis 缓存与 DB 数据一致性?
  10. 高并发下删除缓存后,如何避免 DB 被击穿?
  11. Redis 单机支持高并发的原因是什么?单线程 Redis 与多线程对账任务的性能逻辑差异在哪?
  12. 分库分表后,如何保证交易幂等性(含跨天场景)?
  13. 高频场景下(单独查 A、单独查 B、联合查 A+B),如何设计索引?
  14. 索引的底层数据结构是什么?B+ 树为何查询高效?
  15. MySQL 的请求处理流程是什么?
  16. Spring 框架中值得借鉴的设计模式/原则有哪些?

面经 02 · Java 后端开发(2025-12-09)

  1. HashMap 的原理是什么?JDK 1.8 做了哪些优化、解决了之前的什么问题?并发安全问题怎么解?
  2. JVM 内存模型是什么?方法区和元空间是什么关系,元空间完全替代方法区了吗?
  3. 说说你对垃圾回收器的理解,G1 的工作流程分哪几步?实际调优时优先调整哪些参数?
  4. ThreadLocal 的实现原理是什么?内存泄漏的原因是什么,具体怎么防范?
  5. MySQL 的 MVCC 基于什么机制实现?它能解决幻读吗,具体怎么做到的?
  6. Redis 处理 Hash 冲突用了什么方式?扩容时会阻塞服务吗,为什么?
  7. Spring 三级缓存分别存了什么?为什么用三级缓存而不是两级?
  8. SpringBoot Starter 自动配置的 SPI 机制核心是什么?怎么自定义一个 Starter?
  9. TCP 拥塞控制和流量控制的目标分别是什么?实现上有什么区别?
  10. 实现 LFU 缓存淘汰策略的核心思路是什么?怎么处理访问频率相同的键?
  11. 类加载器双亲委派模型被打破的常见场景有哪些?打破后有什么影响?
  12. AQS 同步器的核心数据结构是什么?ReentrantLock 怎么利用 AQS 实现可重入?
  13. 数据库索引下推的适用场景是什么?它能提升性能的原因是什么?
  14. 分布式 Session 一致性有哪些常见方案?哪种更适合高并发场景?
  15. 消息队列事务消息的核心流程是什么?怎么保证消息不丢失、不重复?

面经 03 · Java 后端开发(2025-12-09)

  1. 设计百万并发秒杀系统,核心要解决什么问题?架构如何选型?
  2. 分布式 ID 生成需要满足哪些要求?不同生成方案的优缺点是什么?
  3. 微服务链路追踪靠什么收集调用数据?怎么实现全链路关联?
  4. 数据库分库分表后,跨库查询有哪些方案?哪种方案性能更好?
  5. 缓存和数据库双写时,先更新缓存还是先更新数据库?怎么避免数据不一致?
  6. 服务网格的核心组件有哪些?在微服务中落地时会遇到什么问题?
  7. 线上 Full GC 频繁,可能的原因有哪些?排查时按什么顺序定位问题?
  8. 分布式锁的 Redlock 算法核心逻辑是什么?争议点主要集中在哪些方面?
  9. 容器化部署的弹性伸缩依据什么指标?怎么配置伸缩策略更合理?
  10. 系统容量评估时会考虑哪些因素?性能压测的关键指标有哪些?
  11. 灰度发布的常见策略有哪些?故障熔断的触发条件怎么设置?
  12. 大数据量下 Elasticsearch 的查询性能会受什么影响?怎么针对性优化?
  13. 实时计算框架有哪些?不同框架的适用场景是什么?怎么技术选型?
  14. 云原生架构下的监控体系要覆盖哪些层面?用什么工具搭建更高效?
  15. 技术债务重构的优先级怎么划分?系统演进时怎么保证业务不中断?

面经 04 · Java 后端开发(2025-12-07)

  1. 三个微服务 A/B/C 分别操作不同的数据库和 Redis,要求最终一致且尽量「准实时」;某次网络抖动导致 A 提交成功、B 超时、C 回滚,这种情况下怎么保证最后状态达成一致?
  2. 跨 IDC(双活)系统必须保证强一致性,但业务方要求写延迟低于 5ms,怎么实现?为什么?
  3. 对一个百亿级大表做 online DDL 且不影响线上读写,用什么方案?
  4. MySQL 主从复制延迟 30 秒后主库挂了,从库又丢了 binlog 的最后 10 秒,业务方要求最终数据一致但不允许回滚用户侧可见状态,你怎么做?
  5. MQ 在多分区、多消费者下怎么防止乱序和重复消费?
  6. 「真正的 Exactly Once」在分布式系统中存在还是不存在?
  7. 订单查询链路要调用 8 个服务、每个服务都要查一次 Redis,怎么把这条链路优化到至少 50%?
  8. 了解 k8s 吗?
  9. K8s 集群节点资源充足但 Pod 一直 Pending,怎么一步步推断可能的原因?
  10. 一个 key 对应的 value 是 JSON 结构,其中有多个子任务并发修改这个 key,会有线程安全问题吗?怎么解决?多节点情况下应该怎么加锁?

《参考解析》

1. 交易最终一致:对账之外的三道防线:对账是事后兜底,事中要靠三样东西。一是本地消息表或事务消息,把业务落库与消息投递放进同一个本地事务,让任何状态变更都被可靠驱动;二是状态机加补偿任务,单据只能在「待支付→支付中→成功/失败」这类合法路径上流转,停在中间态的单据按退避策略反复推进,而不是直接判死;三是主动反查渠道,对未达终态的单据定时查询,而不是只等对方推。单边成功(己方成功、渠道失败)多半源于「先落成功再等应答」,正确做法是本地先落处理中、拿到渠道明确应答再置终态;一旦发现单边,先冻结该单据的后续动作,再冲正或补推渠道,并把差异写入差异池人工兜底。

2. 重试与幂等:怎么重试才不重复扣款:重试的前提是错误可重试,参数非法、余额不足这类业务失败不该重试;重试要有次数上限、指数退避加抖动,且先查询再重试。超时只代表结果未知,必须靠查询或对账确认,把超时当失败直接重试就是重复扣款的经典来源。幂等落地分三层:业务单号做唯一键(渠道单号与商户号也传给渠道,让对方识别重复请求)、落库唯一索引兜住并发、本地流水表记录每次尝试并用状态机锁定终态,重复请求命中终态直接返回原结果。跨天场景要小心幂等键设计,用「单号 + 日期」做键会在 0 点后失效,隔夜重试就被当成新单据。

3. 多线程对账:线程数、线程安全与死锁:对账是 IO 密集型,线程数按「并发度 ≈ 峰值 QPS × 单次耗时」估算,再受数据库连接池与渠道限流约束,一般从核数 ×(2~4) 起调、用压测找拐点,而不是拍一个固定值。线程安全的根因是共享可变状态:计数器、批次汇总 Map、同一张单据被两个线程处理。解法优先级是分片加线程封闭(按商户、日期切分,谁的数据谁处理)优于不可变对象,不可变对象优于显式锁,最后才是并发容器。死锁四条件里最实用的是打破循环等待:统一加锁顺序、tryLock 加超时回退,数据库侧坚持按主键顺序更新并保持短事务。

4. 缓存与 DB 一致性、删缓存后的击穿防护:主流选择是 Cache Aside,先更新数据库再删除缓存,而不是更新缓存,把不一致窗口压到最小;删除失败就投递到消息队列或本地重试表异步补偿,或者订阅 binlog 驱动删除,让缓存失效与数据变更解耦。要绝对强一致只能让热点数据不走缓存,或用版本号、读写锁把读写串起来。删缓存瞬间大量请求落到 DB 的防护有三层:互斥重建或逻辑过期,只放一个线程回源、其余返回旧值或短暂等待;单飞合并同一 key 的并发回源;热点 key 干脆不设过期、由后台任务定时刷新。DB 侧再配限流与降级兜底,避免缓存故障直接压垮数据库。

5. Redis 单机高并发与多线程对账的性能逻辑差异:Redis 快在纯内存操作、单线程事件循环省掉锁与上下文切换、IO 多路复用把大量连接压进一个线程,加上跳表、压缩列表这类为场景定制的高效结构和轻量的 RESP 协议。6.0 之后的多线程只负责网络读写与协议解析,命令执行仍然单线程,所以不存在命令级并发。多线程对账走的是另一条路:把 IO 等待并行化,瓶颈在渠道与数据库的响应时间,加线程能把等待时间重叠起来,但超过下游承载后收益迅速下降,反而引入锁竞争与上下文切换。一句话,Redis 用「少线程 + 非阻塞 IO」换吞吐,对账用多线程掩盖 IO 延迟,优化的瓶颈根本不同。

6. 分库分表后的交易幂等(含跨天):幂等键在分库分表之后仍然要全局唯一,所以用业务单号而不是自增 ID;分片键尽量与幂等键对齐(按商户或订单号分片),保证同一张单据永远落到同一个库,否则要引入路由表或做二次分片来定位。落地上三层:唯一索引(分片内唯一,跨片去重靠全局去重表或 Redis SETNX)、状态机(只允许合法流转,重复请求命中终态直接返回原结果)、幂等窗口。跨天的坑在于「单号 + 日期」这类键在 0 点后失效,隔夜重试会被当新单据再扣一次;解法是幂等键只用业务单号,记录保留到业务允许的最大重试窗口(通常一周以上),并用统一的对账批次号把跨天批次串起来。

7. 高频「单查 A / 单查 B / 联合查 A+B」的索引设计:关键是让三条路径都走得上索引,而联合索引 (a, b) 只能覆盖其中两条:单独查 A 走最左前缀、联合查 A+B 能走全列甚至可以做成覆盖索引避免回表,单独查 B 用不上。所以按查询量权衡:B 的单独查询量大就为 B 单独建索引(或再建一条 (b, a)),代价是写入放大;B 基数很低、联合查询占多数时,可以只留 (a, b),让单独查 B 走扫描加过滤。配套细节:把高频返回列放进索引做覆盖、避免在索引列上做函数运算或隐式类型转换、区分度高的列放前面,用 explain 看 type、rows、Extra 验证,同时记住索引越多写越慢、buffer pool 命中率越低。

8. B+ 树与一条 SQL 在 MySQL 中的处理流程:B+ 树适合做索引是因为非叶子节点只存键不存数据,单页能放更多键、树高更低,千万级数据也就三四层,一次查询三四次 IO;数据全在叶子节点并用双向链表相连,范围查询与排序只需顺序扫叶子,不必回退上层;树自平衡,插入删除代价可控。一条 SQL 的旅程是:连接器做鉴权与连接管理,解析器做词法与语法分析生成语法树,优化器基于统计信息选索引、定 JOIN 顺序并产出执行计划,执行器调用存储引擎接口逐行取数过滤,InnoDB 在 buffer pool 里按页读写、必要时回表。理解链路的意义在于慢查询可能出在不同环节:优化器选错索引、执行器回表过多、存储引擎刷脏页,排查手段完全不同。

9. HashMap 的 JDK 1.8 优化与并发安全:1.8 的结构是数组 + 链表 + 红黑树。关键改动有四:链表长度到 8 且容量到 64 时转红黑树,把最坏查找从 O(n) 降到 O(log n),避免劣质 hashCode 或碰撞攻击把性能打穿;扩容时利用容量是 2 的幂,用 hash & oldCap 判断节点留在原位还是移到「原位 + oldCap」,省掉重新计算 hash,扩容成本减半;头插改尾插,消除 1.7 并发扩容时链表成环、CPU 打满的问题;扰动函数从四次移位异或简化为一次高位异或。但 1.8 只是不再成环,并发 put 依然会丢数据、size 计数不准,所以并发场景要用 ConcurrentHashMap(CAS 加锁单个桶)或直接换不可变 Map。

10. 方法区、元空间与 G1 调优:方法区是 JVM 规范里的逻辑概念,存放类元信息、常量与静态变量;永久代是 HotSpot 在 1.7 及以前对它的实现,位于堆内、受 MaxPermSize 限制,容易 OOM: PermGen。1.8 用元空间替代永久代,元空间在本地内存中、默认只受物理内存限制(可用 MaxMetaspaceSize 兜底),类加载器被回收后对应空间才释放。所以被替代的是永久代这个实现,方法区概念仍在。G1 的流程是初始标记(STW,标 GC Roots 直接可达)→ 并发标记(与用户线程并行,SATB 解决漏标)→ 最终标记(STW,处理剩余 SATB 队列)→ 筛选回收(按回收价值排序选一组 Region 复制清理)。调优先定堆大小与 MaxGCPauseMillis,再调并发标记触发时机 IHOP,最后才动 Region 大小与 GC 线程数。

11. ThreadLocal 原理与内存泄漏:每个 Thread 内部持有一个 ThreadLocalMap,key 是 ThreadLocal 的弱引用,value 是强引用。泄漏链路是:ThreadLocal 实例被回收后 key 变成 null,value 仍被 Entry 强引用,只要线程活着(线程池的核心线程几乎永生)这块 value 就回收不掉,Entry 本身也一直挂在 map 里。防范手段:用完必须 remove,并用 try/finally 保证异常路径也能清理;声明成 static final 让 key 不被回收,至少还能通过 key 找到并清理;不要用它存大对象;线程池场景把清理当作任务收尾的一部分。另外 ThreadLocal 不做父子传递,需要传递要用 InheritableThreadLocal,但线程池复用线程会让它失效,要换成可传递的 TTL 实现。

12. MVCC 与幻读:MVCC 靠三件套实现:隐藏列(事务 ID 与回滚指针)、undo log 版本链、ReadView。读已提交在每条 SELECT 时生成新 ReadView,可重复读在事务首次读时生成并复用,这就是两者可见性差异的来源。判断可见性时用 ReadView 里的活跃事务集合、最小与最大事务 ID 去比对版本链上的事务 ID,再沿回滚指针找到可见版本。InnoDB 在可重复读下确实能让快照读看不到别的事务新插入的行,但当前读(SELECT … FOR UPDATE、UPDATE、DELETE)依然读最新数据,所以「先查后插」这类场景必须靠 next-key lock(记录锁 + 间隙锁)堵住间隙,否则并发插入仍会造成逻辑上的幻读,说 MVCC 单独解决了幻读并不严谨。

13. Spring 三级缓存与循环依赖:一级 singletonObjects 存完整的成品 Bean,二级 earlySingletonObjects 存提前暴露的半成品,三级 singletonFactories 存能产出早期引用的 ObjectFactory(通常包着 AOP 代理工厂)。只用两级也能解决普通循环依赖,但遇上 AOP 就出问题:B 注入 A 时如果拿到原始对象,而 A 最终被代理,B 里持有的就是没被增强的 A,切面失效。三级缓存把「提前引用」延迟到真正被依赖时才通过工厂获取,必要时提前生成代理,保证注入方拿到的正是最终进一级缓存的那个对象。它的边界也要清楚:构造器注入的循环依赖、prototype 作用域的循环依赖都解决不了,Spring Boot 2.6 之后默认禁止循环依赖,正规做法仍是解耦。

14. 事务消息:流程与不丢不重:以 RocketMQ 为例,生产者先发半消息(对消费者不可见),Broker 落盘成功返回后,生产者执行本地事务,再按结果提交 commit 或 rollback,commit 之后消息才对消费者可见。如果生产者没回执,Broker 会定时回查生产者的事务状态,按本地事务表决定提交还是回滚,这一步保证「本地事务成功则消息最终一定可见」。不丢要三段接力:生产端同步发送加重试,Broker 端同步刷盘或主从同步复制,消费端处理成功才提交位点并配重试队列与死信队列。不重则不能指望中间件——它只能做到至少一次投递,真正幂等要消费端自己做:业务唯一键加去重表,或 Redis 记录已处理的消息 ID 并设过期时间,即 Exactly Once = 至少一次 + 消费端幂等。

15. 百万并发秒杀的核心矛盾与架构选型:核心矛盾只有三个:瞬时高并发写、库存不能超卖、不能拖垮整个系统。分层削峰:接入层做页面静态化与动静分离、用验证码或答题拦掉脚本流量、网关限流与黑名单;应用层无状态水平扩容,把库存判断前置到 Redis,用 Lua 把「判断库存 + 扣减 + 记用户购买标记」做成一次原子操作,抢不到的直接快速失败;抢到后只写一条消息,由消费者按数据库能力匀速创建订单,DB 侧用条件更新(库存大于 0 才减)兜底,热点行拆分成分段库存;数据层做订单分库分表。配套还需要分布式 ID(要求全局唯一、趋势递增、高可用、低延迟:雪花算法性能好但依赖时钟、要处理回拨,号段模式依赖 DB 但有批量缓冲,Redis INCR 简单但多一次 RPC)、风控防刷,以及按钮置灰加结果轮询的前端体验设计。