MySQL死锁测试与锁粒度优化实践:从复现到性能提升
1. 项目背景为什么要专门对死锁做一轮测试先交代一下这次工作的起因。我们这边有一套核心订单系统底层是MySQL InnoDB存储引擎业务高峰期经常出现两类问题一是偶发的“事务死锁”报错二是某个高频查询在并发上来之后响应时间断崖式下跌。排查日志时发现死锁报错集中在几个特定的更新路径上而慢查询的瓶颈则出在一张大表的行锁竞争上。DBA团队和开发团队各自处理过几次但都是“头痛医头”式的临时解法。为了彻底摸清死锁的产生规律并且验证“调整锁粒度”这条优化路线是否有效我设计和执行了一轮专门的死锁测试与锁粒度优化验证测试也就是标题里写的这件事。这篇报告不是纯理论分析而是把完整的测试方案、执行过程、问题排查和优化结论都整理出来。如果你是后端开发、DBA或者负责数据库性能调优的工程师这里面大部分内容可以直接参考复现。如果你刚接触数据库事务和锁的概念也能从测试设计的思路里理解“锁粒度”到底是怎么影响并发和死锁的。2. 测试环境与工具选型2.1 环境配置先把测试环境列清楚因为锁的行为跟数据库版本、隔离级别、存储引擎都有直接关系环境不同结论可能完全不同。数据库MySQL 8.0.32InnoDB存储引擎隔离级别REPEATABLE READMySQL默认操作系统Ubuntu 22.048核16G测试工具sysbench 1.0.20 自研Java多线程模拟程序 MySQL performance_schema数据规模单表500万行模拟真实业务数据分布选择MySQL 8.0而不是5.7主要是想用最新的performance_schema和data_locking表来做锁等待分析方便拿到更细粒度的锁信息。隔离级别选REPEATABLE READ是因为很多业务系统默认就是这个级别测试结果对大多数人的参考价值更大。2.2 工具选择与理由sysbench是压测工具里比较主流的选择它的oltp_read_write.lua脚本可以模拟高并发读写。但纯用sysbench有个问题——它的SQL模式比较固定不太好构造特定场景下的死锁。所以我额外写了一个Java多线程模拟程序用JDBC直连MySQL每个线程执行预先定义的“事务脚本”这样可以精准控制加锁顺序和事务边界把死锁复现的成功率提高到可控范围。performance_schema在这一轮测试里帮了大忙。通过data_locks和data_lock_waits两张表可以实时看到每个事务持有哪把锁、在等哪把锁定位死锁的基本盘信息。另外innodb_print_all_deadlocks参数打开之后InnoDB会把所有死锁信息写入错误日志而只默认记录最近一条这对批量测试后的死锁分析很重要。在开始之前建议先把innodb_print_all_deadlocksON打开否则你跑完几十轮压测最后只能看到最后一条死锁信息分析样本量不够说服力会大打折扣。3. 死锁测试方案设计3.1 死锁产生的核心条件拆解死锁的官方定义是“两个及以上的事务互相持有对方需要的锁并且都在等待对方释放”听起来简单但实际构造起来需要满足几个条件多个事务并发执行并且存在交叉加锁。每把锁被一个事务持有同时被另一个事务请求。所有事务都处于阻塞状态没有外力干预时不会自动解除。最经典的死锁场景就是“AB-BA问题”事务T1先锁A再锁B事务T2先锁B再锁A当T1持有A等待BT2持有B等待A时就形成了死锁但实际业务里的死锁往往不是这么对称可能是三个事务交叉等待或者一条UPDATE语句涉及多行记录的加锁顺序不一致导致的。所以测试方案里我设计了三种常见模式单表多行更新死锁、两表交叉更新死锁、插入唯一键冲突死锁。3.2 测试用例设计这一轮我设计了6个测试用例分成三组。第一组是基线测试不做任何优化直接跑高并发随机更新观察死锁发生频率和TPS、延迟的基线数据。第二组是定向复现测试针对业务中实际出现死锁的SQL路径构造特定的并发场景。这组用例的目标很明确复现线上问题确认死锁的“触发路径”。第三组是锁粒度对比测试同一业务逻辑分别用行锁默认、表锁LOCK TABLES模拟、间隙锁锁定范围查询三种方式实现对比并发性能差异。用例编号场景描述预期结果T01高并发随机更新单表不同行死锁低概率出现T02两事务交叉更新A、B两表定向复现死锁T03高并发插入相同唯一键大量死锁或锁等待超时T04同一行并发UPDATE和SELECT FOR UPDATE锁等待时间上升T05范围查询带条件更新产生间隙锁死锁概率上升T06大表随机主键更新行锁压测TPS基线每组用例跑3轮每轮5分钟记录死锁次数、TPS、平均延迟、P95延迟、锁等待次数等指标。3.3 Java模拟程序的设计细节Java程序的核心逻辑是每个线程按预置的“事务脚本”执行SQL序列执行完一个事务后随机休眠0-50ms再开始下一个事务模拟真实业务的思考时间。以下是T02用例的核心代码简化版public class DeadlockSimulator implements Runnable { private final DataSource ds; private final int threadId; private final boolean reverseOrder; public DeadlockSimulator(DataSource ds, int threadId, boolean reverseOrder) { this.ds ds; this.threadId threadId; this.reverseOrder reverseOrder; } Override public void run() { try (Connection conn ds.getConnection()) { // 开启事务 conn.setAutoCommit(false); // 模拟业务更新表A和表B String sqlA UPDATE accounts SET balance balance - 10 WHERE id 1; String sqlB UPDATE accounts SET balance balance 10 WHERE id 2; if (reverseOrder) { conn.createStatement().executeUpdate(sqlB); Thread.sleep(50); // 放大死锁窗口 conn.createStatement().executeUpdate(sqlA); } else { conn.createStatement().executeUpdate(sqlA); Thread.sleep(50); conn.createStatement().executeUpdate(sqlB); } conn.commit(); } catch (Exception e) { // 捕获死锁异常 if (e.getMessage().contains(Deadlock)) { System.out.println(Deadlock occurred in thread threadId); } } } }这里Thread.sleep(50)是关键。真实业务里两个事务到达的时间差可能很小死锁窗口极短不容易复现。人为加一个50ms的延迟可以稳定放大“持有锁等待下一把锁”的窗口期让死锁稳定复现。这个技巧在测试场景里很实用但在线上绝对不能这么干。4. 测试执行与数据采集4.1 基线测试结果先跑T01和T06建立基线。T06单行主键随机更新的场景60%的更新操作集中在10%的热点行上这个分布是模拟真实业务的数据倾斜现象。三轮测试的结果TPS均值4280左右P95延迟212ms死锁次数3轮共7次全部是热点行竞争导致的锁等待超时后被InnoDB检测机制判定为死锁死锁次数看起来不高但注意一个细节InnoDB的死锁检测是主动触发的innodb_deadlock_detect参数控制默认ON。当大量事务同时等待同一行锁时InnoDB每秒钟会做一次死锁检测循环如果发现等待关系成环就会回滚代价较小的事务避免无限阻塞。所以“死锁次数少”不等于“没有锁竞争”只是系统通过回滚强行打断了死锁链条。T01的随机更新测试结果更有意思死锁率极低三轮总共才出现1次死锁。原因很简单随机更新500万行里的不同行事务之间很少形成锁资源交叉等待死锁的天然概率就低。这也验证了死锁不是数据库的“常态故障”而是特定SQL访问模式下的“条件触发”问题。4.2 定向复现测试结果T02是定向复现交叉更新两表的场景也是这次测试中死锁率最高的用例。两个事务T1和T2T1先更新accounts表id1的行再更新id2的行T2全部反过来。跑满5分钟死锁次数达到了57次也就是平均5秒就有一个死锁发生。这个频率放到线上业务方肯定能感知到——每次死锁都会有一个事务被回滚对应的用户请求会收到“事务冲突请重试”的报错。T03的插入相同唯一键场景死锁次数反而没那么高只有12次。原因是InnoDB对唯一键冲突有特殊的处理机制——当插入发生唯一键冲突时会立刻对已存在的记录加S锁这本身就容易引发死锁但不是每次都触发跟当前索引页的状态有关。T05范围查询更新场景也值得注意死锁次数达到了23次明显高于单行更新。这是因为REPEATABLE READ隔离级别下范围查询会对命中的索引范围加间隙锁Gap Lock间隙锁与间隙锁之间在特定条件下会互相阻塞形成死锁链条。4.3 锁信息采集测试过程中我每隔2秒抓一次performance_schema的锁快照SELECT OBJECT_NAME AS table_name, INDEX_NAME, LOCK_TYPE, LOCK_MODE, LOCK_STATUS, COUNT(*) AS lock_count FROM performance_schema.data_locks GROUP BY OBJECT_NAME, INDEX_NAME, LOCK_TYPE, LOCK_MODE, LOCK_STATUS;这个查询可以实时看到当前实例上的锁分布。测试T05时我观察到LOCK_MODEX,GAP的锁数量迅速攀升高峰期同时存在超过200个间隙锁这是导致并发下降的直接原因。后续做锁粒度优化时这个数据是判断优化效果的重要指标。5. 死锁根因分析与锁粒度优化方案5.1 死锁根因分析把所有测试数据汇总后我用几个维度对死锁进行了分类死锁类型次数占比根因典型场景交叉更新死锁41%事务加锁顺序不一致多个业务模块更新同一组表但顺序不同插入意向锁死锁9%唯一键冲突S锁等待并发插入相同主键或唯一索引值间隙锁死锁27%范围查询产生间隙锁互相阻塞REPEATABLE READ下的范围UPDATE热点行竞争23%过高的行锁竞争导致等待图成环数据倾斜、热点账户更新交叉更新死锁占比最高说明线上最大的问题不是数据库本身而是业务代码里的事务设计问题——多个服务更新同一组资源时没有约定统一的加锁顺序。这是一个“业务层可解数据库层难解”的典型问题。间隙锁死锁排名第二这个跟索引设计直接相关。如果查询能走更精确的索引锁定的范围就会缩小产生间隙锁的概率也会下降。热点行竞争本质上是“数据分布问题锁粒度耦合”带来的单行锁已经是最细粒度了热点行就是会被频繁更新这时只能从业务层拆分比如用缓存合并更新请求。5.2 锁粒度优化的核心逻辑锁粒度优化听起来很高深核心逻辑其实一句话就能说清楚锁的粒度越细并发能力越强但加锁开销越大锁的粒度越粗并发能力越差但加锁开销越小。优化的目标就是找到“并发能力”和“加锁开销”之间的平衡点。行级锁InnoDB默认适合高并发、多行分散更新的场景因为不同事务可以同时更新不同行。表级锁适合低频、批量、全表操作的场景因为加锁成本低而且这类操作本身就需要全表独占。间隙锁是REPEATABLE READ下“附赠”的一种锁用来防止幻读但它对并发的影响往往是被低估的。针对测试中发现的问题我制定了三个优化方向调整事务的加锁顺序消除交叉更新死锁。优化索引设计减小范围查询的锁范围减少间隙锁。热点行场景下用行锁变“队列锁”即串行化单行更新来降低竞争。5.3 方案一统一加锁顺序原理不复杂如果所有事务都以同样的顺序访问资源先A后B就不存在“你等我持有的B、我等你持有的A”的循环等待条件死锁自然消失。测试T02中两个线程一个先更新accounts.id1再更新id2另一个反之这就是典型的死锁诱因。优化方法是在Java程序入口处将涉及的更新资源ID先按升序排序再依次执行更新。public void executeTransaction(ListInteger accountIds) { // 关键按ID排序确保所有事务加锁顺序一致 ListInteger sortedIds accountIds.stream().sorted().collect(Collectors.toList()); try (Connection conn ds.getConnection()) { conn.setAutoCommit(false); for (Integer id : sortedIds) { String sql UPDATE accounts SET balance balance - 10 WHERE id ?; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, id); ps.executeUpdate(); } } conn.commit(); } }这里有个容易被忽略的点排序一定要在事务开始之前完成。如果在事务内部才排序可能多个事务并发执行到排序逻辑时就已经产生了资源竞争。排序动作本身很快放在事务外不会影响一致性。5.4 方案二索引优化缩小锁范围范围更新产生的间隙锁本质上是“查询走了不够精确的索引”导致的。举个例子-- 原SQL按status范围更新会锁住大量间隙 UPDATE orders SET status PAID WHERE status UNPAID; -- 优化思路增加create_time条件缩小锁定范围 UPDATE orders SET status PAID WHERE status UNPAID AND create_time 2025-01-01;但减少间隙锁最根本的方式是确认查询是否走了合适的索引。如果status字段的选择性不高只有几个枚举值走status索引反而不如直接全表扫。这种情况下应该通过组合索引把“定位到具体行”的能力拿出来。实际优化中我给模拟的orders表增加了一个联合索引(status, create_time)并将范围更新SQL改成先查出主键集合再逐行更新-- 第一步查出目标主键 SELECT id FROM orders WHERE status UNPAID AND create_time 2025-01-01 FOR UPDATE; -- 第二步逐行更新走主键索引锁定精确行 UPDATE orders SET status PAID WHERE id IN (...);这样做的原理是第一步的FOR UPDATE查询在RR隔离级别下仍然可能产生间隙锁但因为范围被create_time条件精确限制了间隙锁的总量大幅下降第二步走主键索引每行都是精确定位的行锁不再产生间隙锁。5.5 方案三热点行串行化热点行竞争是数据倾斜导致的问题比如秒杀场景下大家都在更新同一个商品的库存字段。行级锁在这里不但不是优势反而成了瓶颈——所有事务都排队等同一行锁并发越高等待越长甚至出现“虚假死锁”。针对这种场景我采取的是“行锁变内存锁”的思路在业务层引入分布式锁如Redis或者本地互斥锁按热点ID串行化更新请求。数据库层面仍然保留行锁但实际到达数据库的并发更新已经大幅降低。用批量合并的方式把同一热点行的多次更新合并成一条SQL。// 热点商品ID: 10001 String lockKey hot:item:10001; try { boolean locked redisLock.tryLock(lockKey, 1000, TimeUnit.MILLISECONDS); if (locked) { // 同一时刻只有一个线程能执行库存扣减 executeUpdate(UPDATE items SET stock stock - ? WHERE id 10001); } else { // 获取锁失败说明该热点正在更新可以选择重试或降级 System.out.println(Hot item update busy, retry later.); } } finally { redisLock.unlock(lockKey); }这种方案的收益是热点行上的数据库锁等待从“大量并发排队”变成“极少量串行”死锁和锁超时同时消除。代价是业务代码变复杂并且分布式锁本身有额外延迟。所以这个方法只适用于极端热点场景一般业务根本不需要走到这一步。6. 优化后的验证测试与效果对比6.1 同一套用例回测在完成上述三个方向的优化后我用完全相同的测试用例和并发参数又跑了一轮对比结果如下指标优化前优化后提升幅度死锁次数三轮合计49次3次下降93.9%TPS均值42805126提升19.8%P95延迟212ms158ms下降25.5%锁等待次数1876次284次下降84.9%间隙锁峰值数量235个47个下降80%6.2 解读数据的几种姿势死锁次数从49降到3这是最直观的效果。但这里要说一句公道话3次死锁并不是完全消除了而是概率降到了业务可接受的范围。只要事务并发执行理论上死锁永远不可能100%避免数据库的死锁检测机制本身就是兜底方案。优化的目标是把“频繁死锁导致用户报错”变成“偶发死锁可以自动重试”。TPS提升19.8%和P95延迟下降25.5%这两个指标的改善一部分来自锁竞争减少一部分来自死锁回滚带来的无效重试减少。之前死锁发生后被回滚的事务占用的数据库资源不会立即释放连接池里的连接还要重新执行事务这些开销反映在TPS和延迟上。优化之后无效回滚几乎消失性能自然回升。还有一个值得关注的数据锁等待次数从1876次降到284次下降幅度非常明显。锁等待和死锁是相关但不相同的两个指标。锁等待是死锁的必要条件之一锁等待减少说明整个系统的锁竞争强度大幅降低这比单看死锁次数更有说服力。6.3 优化后残留死锁的分析残留的3次死锁我专门去翻错误日志确认了原因都是T05范围查询场景下的间隙锁死锁。虽然加了索引缩小范围但由于某个测试线程的查询条件覆盖了两条记录之间的间隙仍然偶发触发间隙锁互相等待。针对这3次残留死锁有两个处理选项一是把隔离级别从REPEATABLE READ降到READ COMMITTED因为RC级别下InnoDB会自动禁用间隙锁只保留行锁二是保持RR在应用层对这种情况做“死锁重试”处理。考虑到业务需求最终选择了第二种方式。原因很务实这个场景不是高频路径偶发的死锁重试成本几乎可以忽略而降隔离级别是数据库全局参数影响面太大在不能确认所有业务都接受RC语义之前不应该为了一个低概率场景去动全局配置。这个取舍过程也提示了一个做数据库优化的原则优化方案的影响半径和收益必须匹配不能为了消除一个角落的疼痛给全身体检换个肝。7. 死锁排查的实战经验总结7.1 线上死锁定位的标准步骤如果你们的系统也出现了死锁问题按以下步骤排查可以少走很多弯路。第一步打开日志。确保innodb_print_all_deadlocksON。这一步必须提前做等出了事故再做就晚了——只保留最后一条死锁信息的情况下你无法看出死锁的频率和规律。第二步抓取死锁信息。死锁发生后从错误日志里找到类似如下的段落------------------------ LATEST DETECTED DEADLOCK ------------------------ 2025-01-10 15:23:45 0x7f8a1bcd7000 *** (1) TRANSACTION: TRANSACTION 871234, ACTIVE 12 sec starting index read mysql tables in use 1, locked 1 LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s) MySQL thread id 12345, OS thread handle 140129837731584, query id 98765 UPDATE orders SET statusPAID WHERE order_noA1001 *** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 31 page no 12 n bits 376 index PRIMARY of table test.orders trx id 871234 lock_mode X locks rec but not gap waiting *** (2) TRANSACTION: TRANSACTION 871235, ACTIVE 10 sec updating or deleting mysql tables in use 1, locked 1 233 lock struct(s), heap size 24752, 8 row lock(s), undo log entries 5 INSERT INTO orders_log(order_no, status) VALUES(A1002, PAID) *** (2) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 31 page no 12 n bits 376 index PRIMARY of table test.orders trx id 871235 lock_mode X locks rec but not gap waiting *** WE ROLL BACK TRANSACTION (2)第三步解读死锁信息。重点关注三段内容——每个事务当前执行到的SQL、等待的锁是什么行锁还是间隙锁、在哪个索引上、哪个事务被回滚。上面这个例子就是典型的“两个事务都在等待对方已持有的行锁”的交锁场景。第四步反推加锁顺序。结合业务代码分析两个事务的加锁顺序是否有交错。大多数死锁都能在这一步找到根因——要么是代码逻辑里对资源的访问顺序不统一要么是同一条SQL扫描了多行数据而扫描顺序因为索引变化或数据分布变化而不同。7.2 最容易踩的坑排查死锁时最容易犯的三个认知偏差在这里单独列出来。第一个误区是“死锁一定是数据库问题”。实际上绝大多数死锁的根源是应用层的事务设计问题——比如一个事务里做了太多事情、锁的粒度没有按需划分、不同模块更新表顺序不一致。数据库只是“如实反映”了这些问题而已。排查时先看业务代码再看索引设计最后才考虑调数据库参数。第二个误区是“加锁顺序完全一致就一定能避免死锁”。是的按同一顺序加锁能解决交叉等待的死锁但如果SQL执行计划的索引扫描顺序不稳定比如一条SQL今天走索引A明天走索引B加锁顺序也会随之变化。所以优化时不仅要看代码里的顺序还要确认涉及的查询是否“绑定”了稳定的执行计划。第三个误区是“死锁重试代码写好了就万事大吉”。如果死锁频率很高重试机制只是放缓了用户感知到的错误并没有解决资源浪费的问题。每次死锁回滚都意味着一次无效的数据库操作高频率下会显著拉升数据库负载和响应时间。重试是应急方案不是根本不治的处方。7.3 一条实用的“死锁排查决策树”按我自己的习惯死锁排查建议按以下顺序走决策路径死锁频率高吗如果不是高频比如一天几次直接在应用层做死锁重试即可。如果高频先看死锁日志里涉及的SQL是否有明显的“交叉更新”特征有则排查业务代码是否统一了加锁顺序。如果没有交叉更新再看是否涉及范围查询或间隙锁特征有则检查索引设计确认查询是否锁住了不必要的范围。如果既没交叉也没范围问题大概率是热点行竞争考虑缓存合并、串行化或者分区分表来分散热点。最后才能考虑到隔离级别级参数调整而且这个操作必须有灰度方案和数据支撑。这套决策树不一定覆盖所有场景但覆盖了日常线上80%以上的死锁问题。8. 锁粒度优化带来的连锁影响8.1 对并发性能的双刃剑效果优化完锁粒度之后TPS和延迟的改善是正向的但这背后有一个会被忽略的事实锁粒度越细加锁开销越大。行锁不是免费的每行锁都需要维护锁结构事务里涉及的锁数量越多内存和CPU开销越大。比如索引优化案例里把“范围UPDATE”改成了“先查主键再逐行UPDATE”。如果目标行数是100行原来一次UPDATE只需要加一把“范围锁”优化后需要先处理100次主键查询的锁、再发起100次UPDATE的锁总锁数量可能翻几倍。在目标行数极小比如个位数时优化效果最好但目标行数一大比如上千行锁开销反而可能拖垮性能。这是锁粒度优化里的核心权衡单独看单行更新的锁开销比范围锁小但事务里涉及很多单行更新时总锁开销可能超过一把范围锁。所以锁粒度优化不是越细越好而是“够用就好”。实际操作中需要根据SQL的影响行数来决定是精确行锁还是允许范围锁。8.2 对应用层代码的影响锁粒度优化几乎不是数据库单方面的事对应用层的改动往往更大。比如前面提到的热度行串行化场景需要在业务代码里引入分布式锁组件再比如统一加锁顺序需要修改多个服务模块的更新逻辑。做这轮优化前我评估了一下改动范围涉及业务代码的地方有5个接口、3个定时任务、2个消息消费逻辑总计约12处。如果把数据库索引和参数调整算进去整轮优化的工作量大头其实在应用层而不是数据库层。这也是一个规律——数据库锁粒度优化的“最后一公里”永远是跟业务代码的协作单靠DBA在数据库层面调来调去效果天花板很低。8.3 对监控体系的要求优化完成后我同步在监控系统里增加了几项针对锁的指标data_locks表中锁总数区分行锁、间隙锁、表锁死锁发生次数按小时统计锁等待事件的平均等待时长事务回滚次数这几项指标配合原有的慢查询监控可以在死锁或锁等待问题再次出现时第一时间报警。我给监控设定的阈值是死锁次数大于5次/小时或者锁等待次数大于200次/小时就触发告警。测试环境里优化前经常超过这个阈值优化后几乎没有触发过。监控的意义不只是“事后复盘”更重要的作用是提供优化效果的“长期验证”。一两次压测的数据可能偶然性很大只有持续观察一段时间的线上指标才能确认优化是真实有效的。9. 这轮测试的局限性与后续优化方向9.1 覆盖面有限这轮测试用的是MySQL 8.0 InnoDB用的是REPEATABLE READ隔离级别测试结果是符合这种配置的。如果换成READ COMMITTED级别间隙锁相关的死锁数量会大幅下降但其他两种死锁模式依然存在。如果换成PostgreSQL它的并发控制和MVCC机制跟InnoDB完全不同死锁特征也会不同。所以这篇报告的方法论可以迁移但具体数据不能直接照搬。测试工具上sysbench和自研Java程序都能稳定制造死锁但都基于“固定SQL模式”的前提。真实业务里SQL的复杂度远高于测试用例包括多表JOIN、子查询、存储过程调用等这些场景下锁的获取顺序更加复杂不是简单的“AB-BA”模型能完全覆盖的。后续如果有条件可以引入更接近生产环境的流量回放工具把线上真实SQL录制下来压测。9.2 下一步优化建议一是考虑对存量冷热数据做分离。把高频更新的热数据比如最近3个月的订单和低频访问的冷数据比如去年的历史订单拆到不同表或分区中从物理上隔离锁竞争。这个方向对热点行竞争场景有根本性的改善但涉及数据迁移和代码路由改造工作量较大。二是引入读写分离或缓存更新合并。把读流量导向从库写流量留在主库对热点数据的更新先更新缓存异步批量回写数据库。这个方案的难点在于缓存与数据库的一致性问题但对“读多写少”的业务收益很大。三是尝试在预发环境引入“混沌工程”思路的死锁注入测试。即在发布前自动模拟并发事务主动制造死锁场景验证新代码的死锁重试机制是否健壮。这比事后排查更进一步能做到“把死锁消灭在上线前”。10. 一些操作层面的私人心得最后聊几点这轮测试中获得的实操体会。第一测试死锁别舍不得开日志。以前我也经历过“压测完发现错误日志没打开死锁信息全部丢掉”的窘境。现在我凡是涉及到锁相关的压测第一件事就是确认innodb_print_all_deadlocksON第二件事是确认performance_schema的锁监控表能查到数据。没有这两样后面的一切分析都是盲人摸象。第二用sleep放大死锁窗口是个非常有效的手段。测试环境里为了让死锁稳定复现可以故意在两次加锁之间加一段休眠时间让交叉等待的窗口尽可能大。这种方法在线上绝对不能使用但在测试环境价值巨大——它能让你在3分钟内复现线上需要跑一整天才能碰到的死锁。第三锁粒度优化之后一定要做回头验证。我见过太多这类情况优化方案在测试环境TPS暴涨但上线后表现反而不如优化前。原因可能是测试环境和生产环境的数据分布、并发模型差异太大。所以每一轮优化都不能只看测试环境的数字必须在预发或灰度环境观察一段时间确认真实的业务流量下指标确实变好才能算真正落地。第四锁粒度优化的价值评估要算上开发成本。有些优化方案比如热点行串行化虽然数据库压测数据很好看但应用层要引入分布式锁、要处理锁超时和重试、要增加监控整体开发成本可能高达几周。对于大多数并发量没那么大的业务这些成本远超收益。优化前先算清楚账别为了“纸面上的TPS提升”去过度设计。