面灵AI→

蚂蚁集团测试开发岗面经合集:智力题、支付测试与八股

时间
2026-09
来源
牛客网

《面试题目》

面经 01 · 测试开发岗(2026-04-23)

  1. 项目介绍与相关的八股问答。
  2. 手撕:最长公共前缀(LeetCode 14)。

面经 02 · 测试开发岗(2026-04-17)

  1. 智力题:有 5L 和 3L 两个杯子,如何量出 4L?
  2. 智力题:8 枚硬币中有 1 枚假币且较轻,用天平最少称几次找出?
  3. 编程题:给定 [1,2,3,4] 四个数字,输出所有不含重复数字的三位数。
  4. 英语朗读:给一段英语材料,朗读。

面经 03 · 测试开发岗(2025-10-10)

  1. 项目拷打,主要是强化学习相关的内容。
  2. RL 在大模型中的应用,RL 的了解与现状?
  3. 实习经历。

面经 04 · 测试开发岗(2025-09-21)

  1. 手撕题 + 测试用例设计。
  2. 项目介绍,以及测试中发现过什么问题?
  3. 扫码支付场景测试。
  4. 场景:如何防止支付场景下重复扣款?
  5. 幂等性。
  6. 场景:一个接口操作多个表,如何确认这些操作在一个事务里、如何检查?

面经 05 · 测试开发岗(2025-09-18)

  1. 对 base 地的考虑。
  2. 为什么想干测开而不是开发?
  3. 为什么不在快手转正?
  4. 进程、线程、协程分别是什么?都怎么通信?通信有哪些操作?
  5. 锁有哪些?锁怎么用?
  6. 三个线程,怎么让它按顺序执行?
  7. 线程安全怎么实现?
  8. 线性数据结构有哪些?数组和链表有哪些区别和特性?
  9. 数组的查询效率?为什么查询效率是 O(1)?
  10. 栈和队列有哪些区别和使用场景?
  11. 非线性数据结构有哪些?平衡二叉树的特性、用法?平衡因子是什么?
  12. 树这种数据结构的查询效率?
  13. HashMap 了解过吗?哈希表底层结构怎么实现?哈希冲突怎么解决?
  14. Java 泛型。
  15. OSI 七层模型?应用层有哪些协议?
  16. HTTP 协议的具体结构?GET、POST 区别?
  17. HTTP 和 HTTPS 有什么区别?HTTPS 是怎么加密的?CA 证书有哪些内容、作用是什么?
  18. 传输层有哪些协议?TCP 三次握手中状态码有哪些、怎么变化?
  19. 网络层有哪些协议?目前计算机数量远大于 IPv4 数量,为什么每个机器都有 IPv4 地址?是怎么避免 IPv4 地址冲突的?两家的 IPv4 地址一致有影响吗,为什么?
  20. 数据链路层有哪些协议?ARP 和 RARP 的作用,引申到数据链路层的作用?
  21. 索引,谈谈理解,底层结构,怎么用?
  22. 场景题:七日签到累计获奖,怎么设计表结构、怎么设计索引?
  23. 联合索引和联合唯一索引。
  24. 数据库事务有什么用?ACID 怎么实现?
  25. 乐观锁、悲观锁。
  26. Java 动态代理,哪个框架用得多?
  27. 什么是 RPC?HTTP 和 RPC 的区别?

面经 06 · 测试开发岗(2025-06-17)

  1. 手撕:实现 rm -rf path 命令(已知 file.isFile()、file.delete()、listFiles() 三个方法)。
  2. 追问:针对 rm -rf 功能设计测试用例。
  3. 场景题:找出 10 万个元素数组中最大的 1000 个数。
  4. 场景题:列出支付宝转账功能的测试点(系统层级)。
  5. SpringBoot 的注解(启动类注解、条件注解)。
  6. Spring 如何管理依赖?
  7. SpringBoot 依赖注入如何使用(三种注入方式)?
  8. Spring 如何管理事务?

《参考解析》

