资讯动态

MySQL死锁深度剖析:从原理、排查到实战预防指南

发布时间:2026/9/18 10:39:14 来源:尧图企业网站定制
1. 先说清楚死锁不是系统卡死而是资源分配僵住做MySQL开发或者运维的人几乎都见过这样一个报错ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction我第一次见到这个报错时第一反应是数据库是不是挂了赶紧查进程、查监控结果一切正常。后来才明白死锁不是数据库崩溃而是两个或多个事务在互相等待对方持有的锁资源谁都不肯放手形成了环形等待。InnoDB引擎检测到这个循环等待之后会选择牺牲其中一个事务回滚它让另一个事务继续执行同时抛出上面的报错。更直白一点说事务A拿着行1的锁想去拿行2的锁事务B拿着行2的锁想去拿行1的锁。两边都卡住两边都等不到对方释放这个时候如果不干预就会无限等下去。InnoDB的解决方案是“拉一个出来祭天”——回滚代价较小的事务释放它持有的锁让另一个事务跑完。这个机制本身是数据库自我保护的手段好消息是数据库不会真的死掉坏消息是你的业务代码会收到一个异常。如果代码里没有对死锁做重试处理用户就会看到操作失败甚至数据写了一半。本文面向的人群很明确后端开发、DBA、以及所有在业务代码里直接操作MySQL事务的工程师。我会从死锁产生的底层条件讲起然后给出完整的排查思路、真实案例分析以及我从实际项目中总结出的解决方案和预防手段。文章里所有场景都基于InnoDB存储引擎因为这是MySQL默认也是用得最广泛的引擎MyISAM根本不支持事务和行级锁也就不存在死锁这码事。2. 死锁的四个必要条件少一个都死不成聊死锁之前先把理论底子打好。死锁的产生需要同时满足以下四个条件缺一个都不会死锁。第一互斥条件。资源同一时刻只能被一个事务占用。MySQL的行锁、间隙锁天然满足这个条件这也是InnoDB为什么会出现死锁的根本原因。第二持有并等待。一个事务已经持有了部分锁资源同时还在等待其他事务持有的锁资源。这个在业务里太常见了——事务里先UPDATE一条记录再去SELECT另一条记录并加锁结果另一条记录被别的事务锁住了。第三不可剥夺。事务已经获取的锁不能被其他事务强行抢走只能由持有者自己释放。这就意味着谁都没法通过“抢”来打破僵局。第四循环等待。事务之间形成了一条完整的等待环路——A等B、B等C、C等A。举个真实业务场景。订单系统里有两个事务事务A先更新订单表订单ID1001再更新库存表商品ID5001。 事务B先更新库存表商品ID5001再更新订单表订单ID1001。如果两个事务同时执行事务A拿到了订单表的锁准备拿库存表锁事务B拿到了库存表的锁准备拿订单表锁环路就形成了。两个事务各自握着一个资源又各自等着对方的资源死锁条件全部满足。这里我想强调一点很多人以为死锁只发生在高并发大流量的场景里其实不是。哪怕QPS很低只要两个事务以相反的顺序访问同一组资源就可能触发死锁。我去年排查过一个线上问题某个低峰期的定时任务和用户手动操作撞在一起同样引发了死锁。理解了这四个条件解决方案的思路也就清晰了打破任何一个条件死锁就能避免。后面讲的所有解决方案本质上都逃不开这个逻辑。3. 用隔离级别与锁的类型推导死锁高发场景3.1 事务隔离级别决定了锁的范围MySQL InnoDB默认的隔离级别是REPEATABLE READ可重复读简称RR。这个隔离级别下InnoDB不仅会锁住命中的行记录还会锁住索引范围区间也就是我们常说的间隙锁Gap Lock和临键锁Next-Key Lock。间隙锁解决的是幻读问题。事务A在RR级别下执行SELECT ... WHERE id 100 FOR UPDATE不仅会锁定id大于100的所有已有记录还会锁住id大于100的整个区间防止其他事务在这个区间插入新记录。这个机制保证了同一事务内多次查询结果一致。但坏处也显而易见间隙锁大大增加了锁冲突的概率。两个事务明明操作的是不同的记录因为间隙锁的存在它们也可能互相阻塞甚至死锁。举个例子。事务A执行UPDATE user SET status 1 WHERE age 20如果age字段没有索引InnoDB会锁住整张表的所有记录和所有间隙。事务B想插入一条age25的记录哪怕目标位置和事务A锁定的记录完全不重叠也可能被间隙锁挡住。这种场景下发生死锁的概率非常高。如果使用READ COMMITTED读已提交隔离级别InnoDB只使用记录锁不使用间隙锁死锁的概率会大幅降低。代价是可能出现幻读同一事务内两次查询的结果可能不一致。对于绝大多数业务系统来说幻读的容忍度其实比死锁要高得多。我的建议是不要盲从“默认隔离级别最安全”这个说法。如果你的业务对幻读不敏感比如大部分互联网业务可以评估切换到READ COMMITTED来降低死锁概率。阿里云上的RDS MySQL默认就是READ COMMITTED这是有道理的。3.2 三种核心锁记录锁、间隙锁、临键锁要理解死锁必须把InnoDB的锁机制搞明白。这里我用一个生活化的类比来拆解。记录锁Record Lock就是锁住某一行。相当于自习室里有人占了某个具体座位其他人不能坐这个座位但旁边的座位还是空的可以坐。间隙锁Gap Lock锁的是一个区间不是具体行。相当于有人把他的座位以及座位周围一片区域都占了别人想在这片区域里加个新凳子都不行。间隙锁的作用就是防止其他事务在锁定区间内插入新记录。临键锁Next-Key Lock是记录锁和间隙锁的组合锁定一个左开右闭的区间范围。比如索引值在(10, 20]之间既锁住范围又锁住记录本身。这相当于一个人占住了某个座位还顺带把座位前方到上一个座位之间的过道也占了。在高并发的插入场景中还有一种特殊的锁叫插入意向锁Insert Intention Lock。多个事务想往同一个间隙里插入数据时如果插入的位置不冲突它们可以同时持有插入意向锁而不互相阻塞。但如果间隙上已经有间隙锁插入意向锁就会等待。死锁的高发场景往往出现在临键锁和插入意向锁的交互中。典型场景是事务A持有某个区间上的间隙锁事务B尝试在同一个区间插入新记录于是B等待A释放间隙锁。此时事务A又想插入一条记录到自己锁定的区间内而这条记录的位置恰恰是事务B试图插入的位置两个事务就互相等待了。这种场景在业务上极难通过代码逻辑完全规避因为你无法预知两个事务会在什么时间点操作什么样的数据。所以实际排查死锁时理解锁的具体类型比死记硬背死锁定义要重要得多。3.3 为什么RR级别下死锁“看起来更多”并不是RR级别真的会产生更多死锁而是RR级别下产生的死锁更容易被复现和观察到。RC级别下阻塞通常发生在记录行上两个事务锁定的记录只要不重叠就可以并行执行冲突概率低。一旦冲突就是明确的同一行记录竞争语义上很好理解。RR级别下间隙锁扩大了锁的粒度冲突不再局限于同一行记录而是扩展到了索引区间。两个事务可能操作的实体完全不同但因为它们的索引区间有重叠也会互相阻塞。排错的时候很难一眼看出两个事务在争抢哪一行。我经手过的一个真实案例一个电商系统的促销活动批量上架接口和用户下单接口发生了死锁。从表面看批量上架更新的商品id和用户下单的商品id完全不同根本不该有交集。最后追查锁信息才发现批量上架接口的事务范围很大锁定了商品表中某个索引条件下的整个区间恰好覆盖了用户下单涉及的那条商品记录。因为索引条件不同两边的锁区间产生了交叉死锁就出现了。类似的坑在RR级别下非常容易踩到。理解了这一层你就明白为什么很多有经验的团队会把隔离级别调整为RC或者在事务SQL的WHERE条件上下足功夫尽量让锁精确落在目标记录上。4. 一次真实死锁的完整排查链路4.1 从报错到定位的全过程有次线上系统突发偶发性的报错频率不高但每次出现都会导致部分订单状态更新失败。监控平台抓到的错误信息就是典型的死锁报错。第一步我登录MySQL实例执行SHOW ENGINE INNODB STATUS\G这个命令会输出当前InnoDB引擎的状态信息其中LATEST DETECTED DEADLOCK部分记录了最近一次死锁的完整信息。注意是“最近一次”如果死锁频繁发生这里只会展示最后一次的详情所以需要多次抓取或者配合监控平台的错误日志。输出结果里的关键信息包括事务1和事务2分别执行了什么SQL语句两个事务各自持有哪个表的哪些锁两个事务在等待哪个锁资源最终哪个事务被回滚第二步把死锁信息里的SQL语句拿到业务代码里搜索定位到具体的代码位置。这一步通常比较顺利因为SQL语句通常是唯一的能直接对应到具体的Mapper方法或者DAO方法。第三步分析两个事务的加锁顺序和加锁范围画出锁的等待关系图找出形成环路的根本原因。4.2 死锁现场的关键信息怎么读SHOW ENGINE INNODB STATUS输出的LATEST DETECTED DEADLOCK段落格式大致像下面这样*** (1) TRANSACTION: TRANSACTION 16728, ACTIVE 12 sec starting index read mysql tables in use 1, locked 1 LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s) MySQL thread id 126, OS thread handle 140123456789, query id 45013 UPDATE order SET status 2 WHERE order_id 1001 *** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 28 page no 45 n bits 72 index PRIMARY of table db.order trx id 16728 lock_mode X locks rec but not gap waiting *** (2) TRANSACTION: TRANSACTION 16729, ACTIVE 16 sec starting index read mysql tables in use 1, locked 1 LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s) MySQL thread id 127, OS thread handle 140123456789, query id 45020 UPDATE order SET status 3 WHERE order_id 1001 *** (2) HOLDS THE LOCK(S): RECORD LOCKS space id 28 page no 45 n bits 72 index PRIMARY of table db.order trx id 16729 lock_mode X locks rec but not gap *** (2) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 28 page no 45 n bits 72 index PRIMARY of table db.order trx id 16729 lock_mode X locks rec but not gap waiting信息量其实很大。能看出来事务1和事务2都在更新db.order表目标都是order_id1001这条记录。事务2持有这个记录的X锁事务1在等待。同时事务1也持有某种锁导致事务2无法完成提交。这里最常见的死锁模型是经典的“两个事务互相持有对方下一步要获取的锁”。比如事务1先锁定A记录再更新B记录事务2先锁定B记录再更新A记录。但在这个案例中死锁信息显示两个事务的SQL语句看起来只涉及同一条记录直接读SQL很难发现问题所在。此时就要往更深一层看事务1在更新order之前已经执行过其他SQL语句持有了额外的锁。SHOW ENGINE INNODB STATUS会列出事务1和事务2各自的锁结构但不会主动告诉你是哪一步造成的必须结合业务代码一段段看。4.3 到底是谁和谁在互相等待这个案例最终定位到的情况是事务A对应下单接口在一个事务里先执行了UPDATE inventory SET stock stock - 1 WHERE goods_id 5001再执行UPDATE order SET status 2 WHERE order_id 1001。事务B对应订单状态回调接口在一个事务里先执行了UPDATE order SET status 3 WHERE order_id 1001再执行UPDATE inventory SET stock stock 1 WHERE goods_id 5001。两个事务都以完全相反的顺序访问inventory表和order表。当它们同时执行时事务A拿到了inventory表的锁等待order表的锁事务B拿到了order表的锁等待inventory表的锁环路形成。这个过程看起来简单但定位过程中有一步特别容易忽略事务A里先执行的那条UPDATE inventory语句持有的是inventory表的锁而死锁信息里只显示两个事务正在竞争order表的锁。如果只看最终的死锁现场很容易误以为两个事务只是竞争同一行order记录而忽略了inventory表上的锁才是死锁环路的一部分。排查死锁一定要把事务的全部语句翻出来而不是只看报错时的那条SQL。这是我踩过坑后最深刻的教训。4.4 附加排查手段info_schema与performance_schema除了SHOW ENGINE INNODB STATUS还有几张系统表在死锁和锁等待排查中也非常有用。information_schema.INNODB_TRX查看当前所有正在执行的事务包括事务ID、执行时间、状态、等待的锁等。死锁发生前可以用它观察哪些事务长期不释放锁。information_schema.INNODB_LOCKS和INNODB_LOCK_WAITS这两张表在MySQL 5.7及之前版本可以用于查看锁的持有和等待关系。8.0版本里这两张表已被移除改用performance_schema下的锁相关表。performance_schema.data_locks和data_lock_waitsMySQL 8.0中的替代品能提供更细粒度的锁信息。我自己排查死锁时的标准动作是先SHOW ENGINE INNODB STATUS抓现场再查performance_schema.data_locks确认有哪些锁正在等待最后结合应用日志定位具体事务对应的业务代码。这里给一个实用的SQL查询当前所有正在等待锁的事务SELECT r.trx_id AS waiting_trx_id, r.trx_mysql_thread_id AS waiting_thread, r.trx_query AS waiting_query, b.trx_id AS blocking_trx_id, b.trx_mysql_thread_id AS blocking_thread, b.trx_query AS blocking_query FROM performance_schema.data_lock_waits w INNER JOIN information_schema.innodb_trx r ON w.BLOCKING_ENGINE_TRANSACTION_ID r.trx_id INNER JOIN information_schema.innodb_trx b ON w.BLOCKING_ENGINE_TRANSACTION_ID b.trx_id;注意这个SQL把等待方和被等待方做了一个自连接实际使用时根据MySQL版本的字段差异调整。8.0版本下该查询通常能直接给出完整的等待链路。5. 高频死锁场景拆解这些坑我都踩过5.1 同一张表的记录更新顺序不一致最经典的死锁就是事务内多条SQL语句更新同一张表的不同记录但两个事务的更新顺序恰好相反。模拟一下表t有两条记录id1和id2。事务A执行BEGIN; UPDATE t SET value value 1 WHERE id 1; UPDATE t SET value value 1 WHERE id 2; COMMIT;事务B执行BEGIN; UPDATE t SET value value 1 WHERE id 2; UPDATE t SET value value 1 WHERE id 1; COMMIT;两个事务同时启动时事务A锁住id1等待id2事务B锁住id2等待id1闭环。解决方案也最简单所有事务按照主键或者某个业务标识的固定顺序操作数据。比如约定先更新id小的记录后更新id大的记录死锁条件中的循环等待自然被打破。这个方案在代码评审阶段就要强约束靠开发人员自觉维护难度比较大所以这类问题很容易从开发阶段漏到线上。建议在代码规范里明确要求事务内的SQL必须保持一致的更新顺序。5.2 更新多条记录但排序不一致上一个例子的变种但更难发现。比如批量更新任务两个任务处理的是同一组数据但因为查询条件的差异或索引选择的差异它们拿到的记录排序不一样导致加锁顺序也不一样。处理批量数据时常见做法是先查出符合条件的记录主键列表再逐条更新。但两个并发任务如果用了不同的条件查询即使最终更新的记录集合完全相同加锁顺序也可能不同。比如任务1查出来的是[1001, 1002, 1003]任务2查出来的是[1003, 1001, 1002]两边同时执行更新第一个等待关系就产生了。解决办法是拿到主键列表后显式排序UPDATE t SET value value 1 WHERE id IN (1001, 1002, 1003) ORDER BY id;或者更稳妥地在代码层面先对主键列表做一次排序再按顺序发起更新。排序操作看似不起眼但对规避死锁来说作用巨大。5.3 同一张表的自关联更新有一种隐蔽的死锁场景是同一张表的自关联更新。举例表结构为树形结构每个节点有parent_id字段业务上需要更新某个节点时同时更新其所有子节点的状态。实现方式可能是先查出子节点列表再逐个更新子节点。两个并发事务如果操作的是同一棵子树的相邻节点就可能出现各自先更新了父节点、再尝试更新对方已锁定的子节点的情况。这种场景除了锁顺序问题还牵扯到查询结果的不确定性。我处理过的一个实际案例是事务A先更新了父节点再更新子节点事务B先更新了其中一个子节点再尝试更新同一个父节点。两个事务都在等对方释放锁。解决思路有两个方向一是整个子树处理过程使用同一把锁比如先锁住根节点二是把树形结构的更新拆分成两个独立事务先更新父节点再单独开事务更新子节点。第二种方案降低了锁持有的时间也减低了死锁概率。5.4 跨表更新顺序不一致前面真实案例已经演示了这个场景。只要系统里有多个事务涉及多张表的更新就要格外留意跨表加锁的顺序问题。订单系统和库存系统的交互是重灾区。下单扣库存和取消订单加库存这两条链路天然是反向的。如果代码没有统一约定先锁订单表还是先锁库存表死锁是迟早的事。规范的做法是在项目里定义一份“表级锁顺序清单”明确规定跨表操作时先锁哪张表后锁哪张表。这张清单可以写进代码评审checklist比事后排查死锁要高效得多。5.5 主键与唯一键的冲突还有一个容易忽略的场景INSERT ... ON DUPLICATE KEY UPDATE这类写操作在遇到唯一键冲突时会对唯一键索引加锁。如果两个事务同时尝试插入可能冲突的数据就会在唯一键锁上产生竞争。更隐蔽的是两个事务插入的数据主键不同但唯一键相同或者唯一键不同但主键的间隙范围重叠都会引发间隙锁竞争。这类死锁多半发生在用户手机号、身份证号、订单号这类业务唯一标识上。高并发注册或者重复提交订单时同一个唯一键被多个事务同时写入死锁率会明显上升。处理方式是如果业务允许尽量让应用层做去重减少重复写入同一唯一键的概率如果无法避免则在事务里对唯一键做先查再写的操作但注意查询要加FOR UPDATE否则查和写之间依然存在时间窗口。6. 死锁的解决方案从应急到根治6.1 设置合理的锁等待超时时间InnoDB提供了innodb_lock_wait_timeout参数默认值是50秒。这个参数的含义是事务等待锁的最长时间超过这个时间仍未获取到锁事务就会报错回滚。注意这个参数针对的是锁等待不是死锁。死锁本身会被InnoDB的死锁检测机制立即识别并回滚一个事务不会等50秒。但锁等待超时意味着两个事务尚未形成完整的循环等待只是单方向阻塞此时另一方可能在等待别的资源最终也可能演化为死锁。经验值如果业务对响应时间敏感建议把innodb_lock_wait_timeout调小一些比如5秒或者10秒避免事务长时间挂起占用连接资源。但也不能调得太小否则大量正常情况下短暂等待的SQL会频繁超时造成更多业务失败。具体调整方式SET GLOBAL innodb_lock_wait_timeout 10; SET SESSION innodb_lock_wait_timeout 10;线上环境修改需要评估业务容忍度一般配合监控告警一起做。6.2 事务代码里的死锁重试机制数据库层面能做的只有识别死锁和回滚最终业务能否恢复还得看应用代码怎么处理。死锁重试是应用层解决死锁最直接的手段。核心思路是捕获死锁异常之后不立即返回失败而是等待一小段时间后重新执行整个事务。伪代码如下public void executeWithDeadLockRetry(Runnable task, int retryTimes) { for (int i 0; i retryTimes; i) { try { task.run(); return; } catch (DeadlockLoserDataAccessException e) { // 记录日志 logger.warn(deadlock detected, retry times: {}, i 1); try { Thread.sleep(ThreadLocalRandom.current().nextLong(50, 200)); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); throw new RuntimeException(ex); } } } throw new RuntimeException(retry times exceeded); }重试的等待时间随机化处理是有讲究的。如果多个事务同时检测到死锁并同时重试固定的等待时间可能导致它们再次撞在一起。随机等待能把重试时间错开降低再次碰撞的概率。MyBatis-Plus和Spring的隔离级别管理对死锁重试也有影响。事务必须保证在每次重试时都是全新的事务不能复用上一次回滚后的连接状态。还有一个细节死锁重试只适用于短事务。如果一个事务执行了太多SQL重试的成本就很高而且重试期间如果重要业务数据已被其他事务修改重试未必能成功。所以重试的前提是事务执行时间短、涉及数据量小。6.3 缩减事务体量缩短锁持有时间很多死锁问题的根源在于事务持有锁的时间太长。事务里塞了很多不必要操作比如远程调用、消息发送、复杂计算导致整个事务的锁持有时间被无限拉长冲突窗口变大。我见过一个典型案例事务里先更新订单状态然后调用外部支付接口接口响应超时事务一直不提交持有着订单表的锁。其他事务过来更新同一订单号时全部阻塞最后堆积成死锁。处理原则就一句话事务里只做数据库的读写操作其他事情全部挪到事务外面。远程调用必须在事务提交之后执行。如果确实需要事务内的一致性保障可以用本地消息表或者事务消息方案解耦。实际操作中我会把一个事务的耗时阈值定在100毫秒以内。超过这个阈值就要审视事务里有没有多余的操作。6.4 降低隔离级别前面提到过把隔离级别从RR调整为RC可以消除间隙锁带来的额外锁冲突显著降低死锁概率。不是所有业务都能这么做。如果你的业务真的依赖可重复读的语义比如多个查询必须保持同一快照那就不适合改RC。但对于绝大多数互联网业务一条记录的生命周期内被多个事务同时操作才是常态幻读的影响反而小。如果你在用Spring管理事务调整隔离级别很简单Transactional(isolation Isolation.READ_COMMITTED) public void updateOrderStatus(...) { // 业务代码 }但需要注意如果MySQL实例的全局隔离级别是RRSpring只是给这次事务指定了RC底层还是要靠MySQL支持该隔离级别。MySQL默认就支持RC所以这个方案在任何版本的InnoDB上都能用。6.5 用索引让锁尽可能精准锁的粒度受索引的影响极大。一个UPDATE语句如果WHERE条件命中索引InnoDB只需要锁定索引对应的记录和间隙如果WHERE条件没有命中索引InnoDB只能全表扫描锁定所有扫描过的记录和间隙锁冲突范围瞬间被放大。判断SQL是否走索引最简单高效的方式是EXPLAINEXPLAIN SELECT * FROM order WHERE order_no 202401010001;看type列和key列。type为ref或者eq_ref且key非空说明索引使用正常type为ALL则说明全表扫描加锁范围会全面膨胀。所以高频更新的表的WHERE条件涉及的字段务必加上索引。这不只是为了查询性能更是为了把锁粒度控制在最小范围。尤其是涉及外键或者业务标识的字段一定要有索引否则两个事务更新不同记录也可能互相锁住。6.6 调整表结构设计来降低冲突有些表结构设计天然容易产生死锁。比如大量事务集中更新同一行记录。这个场景下死锁可能不是主要原因锁等待超时才是常态。但从业务角度来说如果同一行记录被更新的频率过高就该考虑行级拆分。我之前负责过一个积分系统用户签到、消费、退款都会更新用户积分汇总表的同一行。高峰期大量请求同抢一行时不时爆出死锁。后来按用户的积分明细拆分表结构每笔积分操作先插入明细表再异步汇总总积分同一个用户同一时间的并发写操作被大幅削减死锁问题也就消失了。设计阶段多考虑写冲突的分散性能避免很多运维期的头疼问题。7. 预防死锁写代码时的几条硬规矩7.1 统一加锁顺序多个事务如果需要同时访问多张表或多行记录锁的顺序必须一致。这条规矩要在团队规范中白纸黑字写清楚。比如约定先更新主表再更新子表先更新订单表再更新库存表先更新父节点再更新子节点。所有人都遵守同一个顺序循环等待就不容易产生。7.2 事务保持短小精悍事务不是用来做业务编排的容器它只能承担必须保证原子性的数据库操作。把查询尽量提到事务外面把可以在事务之后执行的更新放到事务之后把可以异步的操作异步掉。短事务不仅降低死锁概率还能减少锁等待时间是一个成本最低但收益极高的优化方向。7.3 使用SELECT ... FOR UPDATE时要清醒SELECT ... FOR UPDATE会立即给选中的记录加锁。如果有两个事务都执行FOR UPDATE查询再根据查询结果做更新它们可能在查询阶段就互相阻塞。FOR UPDATE使用的原则是能用普通SELECT就不要FOR UPDATE必须用的时候加锁范围要尽可能精确并且查询条件必须命中索引。7.4 定期监控与告警线上环境应当配置死锁监控。MySQL死锁发生后错误日志会记录相关信息SHOW ENGINE INNODB STATUS中也会保留LATEST DETECTED DEADLOCK信息。更推荐的方式是接入Prometheus加MySQL Exporter监控锁等待指标或者使用云数据库自带的风险诊断功能。一旦发现死锁数量突增立刻排查千万不能等到用户投诉了才来处理。我从自己的运维经验来看死锁问题的排查成本远高于预防成本。很多死锁代码只要在评审阶段多看一眼加锁顺序根本不会带到线上。8. 我的经验教训总结搞了好几年MySQL死锁这个问题看着小实际牵扯到的知识面很广事务隔离级别、索引原理、锁机制、应用代码结构、团队协作规范环环相扣。分享几个我自己的习惯。第一每季度抽查一次核心业务表的高频更新SQL确认隔离级别设置是否符合预期索引是否还在生效。数据库表结构一旦变更索引可能失效死锁概率就会悄悄上升。第二写事务代码时我会强制自己在注释里写明这个事务的加锁顺序。比如“先锁订单表再锁库存表”这样做代码评审的时候一目了然后面维护的人也不会改乱。第三监控告警里把死锁单独建一条规则不要和普通SQL错误混在一起。死锁出现时立刻拉取最近十分钟的慢查询和锁等待情况越早介入越容易定位。最后再分享一个实用小技巧如果你的系统频繁出现死锁且实在无法通过SQL层面解决可以尝试把死锁发生频率最高的几条SQL合并成一条。比如原来事务里先查再更新改为一条带子查询的UPDATE语句。SQL合并后锁的获取次数变少死锁概率也随之降低。当然子查询的性能消耗需要验证别为了解死锁引入更大的性能问题。MySQL的死锁问题本质上不是一个纯粹的技术问题更像是一个工程问题。技术选型和参数调优只是手段真正有效的做法是团队里沉淀一套清晰的事务规范让所有人按照同样的节奏去操作数据库。这套规范建立起来死锁会少很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价