蚂蚁集团测试开发岗面经合集:智力题、支付测试与八股
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
面经 01 · 测试开发岗(2026-04-23)
- 项目介绍与相关的八股问答。
- 手撕:最长公共前缀(LeetCode 14)。
面经 02 · 测试开发岗(2026-04-17)
- 智力题:有 5L 和 3L 两个杯子,如何量出 4L?
- 智力题:8 枚硬币中有 1 枚假币且较轻,用天平最少称几次找出?
- 编程题:给定
[1,2,3,4]四个数字,输出所有不含重复数字的三位数。 - 英语朗读:给一段英语材料,朗读。
面经 03 · 测试开发岗(2025-10-10)
- 项目拷打,主要是强化学习相关的内容。
- RL 在大模型中的应用,RL 的了解与现状?
- 实习经历。
面经 04 · 测试开发岗(2025-09-21)
- 手撕题 + 测试用例设计。
- 项目介绍,以及测试中发现过什么问题?
- 扫码支付场景测试。
- 场景:如何防止支付场景下重复扣款?
- 幂等性。
- 场景:一个接口操作多个表,如何确认这些操作在一个事务里、如何检查?
面经 05 · 测试开发岗(2025-09-18)
- 对 base 地的考虑。
- 为什么想干测开而不是开发?
- 为什么不在快手转正?
- 进程、线程、协程分别是什么?都怎么通信?通信有哪些操作?
- 锁有哪些?锁怎么用?
- 三个线程,怎么让它按顺序执行?
- 线程安全怎么实现?
- 线性数据结构有哪些?数组和链表有哪些区别和特性?
- 数组的查询效率?为什么查询效率是 O(1)?
- 栈和队列有哪些区别和使用场景?
- 非线性数据结构有哪些?平衡二叉树的特性、用法?平衡因子是什么?
- 树这种数据结构的查询效率?
- HashMap 了解过吗?哈希表底层结构怎么实现?哈希冲突怎么解决?
- Java 泛型。
- OSI 七层模型?应用层有哪些协议?
- HTTP 协议的具体结构?GET、POST 区别?
- HTTP 和 HTTPS 有什么区别?HTTPS 是怎么加密的?CA 证书有哪些内容、作用是什么?
- 传输层有哪些协议?TCP 三次握手中状态码有哪些、怎么变化?
- 网络层有哪些协议?目前计算机数量远大于 IPv4 数量,为什么每个机器都有 IPv4 地址?是怎么避免 IPv4 地址冲突的?两家的 IPv4 地址一致有影响吗,为什么?
- 数据链路层有哪些协议?ARP 和 RARP 的作用,引申到数据链路层的作用?
- 索引,谈谈理解,底层结构,怎么用?
- 场景题:七日签到累计获奖,怎么设计表结构、怎么设计索引?
- 联合索引和联合唯一索引。
- 数据库事务有什么用?ACID 怎么实现?
- 乐观锁、悲观锁。
- Java 动态代理,哪个框架用得多?
- 什么是 RPC?HTTP 和 RPC 的区别?
面经 06 · 测试开发岗(2025-06-17)
- 手撕:实现
rm -rf path命令(已知file.isFile()、file.delete()、listFiles()三个方法)。 - 追问:针对 rm -rf 功能设计测试用例。
- 场景题:找出 10 万个元素数组中最大的 1000 个数。
- 场景题:列出支付宝转账功能的测试点(系统层级)。
- SpringBoot 的注解(启动类注解、条件注解)。
- Spring 如何管理依赖?
- SpringBoot 依赖注入如何使用(三种注入方式)?
- 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。