1. 两道智力题:量水与找假币:量 4L 的通用套路是「用容量差反复制造差值」:装满 5L 杯,倒入 3L 杯至满,5L 杯剩 2L;倒空 3L 杯,把这 2L 倒进去;再装满 5L 杯,往 3L 杯里补满(只倒出 1L),5L 杯剩下的正好是 4L。它成立的本质是 4 = 5×2 − 3×2,即目标值是两个容量的整数线性组合,所以能配出的量一定是 gcd(5,3)=1 的倍数;反过来若目标是 gcd 的非倍数就无解。(a,b) 状态空间只有 4×4 种,BFS 可以严格求出最少步数,面试时先说思路再补这个推广通常就够。8 枚硬币找较轻者在 2 次以内必然能定位:第一次分成 3、3、2,称 3 对 3——若不平,假币在轻的那 3 枚里,第二次取其中 2 枚各放一边,轻的一枚即假币,若平衡则第三枚是假币;若第一次平衡,假币就在剩下的 2 枚里,第二次一比即出。为什么 2 次是最优?每次天平有左重、右重、平衡三种结果,两次最多区分 3²=9 种情况,而 8 枚硬币加「无假币」共 9 种可能性,刚好卡满信息量下界,1 次只能区分 3 种所以不可能。

2. 两道手撕:最长公共前缀与不含重复数字的三位数:最长公共前缀用纵向扫描最省事:以第一个字符串为基准,逐列比较所有字符串的第 i 个字符,一旦有串已到末尾或字符不等,答案就是前 i 个字符;时间复杂度 O(n·m),空间 O(1)。要主动交代边界:数组为空返回空串,只有一个字符串返回它本身,务必先判长度避免越界;横向扫描(两两求 LCP 再与下一个求交)也可以,而且前缀长度单调不增,能提前剪枝,但实现细节更多。三位数那道题,[1,2,3,4] 的结果数是 4×3×2=24,不是 4³=64,因为每位用过的数字不能再出现。写法用回溯最标准:path 记录已选数字、used 数组标记占用,深度到 3 就输出并回溯,天然不会有重复。要能主动说出两个变体:如果候选集合里含 0,百位不能取 0,需要额外剪枝;如果集合里有重复数字(如 [1,1,2]),必须排序后在同层去重,否则结果重复。

3. rm -rf 的实现与测试用例设计:递归实现很直接:if (file.isFile()) { file.delete(); } else { File[] children = file.listFiles(); if (children != null) { for (File c : children) remove(c); } file.delete(); }。四个必须点名的细节:listFiles() 在目录不存在或无权限时返回 null,不判空就是 NPE;必须「先删子、再删父」,否则非空目录删不掉;delete() 失败只返回 false、不抛异常,静默忽略会造成「报成功但文件还在」;绝对不能用 Files.walk 之类默认跟随符号链接的接口,否则会把链接指向的整棵外部目录删掉,应显式判断 Files.isSymbolicLink 并只删链接本身。生产实现还要拒绝 .. 与越界路径、加递归深度或改迭代防栈溢出、把删除失败的路径汇总上报。测试用例按等价类与边界铺开:单个文件、空目录、多层嵌套目录、只读文件、无权限目录、文件被其他进程占用、路径不存在、路径含空格/中文/超长字符、符号链接指向目录、参数为 / 或盘符根(必须被安全拦截)、并发删除同一目录、磁盘满或 IO 错误;再补非功能项——删除是否可逆、有无二次确认、大目录的耗时与内存、日志是否记录被删对象。

4. 10 万个元素里取最大的 1000 个:标准答案是容量为 1000 的小顶堆:遍历数组,堆未满就入堆,满了就比较当前元素与堆顶,比堆顶大就替换堆顶并下沉,遍历结束时堆里就是最大的 1000 个。复杂度 O(n log k):n=10 万、log₂1000≈10,大约百万次比较,内存只要 O(k),而且支持「数据分批到达」的流式场景,这是它比排序强的关键理由。对比方案要能说出来:全排序 O(n log n) 最简单,10 万条实际也就几十毫秒,工程上常够用但体现不出取舍;快速选择(基于 partition 找第 k 大)平均 O(n)、常数最小,代价是原地打乱数组且最坏 O(n²)(随机选 pivot 可把概率压到极低),适合一次性离线批处理;数据再大几个量级就分块各自取 TopK 再归并。细节追问点:k 大于 n 时直接返回全量;元素有重复时堆要允许重复;求「最小的 1000 个」就把小顶堆换成大顶堆;Java 里 PriorityQueue 默认就是小顶堆。

