现象与背景:并发测试偶现失败,最贵
在测试开发工作中,我们经常会遇到一些令人困惑的失败场景。比如,我们维护的一条 B 端 SaaS 订单链路接口测试仓库,约 1850 个接口用例,CI 上以 @Execution(CONCURRENT) 并行跑,并行度 8。在 2024 年 Q4 三个月里,夜间回归偶现失败 94 次,失败率 4.2%。其中 76 次重跑一次就过,剩下的 18 次要么是环境问题,要么是代码 bug。研发侧因此被拉去复盘了 31 次,每次平均占用 25 分钟。测试侧因此被研发同学私下吐槽"你们的用例不稳定",这在 8 年里我听过最扎心的一句。
关键迷惑点:这些用例单独跑、串行跑,100% 通过。只有并发 8 时偶发挂,而且挂的永远是"造数 → 调接口 → 断言"里那一步断言——读出来的数据是"上一个人的世界"。
根因:InnoDB REPEATABLE READ 的快照读,不是你以为的"每次读都是最新的"
MySQL InnoDB 默认隔离级别是 REPEATABLE READ,它不是"每次 SELECT 都拿最新提交数据",而是:
- 事务里第一次执行一致性读(普通 SELECT,非
FOR UPDATE)时,InnoDB 会构造一个 Read View; - 这个 Read View 记录"当前所有活跃事务 ID 列表 + 最大分配事务 ID + 最小活跃事务 ID";
- 后续同一事务里所有普通 SELECT 复用同一个 Read View——即使期间别的事务提交了新数据,本事务也"看不见";
- 只有
FOR UPDATE/LOCK IN SHARE MODE/UPDATE/DELETE这种当前读(current read)才会看到最新已提交版本。
这带来的接口测试副作用非常具体:
- 用例 A:
BEGIN → INSERT 一条 order → 调 /order/query → SELECT 断言 - 用例 B:在 A 的事务打开之后、SELECT 之前,
INSERT 一条 order,但因为 A 的事务快照读看不见 B 的插入,导致断言失败。
具体做法:4 种修法与 CI 配置清单
1. 使用 FOR UPDATE 或 LOCK IN SHARE MODE 进行当前读
在需要读取最新数据的场景中,使用 FOR UPDATE 或 LOCK IN SHARE MODE,让事务看到其他事务提交的数据。
2. 使用 SELECT ... FOR UPDATE 或 SELECT ... LOCK IN SHARE MODE 来确保数据一致性
在需要确保数据一致性时,使用 SELECT ... FOR UPDATE 或 SELECT ... LOCK IN SHARE MODE,这样可以避免快照读带来的问题。
3. 在测试环境中配置 MySQL 的隔离级别
在测试环境中,可以配置 MySQL 的隔离级别为 READ COMMITTED,这样可以避免快照读带来的问题。
4. 使用事务管理工具
使用事务管理工具,如 JTA(Java Transaction API),可以更好地管理事务,避免快照读带来的问题。
踩过的坑或反直觉之处
- 在测试环境中,使用
SELECT语句时,不要假设每次读取的数据都是最新的,尤其是在并发测试中。 - 使用
FOR UPDATE或LOCK IN SHARE MODE时,要注意事务的管理,避免死锁。 - 在配置 MySQL 隔离级别时,要根据具体场景选择合适的隔离级别。
结论与可复用清单
- 在测试开发中,要注意并发测试的场景,避免快照读带来的问题。
- 在需要读取最新数据的场景中,使用
FOR UPDATE或LOCK IN SHARE MODE。 - 在测试环境中配置 MySQL 的隔离级别为
READ COMMITTED。 - 使用事务管理工具,如 JTA,更好地管理事务。
这些经验可以帮助我们在测试开发中更好地管理并发测试,避免快照读带来的问题,提高测试的稳定性和可靠性。