蚂蚁集团后端开发面经合集:分布式锁、缓存与 AI Coding
- 轮次
- 多轮面试合集
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
面经 01 · AI 后端开发(2026-04-26)
- 对 Agent 了解多少?RAG 了解吗?
- Skill 和 MCP 的区别是什么?
- 如何用 Redis 实现分布式锁?
- 为什么必须用 Lua,不用 Lua 会有什么问题?
- 你实现的分布式锁是乐观锁还是悲观锁?
- CAS 实现的乐观锁有什么局限性?在哪些场景下有风险?
- 如果让你用 Redis 做 MySQL 的缓存,你会采取什么样的更新策略?
- 在浏览器里输入 URL 到页面完全展示,中间都发生了什么?
- Linux 下的常用环境变量有哪些?
- 如何在 shell 脚本中判断上一条命令执行成功与否?
- 设计一个短链系统。
- 你认为这个短链系统的性能瓶颈是在读还是在写?
- 那你如何优化性能呢?
- 手撕:判断一个链表内部是否有循环,有的话找到循环开始的节点位置。
面经 02 · AI 后端开发(2026-04-23)
- 讲讲乐观锁。
- Redis 和 MySQL 数据读写顺序,读和写不同吗?
- Redisson 分布式锁原理是什么?
- 为什么选择使用 RocketMQ,不用 RabbitMQ 或其他中间件,它对比别的消息队列有什么优势?
- 如果发生了超卖问题该怎么处理?
- 生产者消费者模型中如果消费者获取信息失败该怎么办?
面经 03 · 后端开发岗(2026-04-23)
- 在做过的项目或实习中,有没有碰到过比较困难、有技术挑战的问题?挑一个展开讲讲当时的场景和解决方案。
- 解决那个技术问题是你自己独立完成的,还是有同学协助?
- 在这些项目中,你自己代码量最大的是哪一个?大概有多少行代码?
- 你平时用 AI 辅助编码吗?怎么用的?
- 如何用 AI 写出高质量的代码?
- 手撕:前缀和(区间求和)。
- 手撕:LRU 缓存。
- 你写的 LRU 缓存是线程安全的吗?如果要改成线程安全,需要怎么修改?具体哪几行代码要加锁?
- 你对区块链领域有了解吗?
- 如果能来实习,大概什么时候能到岗?能持续多长时间?
面经 04 · 后端开发岗(2026-04-20)
- 介绍下实习项目,详细说说具体过程。
- ES 你是怎么用的?单体还是分布式?
- ES 与 MySQL 一致性怎么保证?
- 数据过期问题怎么处理?
- embedding 用的什么?使用过别的吗?原理了解吗?
- RAG 数据过期怎么办?
- 语义相似不相同怎么办?
- 聊聊 Redis,基本数据结构有哪些?
- 你用 Redis 做知识库缓存,怎么做的?
- DDD 是什么?
- 场景题:三个线程,1 把锁,每 5 分钟抢占一次,让设计锁。
面经 05 · Java 后端开发(2026-04-14)
- 讲一讲 Java 里面的 JVM 内存分配,内存模型是怎么样的?
- 堆里面具体又是怎么分的?
- 为什么在堆上去做分代分区的模式?目的是什么?
- 详细介绍一下你提到的几类垃圾回收算法。
- Java 里面多线程是怎么用的?举个具体的应用场景。
- Java 里面常用的工具线程池有哪些?
- ThreadPoolExecutor 构造函数里面有哪些核心参数?
- 线程池的队列满不满是怎么判断的?
- Java 里的有界队列是用什么实现的?
- 如果参数里用了无界队列,会产生什么问题?
- 线程安全的产生原因是什么?请从内存分配的角度解释。
- 什么是 HTTP 协议?它的主要格式(请求体/响应体)和特征有哪些?
- Cookie 是什么?它和 HTTP 协议的关系是什么?
- MySQL 的表引擎有哪些?InnoDB 的特征和使用场景是什么?
- 在数据库中如何实现悲观锁和乐观锁?
- 写 SQL 时如何避免慢查询?有哪些手段?
- 谈谈你对 AI Coding 的了解,以及在复杂项目中如何用 AI 辅助开发、如何验证代码正确性?
- 使用 AI 辅助实现功能时,大体上分为哪几个阶段?
- 详细展开讲讲 AI Coding 中的 Plan(计划)模式环节。
- 什么是 Agent?它和大模型是什么关系?
- AI 领域的 Skill(技能)是什么?
- 在上下文(Context)有限的情况下,如何保证 AI 能理解长对话内容?
- 大模型中「深度思考」和「快速思考」的区别和背后逻辑是什么?
《参考解析》
面经 01 / 02 · 分布式锁与缓存
1. Redis 分布式锁的实现、Lua 的必要性与乐观/悲观归属:加锁用 SET key value NX PX ttl,value 必须是本机唯一标识(UUID + 线程 ID),解锁时先比对 value 再删除,防止误删别人的锁。必须用 Lua 的原因是「比对 value」和「删除 key」是两步,中间可能发生锁过期被他人抢走、或客户端暂停,导致当前线程删掉了新持有者的锁;Lua 脚本在 Redis 里整体原子执行,把校验与删除合成一个不可分割的操作。同理,复杂的「判断库存 + 扣减」也都要用 Lua 保证原子性。这套实现属于悲观锁:先抢到锁再操作,假设冲突一定会发生,用串行化换取安全。它的边界要清楚:单实例 Redis 在故障切换时可能丢锁(主从异步复制导致新主上没有锁记录),可以用 Redlock、多副本多数派,或者干脆放弃分布式锁改用「唯一约束 + 状态机」的乐观方案。另外锁要设过期时间、看门狗续期、业务侧还要有幂等兜底,因为锁只能降低并发,不能替代幂等。
2. CAS 乐观锁的局限与风险场景:CAS 比较并交换在无竞争时几乎零开销,但有一串坑:ABA 问题(值被改回原样,比较看不出来,需要版本号或 AtomicStampedReference);自旋重试失败时白烧 CPU,高竞争下比悲观锁更慢;只能保证单个共享变量的原子性,多个字段要合并成一个对象或改用锁;无法像锁那样提供可见性与有序性的完整语义,跨多个变量的复合不变式它管不了。风险场景很典型:秒杀扣库存(高竞争下自旋爆炸)、先查再改的「读改写」逻辑(检查与执行之间有窗口,超卖就是这么来的)、跨服务/跨库的并发控制(CAS 只能作用于同一份存储)、以及把版本号当业务字段却又在多处更新导致版本跳跃。所以工程上的用法是:低冲突、临界区极短、单变量的场景用 CAS;高冲突或需要跨行跨库时,用数据库行锁、唯一约束、排队或分布式锁;无论如何都在最外层再叠一层幂等。
3. 用 Redis 做 MySQL 缓存的更新策略:主流是 Cache-Aside:读请求先查缓存,未命中查库并回填(回填设随机过期时间,避免同一时刻集体失效);写请求先更新数据库,再删除缓存,而不是更新缓存——删除比更新更安全,因为更新缓存在并发下可能写入旧值。为什么是「先更新 DB 再删缓存」:如果先删缓存再更新 DB,删完到更新完之间的读请求会把旧值重新加载进缓存,脏数据长期存在;反过来,先写库再删缓存即使删除失败,最坏也只是短期不一致(可以靠过期时间兜底)。并发极端情况下仍有窗口,可以叠加延迟双删(更新后延迟几百毫秒再删一次)或订阅 binlog(Canal)异步失效。读写顺序不同的本质是:读路径追求命中率与低延迟,可以容忍短暂旧值;写路径追求最终正确,必须让缓存失效而不是让缓存参与决策。真正的一致性保障要靠对账与监控,比如比对缓存与 DB 的关键字段、给不一致率设告警。
4. Redisson 分布式锁的原理解析:Redisson 的核心是「加锁逻辑 + 后台续期」两件套。加锁时执行一段 Lua:key 不存在就创建并设置过期时间与持有者标识(Hash 结构存「客户端 ID:线程 ID → 重入次数」),存在且是本线程就重入计数加一,不是本线程则返回剩余 TTL。释放锁同样用 Lua 做重入计数减一,减到零才真正删除并发布解锁消息。看门狗(watchdog)解决的是「业务执行时间超过锁过期时间」:加锁成功后启动一个后台定时任务,默认每 10 秒(锁 TTL 的三分之一)检查一次,只要业务还在执行就把过期时间续回 30 秒;如果客户端宕机,续期停止,锁在 TTL 后自动释放,不会死锁。必须注意:只有不显式指定 leaseTime 时才启用看门狗,指定了固定过期时间就不会自动续期;主从切换时仍可能丢锁,所以 Redlock 与「锁 + 幂等」的组合才是完整方案;锁等待要用 tryLock 带超时,避免线程无限堆积。
5. Redisson、MQ 选型与超卖治理:Redisson 在生产上通常被用来替代手写 SETNX,因为它把可重入、续期、阻塞等待、公平锁、读写锁和多锁都封装好了,且所有操作都是 Lua 原子执行;代价是要理解它的语义(leaseTime 与看门狗的相互作用、锁释放的线程归属)和网络分区下的边界。消息队列选 RocketMQ 的常见理由是:事务消息原生支持(解决「本地事务 + 发消息」的一致性)、消息可持久化且支持同步刷盘不丢、有定时/延迟消息(电商超时关单直接用)、支持顺序消息(同一订单号走同一队列)、死信队列与重试机制完善、单机吞吐高且运维成熟;RabbitMQ 强在路由灵活与低延迟,但延迟消息要插件、顺序与事务支持弱;Kafka 强在吞吐与流处理,但主题模型偏日志、不支持延迟消息与事务消息那种业务语义。超卖的治理要分层:入口限流与用户幂等占位、Redis Lua 原子扣减库存、数据库用「库存 > 0」的乐观更新或行锁做最终校验、MQ 异步下单并在失败时回补库存,最后用对账任务扫「库存为负」「订单数超卖」的异常。发现超卖后的动作是止损、人工/自动补偿(补货或退款)与根因修复,而不是只改代码。
面经 01 · 网络与系统设计
6. 从输入 URL 到页面完全展示:链路分四段。网络层:URL 解析与缓存检查、DNS 解析(浏览器缓存 → 系统 hosts → 递归查询)、TCP 三次握手、TLS 握手、发送 HTTP 请求、服务端处理并返回响应,其间可能经过 CDN、网关、负载均衡。渲染层:解析 HTML 构建 DOM,解析 CSS 构建 CSSOM,两者合成渲染树,做布局(回流)与绘制,再栅格化上屏;JS 会阻塞 DOM 解析(用 defer/async 缓解),CSS 阻塞渲染。资源层:图片、字体、异步请求并发加载,字体加载还会触发 FOUT/FOIT。可观测与优化点:DNS 预解析、预连接、HTTP/2 多路复用与 Keep-Alive、压缩与缓存(强缓存 + 协商缓存 ETag/Last-Modified)、CDN 就近、关键 CSS 内联、延迟加载。答题时能按段给出「哪一步可能慢、怎么量化」比背流程更有价值。
7. Shell 环境变量与退出码判断:常用环境变量按用途记:PATH(命令搜索路径)、HOME、USER、SHELL、PWD/OLDPWD、LANG/LC_ALL(本地化,脚本里影响排序与日期格式)、TZ(时区)、IFS(字段分隔符,改它容易踩坑)、以及 PS1、HISTSIZE 这类交互变量;脚本里自定义的用 export 导出才能被子进程继承,只赋值不 export 只在当前 shell 有效。判断上一条命令是否成功靠退出码 $?:0 表示成功,非 0 是失败,且它会被下一条命令覆盖,所以要立刻保存。写法有几种:cmd; if [ $? -ne 0 ]; then ...、更简洁的 if cmd; then ... fi、短路运算符 cmd && echo ok || echo fail(注意 || 分支在 cmd 成功但 echo 失败时也会触发)、以及脚本开头的 set -e(命令失败即退出)配合 set -o pipefail(管道中任一环节失败即算失败)和 set -u(使用未定义变量报错)。调试用 bash -x。
8. 短链系统的设计与瓶颈判断:核心是「短码 → 长链」的映射。短码生成有两条路:发号器 + 进制转换(雪花 ID 或数据库自增,转 62 进制,长度可预测且无冲突,需要防遍历可加混淆或随机前缀)、或随机生成短码 + 唯一索引去重(简单但要处理冲突)。存储用 KV(Redis 存热点映射,MySQL/持久 KV 存全量),跳转走 302(便于统计点击、可改目标)或 301(浏览器缓存、省流量但不可统计)。功能上要有:自定义短码与黑名单、过期时间、访问统计(异步写日志或 Kafka 落地)、防刷与限流、以及缓存穿透保护(布隆过滤器挡住不存在的短码)。瓶颈通常在读:跳转是典型读多写少(读写比可能 1000:1 以上),且要求极低延迟,所以优化重点是缓存命中率与就近接入——多级缓存、CDN/边缘节点直接 302、热点短码本地缓存、长链的预解析,写路径则可以异步化与批量。若确实要应对写入热点(同一个短码被疯狂创建或统计写入),用消息队列削峰与分片计数。
面经 03 / 04 · 工程实践与检索
9. 手撕 LRU 与链表的线程安全改造:LRU 的标准实现是哈希表 + 双向链表:哈希表提供 O(1) 定位,双向链表维护访问顺序,get 命中时把节点移到头部,put 时若容量已满就淘汰尾部节点,新节点插到头部;用带哨兵头尾的链表可以省掉大量边界判断,这也是面试里最容易写错的地方(漏更新前后指针、容量为 1、更新已存在的 key 时忘记移动)。改成线程安全有几种粒度:最简单是给 get/put 整体加 synchronized 或 ReentrantLock,但会串行化所有操作;更好的是用 ConcurrentHashMap + 对链表结构调整加锁,或直接用 ConcurrentLinkedHashMap / Caffeine(W-TinyLFU,读多写少场景命中率更高)。具体要加锁的临界区是「读链表 + 移动节点」和「写哈希表 + 插删链表」这两段整体,不能只锁容器操作——并发下 map.get 与 list.moveToHead 之间的窗口会导致链表断裂或节点丢失。面试里要能说出「锁保护的是不变式(哈希表与链表的一致性),不是单个方法」,并提到分段锁或按 key 分桶降低竞争。
10. ES 与 MySQL 的一致性、embedding 与 RAG 数据过期:ES 通常作为检索副本而不是数据源,一致性策略是「以 MySQL 为准、通过 binlog/Canal 或业务双写异步同步到 ES」,接受秒级延迟并用版本号(乐观锁或 ES 的 version/seq_no)防乱序覆盖,同时要有全量重建与对账任务兜底。单体还是分布式取决于数据量与查询并发:数据量小、查询简单用单节点足够;要做水平扩展就设计好分片数(分片数不可改,只能 reindex)、副本数与路由,避免分片过多导致小文件与查询扇出。embedding 选型要看语言、领域、维度与成本:中文常用 BGE、M3E、m3e-base 或云厂商接口,通用多语言可用 multilingual-e5、text-embedding-3 系列;核心原理是把文本映射到高维向量空间,让语义相近的文本距离近,常见做法是双塔对比学习(正样本拉近、负样本推远),所以它捕捉的是语义相似而不是关键词匹配。RAG 数据过期要靠元数据治理:每块知识带来源、版本、生效时间与失效时间,检索时按时间过滤,更新用增量重建(文档哈希判断变更),过期内容标记归档而不是直接删(便于追溯),对时效敏感的问题在回答里显式标注知识时间或并行做一次动态检索比对。语义相似但实质不同(比如「退款」与「退货」)要靠元数据过滤、rerank 精排与在 prompt 里要求引用原文片段来区分,必要时引入业务标签强制区分。
11. DDD 与「三线程周期抢锁」的设计题:DDD 的核心是用业务语言划分边界:先识别限界上下文(Bounded Context),在每个上下文内定义实体、值对象、聚合(一致性边界)、领域服务与仓储,再通过领域事件与应用服务编排跨上下文协作;它解决的是「业务复杂、模型混乱、团队协作边界不清」的问题,落地手段包括统一语言、聚合根保证事务边界、防腐层隔离外部系统。别把它当成目录规范,小项目硬套只会增加成本。三个线程每 5 分钟抢一次同一把锁的场景,本质是周期性互斥任务,设计要点:用分布式锁(Redis SETNX + 过期时间)或数据库的唯一任务记录保证同一时刻只有一个线程执行;锁必须有过期时间防止持有者宕机死锁,业务可能超过 5 分钟时要续期(看门狗),并让持有者执行完主动释放;抢锁失败要立即放弃而不是自旋,等到下一个周期再抢,避免空转与线程堆积;用「同一时间点统一触发 + 抢锁」还是「各自独立周期」会影响公平性,前者更可控;如果希望三个线程轮流执行,可以加公平锁或令牌轮转;最后,任务本身仍要幂等,因为锁过期、网络分区都可能导致重复执行。
面经 05 · Java 基础与 AI Coding
12. JVM 内存分配、分代原因与 GC 算法:JVM 运行时数据区包括线程私有的程序计数器、虚拟机栈、本地方法栈,和线程共享的堆、方法区(JDK 8 后为元空间,本地内存),另有直接内存。堆按对象的生命周期分代:新生代分 Eden 与两个 Survivor(默认 8:1:1),新对象先在 Eden 分配(大对象直接进老年代,栈上分配与标量替换是 JIT 的优化),Minor GC 后存活对象在 Survivor 间来回复制、年龄到阈值晋升老年代;老年代放长生命周期对象,用 Major/Full GC 回收。分代的目的是让回收算法与对象死亡率匹配:绝大多数对象朝生夕死,新生代用复制算法只需扫描少量存活对象、成本低停顿短;老年代存活率高,用标记-清除或标记-整理避免大量复制。GC 算法各有适用场景:复制(新生代,浪费一半空间但高效)、标记-清除(老年代,CMS 低延迟,碎片化)、标记-整理(老年代,压缩空间但停顿长,Serial Old/Parallel Old)、G1 按 Region 增量回收可预测停顿、ZGC/Shenandoah 并发整理把停顿压到毫秒级适合大堆低延迟。
13. 线程池参数、队列选择与线程安全的内存视角:ThreadPoolExecutor 的核心参数是 corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler。队列满不满由「当前工作线程数与队列容量」共同决定:核心线程满之后任务入队,队列有界且容量已满时才尝试扩到最大线程数,仍失败则走拒绝策略——所以判断依据是队列的 offer 返回 false(有界队列)或剩余容量为零,无界队列永远不会触发扩容与拒绝。有界队列在 Java 里用 ArrayBlockingQueue(数组、容量固定、可选公平锁)和 LinkedBlockingQueue(可指定容量;不指定时近似无界,这是最常见的坑)。用无界队列的后果是 maximumPoolSize 形同虚设、任务无限堆积、内存被耗尽直至 OOM,且任务延迟不可控,表现为「服务没报错但请求全部超时」。线程安全的产生根源要从内存模型看:多线程下每个 CPU 有缓存与写缓冲区,共享变量的写不保证对其他线程立即可见(可见性),非原子操作(i++ 的读改写)在交错执行下会丢更新(原子性),编译器与 CPU 的指令重排会破坏依赖顺序(有序性)。对应解法:synchronized/锁保证互斥与可见性、volatile 保证可见性与禁止特定重排但不保证原子、原子类用 CAS、final 字段的安全发布、以及把共享可变状态尽量改成不可变对象或线程私有。
14. HTTP 协议、Cookie、MySQL 引擎与慢查询:HTTP 是应用层无状态请求-响应协议:请求行(方法、路径、版本)+ 请求头 + 空行 + 请求体;响应行(版本、状态码、原因短语)+ 响应头 + 空行 + 响应体。特征是文本可读、无状态(靠 Cookie/Session/Token 维持状态)、可扩展、支持缓存与连接复用;方法语义上 GET 安全幂等、POST 非幂等、PUT/DELETE 幂等,状态码要能区分 2xx/3xx/4xx/5xx 与典型含义。Cookie 是服务端通过 Set-Cookie 下发、浏览器在后续同域请求里自动携带的小段数据,属性包括 Domain、Path、Expires/Max-Age、HttpOnly、Secure、SameSite——它是 HTTP 无状态之上的补丁,所以安全上要防 CSRF(SameSite + token)与 XSS 窃取(HttpOnly)。MySQL 常用引擎是 InnoDB 与 MyISAM(后者不支持事务与行锁,已基本淘汰)以及 MEMORY;InnoDB 的特征是事务、行级锁、外键、MVCC、崩溃恢复(redo/undo),适合绝大多数 OLTP 场景。数据库里的悲观锁用 SELECT ... FOR UPDATE 或 LOCK IN SHARE MODE 加行锁,乐观锁用 version 字段或状态条件更新(update … where version = n)并在应用层重试。避免慢查询的手段:合理索引与最左前缀、避免索引失效(函数、隐式转换、前导模糊)、覆盖索引减少回表、只查需要的列避免大结果集、分页用游标替代大 offset、避免 JOIN 过多表与子查询、用 EXPLAIN 看执行计划(type、key、rows、Extra 里的 filesort/temporary)、控制单表数据量做归档或分表,并对慢查询日志做定期治理。
15. AI Coding 的流程、Agent 概念与上下文管理:用 AI 写功能通常分几个阶段:需求澄清与任务拆解(把模糊需求变成可验收的条目)→ Plan 模式产出方案(涉及哪些文件、接口与数据结构,先对齐再写)→ 分步实现(小步提交、每步可编译可测试)→ 验证(跑测试、类型检查、自己读 diff、必要时用另一个模型做 review)→ 收尾(补测试、更新文档、清理临时文件)。Plan 模式的价值在于把「写代码」变成「先对齐设计」,避免 AI 一上来就大范围改文件、方向错了才发现;复杂项目里要给它明确的上下文边界(相关文件、约定、禁止改动范围)并要求它先解释方案。验证正确性是关键:不能只看代码「像是对的」,要有可执行的测试、边界用例与人工 diff 审查,特别是并发、错误处理与安全相关改动。Agent 与大模型的关系是:大模型提供理解与生成能力,Agent 是在其外加了工具调用、循环执行、记忆与规划的运行框架,让它能读文件、跑命令、观察结果再修正;Skill 则是把某类任务的做法、脚本与约束封装成可复用的能力包。上下文有限时的处理:分层组织(系统指令与约束常驻、近期对话保原文、久远历史做摘要压缩)、按需检索(用文件检索和向量召回只取相关片段而不是全量塞入)、结构化表达(用清单、schema、状态文件代替长散文),以及把关键结论写进外部持久化文件(如计划与进度文档)让下一轮重新加载,相当于给对话做外部记忆。深度思考与快速思考的区别在于是否在输出前进行长链推理:快速模式直接生成,延迟低、成本低,适合简单问答与格式转换;深度思考模式先生成较长的推理过程再给结论,多步推理、数学与复杂代码任务上更准,但延迟与 token 成本显著更高,工程上要按任务难度分级路由,并对思考模式的输出做长度与超时控制。