5. 扫码支付与转账功能的测试点怎么列:按维度铺比零散罗列更能体现体系。功能层:正常支付成功、余额不足、超时未支付自动关单、重复提交、用户主动取消、全额与部分退款、优惠券红包叠加、免密额度与单笔/日累计限额、不同卡种(储蓄卡/信用卡/境外卡)、金额边界(0、负数、超限、最小币种精度、大额分账)。交互与兼容层:扫码识别(模糊、反光、破损、非本平台码、远处小码)、前后台切换、杀进程后恢复、网络切换(4G/WiFi 互切、弱网、断网重连)、多机型多系统版本、字体与深色模式。数据一致性:订单状态、支付流水、账户余额、渠道回执、对账文件五方一致,重复回调不重复入账,跨天对账差异能自动识别。安全:篡改金额或商户号、重放请求、越权查询他人订单、敏感信息脱敏、二维码被替换。异常与性能:渠道超时但实际成功、扣款成功而订单未更新、单渠道故障切备用通道、高峰期 TPS 与响应时间、幂等压测。系统层级要明确覆盖客户端、网关、支付服务、账务核心、渠道对接、对账清算这六层,每一层分别设计正常流、异常流与补偿流。

6. 幂等与支付场景防重复扣款:幂等的定义是同一请求执行多次与执行一次的结果一致,支付场景的实现手段按可靠性排序:① 业务唯一键(订单号/请求流水号)在账务侧建唯一索引,插入冲突即判定为重复请求,直接返回首次的处理结果;② 状态机 + 乐观锁,update ... set status='已支付' where order_no=? and status='待支付',靠影响行数判断谁抢到了这次状态跃迁;③ 分布式锁(Redis SETNX+过期时间或 Redisson 看门狗)把同一订单的并发请求串行化;④ 单独的幂等去重表记录已处理请求;⑤ 数据库唯一约束是最后一道防线,比任何应用层锁都可靠,因为锁会因超时或进程崩溃失效。重复扣款的真实来源是用户连点、客户端超时重试、网关重发、渠道回调重放,所以正确做法是扣款请求必须携带全局唯一业务单号,账务侧以它收敛所有入口;当渠道已扣款而我方超时就绝不能重试扣款,只能走主动查单与对账补偿。测试要专门构造并发重复提交、回调重放、超时后重试三类场景,断言「只扣一次、只入账一次」。

7. 一个接口操作多表,如何确认在同一事务、如何检查:先确认代码路径:多表操作是否都在同一个 @Transactional 方法里;事务传播行为是 REQUIRED(加入已有事务)还是 REQUIRES_NEW(新开事务,会导致外层回滚而它已提交);有没有同类内部方法直接自调用导致代理失效、事务根本没生效;异常是否被 catch 吞掉——默认只在遇到 RuntimeException 时回滚,受检异常必须显式配 rollbackFor;多个操作是否走了同一个数据源,跨数据源不可能共用一个本地事务。检查手段要能落地:① 单元/集成测试里用 TransactionSynchronizationManager.isActualTransactionActive() 断言事务确实开启;② 在方法中途抛异常,验证多张表的数据全部回滚,而不是只回滚了第一张;③ 另开一个连接查询 information_schema.innodb_trx,看是否只有一个活跃事务、以及它的开始时间是否覆盖整个过程;④ 打印各表操作拿到的 Connection 哈希是否一致;⑤ 打开 MySQL general log 看 commit/rollback 的边界在哪儿;⑥ 压测并发场景,确认外部读不到「只写了一部分」的中间态。反向的坑也要提:事务里别做 RPC、发消息、写文件,长事务会长时间持锁;跨系统一致性靠本地消息表或事务消息,而不是把远程调用塞进事务。

8. 进程、线程、协程与各自的通信方式:进程是资源分配单位,拥有独立虚拟地址空间、文件描述符表、信号处理表;线程是 CPU 调度单位,同进程内的线程共享地址空间、堆、fd 表与全局变量,各自独享栈、寄存器上下文、TLS 和 errno;协程是用户态调度单元,由程序自己在合适的点让出,切换不陷入内核、几乎没有上下文切换成本,一个线程可以承载成千上万个,特别适合 IO 密集场景。通信方式要分开答:进程之间用管道/FIFO、消息队列、共享内存(最快,但要配信号量或 futex 做同步)、信号、信号量、域套接字与网络套接字;线程之间天然共享内存,靠互斥量、条件变量、读写锁、原子变量、信号量协调,需要隔离时用线程局部存储;协程之间通常用 channel 或队列传数据,因为同一线程内不真并行,单线程协程间的传递往往不需要加锁。被追问「通信有哪些操作」时按原语讲:创建、发送/接收(阻塞与非阻塞)、同步等待与超时、关闭与 EOF 语义、缓冲区大小与背压处理。最后补成本对比:进程切换要换页表并影响 TLB,线程切换只换上下文,协程切换只换用户栈——这也解释了高并发服务为什么选「少量线程 + 大量协程/事件循环」。

9. 三个线程按顺序执行的三种写法:第一种是单锁加共享状态:所有线程持有同一把锁和同一个 state 变量,每个线程在 while (state != myTurn) lock.wait(); 处等待,执行完自己的任务后修改 state 并 notifyAll()。两个必须注意的细节是谓词判断要用 while 而不是 if(防虚假唤醒与唤醒后条件又被抢走),以及状态修改和 notify 必须在持锁状态下完成。第二种是锁链:给每个线程一把锁,T1 执行完 notify T2 持有的锁,T2 完成后再 notify T3,形成 T1 → T2 → T3 的唤醒链,也可以首尾相接做成循环打印。第三种也是工程上最清晰的:用 Semaphore,s1(1)、s2(0)、s3(0),T1 先 acquire(s1) 完成后 release(s2),T2 完成后 release(s3),依此类推,信号量自带状态不需要额外的条件判断,出错概率最低。如果只是想在测试里把任务串起来,用单线程池按序提交或 CompletableFuture.thenRun 更省事;但面试问的是原理,要能说出 wait 与 notify 必须同锁、CountDownLatch 是一次性的而 CyclicBarrier 可复用这类区别。

10. 线性与非线性数据结构的特性与查询效率:数组内存连续,靠「基地址 + 下标 × 元素大小」直接算出地址,所以随机访问是 O(1),但插入删除要搬移后续元素,是 O(n);链表节点离散存放,按序访问 O(n)(因为无法跳跃),插入删除本身只需改指针是 O(1),但前提是已经拿到前驱节点,且每个节点多一个指针开销、缓存局部性差。实践结论要反直觉地说出来:绝大多数场景下动态数组比链表更快,因为 CPU 预取与缓存行命中带来的收益压过了理论复杂度。线性结构还有栈(后进先出,用于函数调用栈、括号匹配、表达式求值、DFS)、队列(先进先出,用于任务调度、BFS、生产者消费者缓冲)、双端队列与循环队列(音视频与网络收发缓冲)。非线性结构主要看树与图:普通二叉搜索树的查询期望 O(log n),但有序插入会退化成链表变成 O(n);AVL 树与红黑树通过旋转保持平衡,查询稳定 O(log n),其中红黑树插入删除时旋转次数更少(最多 3 次),更适合频繁改动的场景,Java 的 TreeMap、C++ 的 map 底层都是红黑树。平衡因子是 AVL 的概念,定义为左右子树高度之差,AVL 要求每个节点平衡因子的绝对值不超过 1,插入或删除后失衡就要按 LL/RR/LR/RL 四种情况旋转修正;它和哈希表没有关系,这是常见的混淆点。

11. HashMap 的底层实现与哈希冲突处理:JDK 8 的 HashMap 是「数组 + 链表 + 红黑树」。数组长度默认 16 且始终是 2 的幂,这样可以用 (n - 1) & hash 代替取模来定位下标,速度更快。put 的流程是:先算扰动后的哈希(高 16 位与低 16 位异或,让高位也参与定位,减少碰撞)→ 定位桶,为空就直接放入 → 否则比较哈希与 equals,相等则覆盖旧值,不等则追加到链表或红黑树尾部 → 元素总数超过阈值(容量 × 负载因子 0.75)就扩容一倍。链表长度达到 8 且数组长度不小于 64 时转成红黑树,节点数退化到 6 以下再转回链表,两个阈值不同是为了避免在边界反复转换。扩容时 JDK 8 用「原位置」与「原位置 + 旧容量」两条链拆分节点,省掉重新计算哈希的开销。哈希冲突的解决办法要能分两类:拉链法(HashMap、ConcurrentHashMap)和开放寻址(ThreadLocalMap 的线性探测、Python dict 的伪随机探测)。冲突严重时的表现是查找从 O(1) 退化到 O(n),诱因是哈希函数质量差、初始容量太小导致负载因子过高反复 rehash、键有明显规律、或攻击者构造同哈希键;对策是按预期规模 new HashMap<>(expected / 0.75 + 1) 一次定容、键对象正确重写 hashCode 与 equals(只重写一个会造成「放进去取不出来」),安全敏感场景换用带随机种子的哈希。最后要主动说 HashMap 线程不安全,并发 put 可能丢数据,多线程应使用 ConcurrentHashMap(CAS 加锁单个桶头,锁粒度更细)。

12. 网络分层、HTTP 与 HTTPS、TCP 握手状态、IPv4 地址复用:HTTP 报文结构是「请求行/状态行 + 头部字段 + 空行 + 消息体」,头部以 CRLF 分隔、空行标志头部结束,消息体的边界由 Content-Length 或 Transfer-Encoding: chunked 决定。GET 与 POST 的差别要从语义答:GET 安全、幂等、用于取资源,参数在 URL 上、可被缓存与收藏;POST 用于提交、有副作用、不保证幂等、参数在请求体;但两者在传输层没有区别,GET 也能带 body,真正的安全性由 HTTPS 提供而不是方法本身。HTTPS 是 HTTP over TLS:握手阶段用非对称算法(RSA 密钥交换或 ECDHE,后者具备前向安全)完成身份认证与密钥协商,之后所有数据用协商出的对称密钥加密(AES-GCM、ChaCha20-Poly1305),对称加密只用于数据传输是因为它快几个数量级,同时 AEAD 还保证了完整性。CA 证书里包含版本、序列号、签名算法、签发者、有效期、主体(域名与组织)、公钥、扩展项(SAN 域名列表、KeyUsage、CRL/OCSP 地址)以及 CA 用自己私钥对以上内容的签名;它解决的是「域名与公钥的绑定」问题,浏览器沿证书链逐级验签到内置根证书,再校验域名匹配、有效期和吊销状态,从而抵御中间人。TCP 三次握手的状态变迁:客户端 CLOSED → 发 SYN → SYN_SENT → 收到 SYN+ACK 并回 ACK → ESTABLISHED;服务端 CLOSED → LISTEN → 收到 SYN 回 SYN+ACK 进入 SYN_RCVD → 收到 ACK 进入 ESTABLISHED。关闭时主动方 FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT(等 2MSL 再回 CLOSED),被动方 CLOSE_WAIT → LAST_ACK → CLOSED。IPv4 地址看起来「每台机器都有」靠的是分层复用:私有地址段(10/8、172.16/12、192.168/16)可以在无数内网里重复使用,出公网时经 NAT 把「内网地址 + 端口」映射成公网地址 + 端口,加上 DHCP 动态分配与 CIDR 子网划分;避免冲突靠的是同一网段内 DHCP 租约分配与 ARP 探测、以及路由隔离不同网段。两家公司内网地址完全相同不会有影响,因为它们的流量在各自网关后就被 NAT 转换,公网上只暴露各自的公网地址,这也正是 NAT 拖延了 IPv6 普及的原因。

13. MySQL 索引与七日签到场景的表结构与索引设计:索引的本质是把「全表扫描」换成「按序定位」,InnoDB 用 B+ 树:非叶子节点只存键、扇出极大所以树高通常 3 层就能覆盖千万行,叶子节点按序双向链表相连,天然支持范围查询与排序,这三点是它击败 B 树和红黑树的原因;聚簇索引的叶子存整行数据,二级索引的叶子存主键值,因此二级索引查非索引列要「回表」,覆盖索引可以避免这次回表。七日签到累计获奖的设计思路:不要用「一个用户一行、七天七个字段」这种定宽结构,那会让「查询某天是否签过」「统计连续天数」都很难写。常用方案是明细表 sign_record(user_id, sign_date, biz_id),在 (user_id, sign_date) 上建唯一索引——唯一约束本身就是防重复签到的幂等保证;按周期统计领奖时再按 (user_id, period_start) 建联合索引。若签到量极大,可以按用户或时间分表,并把「连续签到天数、本周已签天数」这类派生态冗余到用户维度表,用签到成功后的同事务更新维护,读路径就不用聚合。联合索引要讲清最左前缀:(a,b,c) 能服务 a、a,b、a,b,c 的等值与范围组合,跳列会让后面的列用不上索引;联合唯一索引是在多列组合上保唯一,(user_id, sign_date) 就属于这一类,它和「给每一列各建一个唯一索引」语义完全不同,后者会错误地限制单列取值。

14. 事务 ACID、隔离级别与乐观锁悲观锁:事务的价值是把一组操作打包成「要么全成功、要么全失败」的原子单元,同时提供一致性视图与并发隔离。ACID 的实现要落到具体机制:原子性靠 undo log,回滚时用反向操作把数据改回去;持久性靠 redo log(WAL 先写日志再刷盘),崩溃后按 redo 前滚、按 undo 回滚未提交事务;隔离性靠锁与 MVCC——读已提交和可重复读都用 MVCC 生成一致性快照(ReadView),区别在快照的生成时机(每条语句 vs 事务首次读),可重复读下 InnoDB 还用间隙锁(Next-Key Lock)阻止幻读;一致性是前三者加上业务约束共同保证的目标。隔离级别与并发问题要成对记:读未提交会脏读,读已提交解决脏读但有不可重复读,可重复读解决不可重复读(MySQL 默认,靠间隙锁基本解决幻读),串行化最安全但并发最差。乐观锁与悲观锁是两种并发策略:悲观锁假设冲突必然发生,用 select ... for update 或 lock in share mode 在事务里先锁住行,适合写冲突激烈、临界区短的场景,代价是持锁期间阻塞与死锁风险;乐观锁假设冲突少,用版本号或 where status = 旧值 做 CAS 更新,靠影响行数为 0 判断失败再重试,适合读多写少、冲突概率低的场景,代价是失败重试的开销与 ABA 问题(用版本号而非状态判断可避免)。要注意 for update 必须走索引,否则会升级成锁全表。

15. Spring 依赖注入、事务管理、动态代理与 RPC:依赖注入的三种方式是构造器注入、setter 注入和字段注入;构造器注入能用 final 保证依赖不可变、便于单测、也能在启动时就暴露循环依赖,所以是官方推荐,Spring 4.3 之后单构造器无需 @Autowired;字段注入写起来最省事但隐藏依赖、脱离容器无法实例化,不推荐在正式代码里用。Spring 管理依赖靠 IoC 容器:扫描到 Bean 定义后实例化、按类型(必要时按 @Qualifier 或名称)注入、执行 BeanPostProcessor 与初始化回调,三级缓存(singletonObjects、earlySingletonObjects、singletonFactories)用来在需要时提前暴露半成品对象以打破构造器之外的循环依赖。事务管理本质是 AOP:@Transactional 由代理对象在方法前后开启/提交/回滚事务,所以同类内部自调用会绕过代理导致事务失效,异步线程里也拿不到事务上下文;@Transactional 只对 RuntimeException 默认回滚,受检异常要配 rollbackFor,传播行为决定是加入现有事务还是新开一个。动态代理有两种实现:JDK 动态代理要求目标类实现接口,基于 InvocationHandler 生成接口的兄弟类;CGLIB 通过继承生成子类,能代理没有接口的类,Spring 默认对接口用 JDK、对类用 CGLIB(Boot 2.x 起默认强制 CGLIB)。它被 AOP 事务、缓存(@Cacheable)、异步、日志埋点大量使用,MyBatis 的 Mapper 接口也是靠动态代理在运行时生成实现。RPC 的目标是「像调用本地方法一样调用远程方法」,框架负责序列化、网络传输、服务发现、负载均衡与超时重试;HTTP 与 RPC 的区别在于:HTTP 是应用层协议,通用、可读、天然跨语言、易被网关与浏览器理解,但文本头部冗余、每次调用语义靠 URL 与状态码约定;RPC 通常是二进制协议(Dubbo、gRPC)配 IDL 生成强类型桩代码,序列化更紧凑、性能更高、契约更严格,代价是耦合框架与 IDL,调试不如 HTTP 直观。所以对外提供能力多用 HTTP/JSON,对内高频服务间调用多用 RPC。