ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MySQL锁机制深度解析:从行锁到死锁的并发控制与优化实战

MySQL锁机制深度解析:从行锁到死锁的并发控制与优化实战

做数据库开发或者运维的朋友,一定都遇到过这样的场景:白天业务正常跑着,突然某个更新操作卡住了,等了半天还在执行,紧接着一堆请求堆积起来,数据库CPU蹭蹭往上飙。查了半天,发现罪魁祸首就是一条没走索引的UPDATE,把整张表的写锁给拿住了,其他事务全在排队等锁。这种"一个人卡住,所有人遭殃"的现场,我在生产环境里见过太多次了。

这篇文章想好好聊一聊MySQL锁机制里的并发控制与优化策略。锁这个东西看似简单——无非就是加锁、等待、释放,但真正用起来,里面的坑一个比一个深。意向锁、间隙锁、临键锁、插入意向锁,还有MVCC多版本并发控制,这些概念如果只是纸上谈兵,遇到真实故障根本不知道从哪里下手排查。这篇文章适合刚接触InnoDB锁机制的读者建立完整认知,也适合已经写过不少SQL但被锁等待、死锁折磨过的开发者拿来查漏补缺。我会结合自己实际排查过的案例,把这套机制掰开了讲清楚。

1. 并发控制的地基:MySQL锁机制全景拆解

1.1 为什么不加锁不行:并发场景下的三个典型问题

先想一个问题:数据库为什么需要锁?根本原因只有一个——多个事务同时操作同一份数据时,如果不加控制,结果就会乱套。这个"乱套"具体有三种表现形式:

脏读。事务A修改了一条数据但还没提交,事务B正好读到这条被改过的数据,然后事务A回滚了,事务B拿着一个根本不存在的结果继续往下算。在金额计算、库存扣减这类场景里,脏读的危害是致命的。

不可重复读。事务A先查询了一条记录,事务B修改并提交了这条记录,事务A再次查询时发现结果变了。一次事务里,同一条SQL查出两个不同的结果,这在报表统计和逻辑判断里是绝对不能接受的。

幻读。事务A按某个条件范围查询出N条记录,事务B插入了一条符合条件的新记录并提交,事务A再次按同样的条件查询时,多出了一条之前没见过的记录。数据量对不上的感觉,就像撞了鬼一样。

这三种问题,对应的是不同的并发控制级别。MySQL给出的解决方案,就是锁机制加MVCC的混合策略。MyISAM时代只能靠表锁硬扛,并发一高就垮;到了InnoDB时代,行级锁配合MVCC,才真正撑起了高并发读写场景。

1.2 锁的大类:全局锁、表级锁与行级锁

MySQL的锁可以按照粒度分成三个层级,理解这一点是掌握整个锁机制的第一步。

全局锁是最狠的。执行FLUSH TABLES WITH READ LOCK之后,整个数据库实例只允许读,不允许写,所有DML语句全部阻塞。我在做全库逻辑备份的时候用过,但生产环境基本不敢碰——一条命令下去,业务直接进入只读状态,那场面就是事故现场。

表级锁包括表锁、元数据锁(MDL锁)和意向锁。表锁分读写两种,LOCK TABLES ... READ/WRITE这种用法在日常开发中不推荐。MDL锁是MySQL自己管理的,执行DDL语句(比如ALTER TABLE)时会自动加上,它的存在是为了保护表结构不被并发修改。你或许遇到过这种情况:一个事务开启了但一直没提交,另一个事务想给表加个字段,结果被MDL锁堵住,后续所有对该表的读写全部排队——这就是典型的MDL锁导致的"表不可用"事故。

行级锁是InnoDB的核心竞争力。它允许不同事务同时修改同一张表里的不同行,只要这些行不冲突,就不用排队等待。代价是加锁的开销更大,锁管理更复杂。从MyISAM换到InnoDB,等于是用一点锁管理的CPU开销,换来了并发能力的大幅提升,这笔交易非常划算。

2. 深入InnoDB行级锁:记录锁、间隙锁与临键锁

2.1 共享锁与排他锁:读写之间的基本博弈

行级锁里最基础的就是共享锁(S锁)和排他锁(X锁)。S锁是读锁,多个事务可以同时持有同一行上的S锁,互相不阻塞。X锁是写锁,同一行上面只能有一个X锁,和其他所有锁都互斥。

用SQL怎么控制呢?普通的SELECT是不加锁的,走的是MVCC的快照读。想主动加S锁,用SELECT ... LOCK IN SHARE MODE(8.0版本也可以写成SELECT ... FOR SHARE)。想主动加X锁,用SELECT ... FOR UPDATE。UPDATE和DELETE语句会自动给涉及到的行加X锁。

这里有个容易被忽略的点:S锁和X锁的互斥关系,不是看"谁先谁后",而是看"读写冲突"。两个S锁可以共存,但一旦有事务想要写这行数据,就必须等所有S锁释放。所以如果业务逻辑里大量的只读操作都用了FOR SHARE,写操作的压力会非常大。我见过有人因为图省事,所有SELECT都加FOR SHARE,结果把读写并发完全掐死了。这属于自己给自己挖坑。

2.2 间隙锁与临键锁:幻读的终结者

说到间隙锁,得先明白一个关键点:行级锁锁的不是记录本身,而是索引记录。在可重复读隔离级别下,InnoDB为了防止幻读,引入了间隙锁(Gap Lock)和临键锁(Next-Key Lock)。

间隙锁锁定的是一个范围区间,这个区间里可能没有实际记录,但它把区间内插入新记录的行为给禁止了。临键锁则是记录锁和间隙锁的组合,锁定的范围是"左开右闭",也就是包含最右边的记录,也包含记录之前的间隙。

举个实际例子。表里有 id 为 1、3、5 的几条记录,现在事务A执行:

SELECT * FROM t WHERE id > 2 FOR UPDATE;

此时InnoDB会对 (3, 5] 这个区间加临键锁,同时可能对整个范围 (5, +∞) 加锁。另一个事务想插入一条 id=4 的记录,就会被阻塞。为什么要这么做?因为如果不锁间隙,事务A执行完查询后,另一个事务插入了 id=4,事务A再查一次就会多出一条记录——幻读。

间隙锁的副作用也很明显:它会锁住一个不存在的区间,导致原本不冲突的插入操作互相阻塞。所以很多人在RC(读已提交)隔离级别下运行,就是为了避免间隙锁带来的并发下降。但注意,RC级别下InnoDB虽然弱化了间隙锁,但要在binlog用row格式的情况下才能彻底避免幻读相关的bug,这个知识点后面讲隔离级别时会展开。

2.3 意向锁:表级和行级之间的"通信桥梁"

意向锁是InnoDB内部的一种表级锁,分为意向共享锁(IS)和意向排他锁(IX)。它的作用不是直接锁住数据,而是告诉其他事务:"我这个事务准备在表里的某些行上加共享锁或排他锁"。

为什么需要意向锁?想象一下:事务A在表里锁了某一行,事务B想对整张表加表级排他锁。如果没有意向锁,B就得遍历所有行去检查有没有行被A锁住,这种扫描的成本太高了。有了意向锁,B只需要检查表上有没有IX锁,有就直接阻塞等待,一条判断就完了。

意向锁之间互不排斥,IS和IX可以在同一个表上共存。它只和表级的S锁、X锁冲突。比如事务A持有IX锁,事务B想获取表级S锁,会被阻塞,因为S锁要求整个表不能有正在进行的修改意图。这个机制设计得挺巧妙的,用一个轻量级的标记,避免了昂贵的行扫描判断。

2.4 MVCC:让快照读与锁读共存

MVCC(多版本并发控制)是InnoDB另一个并发神器。它通过undo log保存数据的历史版本,让读操作不加锁也能拿到一致性快照。

具体原理是每一行都有隐藏列:DB_TRX_ID(最近修改该行的事务ID)、DB_ROLL_PTR(指向undo log中回滚段记录的指针)。当一个事务读取数据时,会根据当前活跃事务列表生成一个ReadView,然后顺着版本链找到对当前事务可见的那个版本。这就是快照读,不需要加锁也不会被写事务阻塞。

MVCC和锁的分工很清晰:快照读(普通SELECT)走MVCC,不加锁;锁定读(SELECT ... FOR UPDATE / FOR SHARE)和INSERT/UPDATE/DELETE走当前读,必须加锁。这样读读不阻塞,读写也不完全阻塞,只有在真正存在写冲突时才让锁发挥作用。

实测下来,MVCC解决的是"读不可重复"和"读脏数据"的问题,但解决不了幻读——因为快照读看到的是历史版本,当前读看到的是最新数据。所以幻读还得靠间隙锁来防,这就是为什么RR隔离级别下InnoDB既要MVCC又要间隙锁的原因。

3. 锁的获取、等待与死锁:实战中必须啃下的硬骨头

3.1 一行SQL是怎么加锁的:加锁规则总结

排查锁问题的时候,脑子里必须有清晰的加锁链路。根据InnoDB的加锁规则,可以总结成这么几条:

等值查询命中唯一索引时,只对匹配的那一行加记录锁。比如WHERE id = 5,如果id是主键,就只锁 id=5 这一行,锁最小,并发最好。

等值查询命中普通索引时,会对命中的索引记录加记录锁,同时对相邻的索引间隙加间隙锁,防止幻读。主键或二级索引上的回表记录也会被加锁。

范围查询则复杂得多,会对扫描到的每一个索引项加临键锁,直到找到第一个不满足条件的记录为止,并锁定该记录所在的间隙。

还有一个常见误解是"UPDATE 只锁更新的行"。实际上如果UPDATE语句的WHERE条件没走索引,InnoDB需要全表扫描来确定要更新的行,扫描过程中会给所有扫描过的记录都加上X锁。这就是开头说的场景:一条没走索引的UPDATE,等于给整张表所有行都上了写锁,其他任何写操作都得排队。所以优化锁问题,第一件事永远是检查SQL有没有走合适的索引。

3.2 死锁的成因与检测机制

死锁的本质是两个事务互相持有对方想要的锁,并且谁都不愿意先释放。经典的例子是AB-BA问题:事务A先锁了记录1再想锁记录2,事务B先锁了记录2再想锁记录1,两边互相等待,形成循环。

InnoDB对死锁的处理机制值得单独说一下。它内部有一个后台线程专门检测死锁,定期检查锁等待的关系图。一旦发现死锁循环,会立刻选择回滚其中一个代价较小的事务(通过分析事务影响的行数、持有的锁数量来评估),释放它占用的锁,让另一个事务继续执行。被回滚的事务会收到Deadlock found when trying to get lock; try restarting transaction这种错误。

u还有两个与死锁相关的参数:innodb_deadlock_detect控制是否开启死锁检测,innodb_lock_wait_timeout控制单条锁等待的超时时间。在并发量极高且锁冲突频繁的系统中,死锁检测本身会消耗一定的CPU,有团队会关闭检测依赖锁超时兜底,但这需要仔细评估业务场景,一般不推荐贸然关闭。

3.3 模拟一个死锁案例:从SQL层面还原现场

用一个最简单的死锁场景来演示。两个事务同时操作两张表,顺序正好相反:

事务A执行:

BEGIN; UPDATE orders SET status = 1 WHERE id = 100; -- 此时持有 orders 表 id=100 这行的X锁 UPDATE inventory SET quantity = quantity - 1 WHERE id = 200; -- 需要获取 inventory 表 id=200 这行的X锁,但事务B已经持有了,进入等待

事务B执行:

BEGIN; UPDATE inventory SET quantity = quantity - 1 WHERE id = 200; -- 此时持有 inventory 表 id=200 这行的X锁 UPDATE orders SET status = 1 WHERE id = 100; -- 需要获取 orders 表 id=100 这行的X锁,但事务A已经持有了,进入等待

两个事务各自拿着对方需要的锁,形成死锁。InnoDB检测到之后,大概率会回滚事务B(因为它影响的行数更少,回滚代价更小)。事务B的应用层会收到死锁错误,需要业务代码做重试。

这个案例告诉我们一个非常重要的规则:多行、多表操作时,所有事务必须按照相同的顺序访问资源。只要大家都先操作orders再操作inventory,就不会出现循环等待。

3.4 实战查看锁状态:三条SQL定位问题

遇到锁等待,怎么快速定位?我平时排查锁问题是按这个顺序来的:

先看有没有锁等待正在发生:

SELECT * FROM performance_schema.data_lock_waits\G

这个8.0以后的新视图(5.7用information_schema.innodb_lock_waits)能直接显示谁在等锁、谁持有锁、等待了多久。

再看当前所有事务的锁情况:

SELECT * FROM performance_schema.data_locks;

最后用压轴大招,看InnoDB的引擎状态,里面包含最近一次死锁的完整信息:

SHOW ENGINE INNODB STATUS\G

输出里找到LATEST DETECTED DEADLOCK段落,能看到导致死锁的两条SQL语句、加锁的索引、持有的锁类型。我第一次分析死锁的时候,就是靠着这段输出一步步还原出事务执行顺序的。注意这个查询输出的死锁信息是最近一次的,如果死锁频繁发生,只靠它是看不到历史的,需要借助日志系统或者定期采集。

4. 并发优化策略:从锁层面压榨数据库性能

4.1 索引优化是第一优先级:锁范围取决于索引

锁的粒度跟索引的选择直接挂钩。同样的SQL,走不同索引,锁的范围天差地别。

拿这个场景来说:表 user_order 有 id(主键)、user_id(普通索引)、status 三个字段。现在执行:

UPDATE user_order SET status = 2 WHERE user_id = 12345;

由于 user_id 有索引,InnoDB只需要找到该用户对应的少量索引记录,对这些记录加X锁就行了,其他用户的订单完全不受影响。但如果 user_id 没有索引,这条更新就得全表扫描,每一行都会被加锁,整个表的写操作全部瘫痪。

所以优化锁竞争的第一步永远是查看执行计划:

EXPLAIN UPDATE user_order SET status = 2 WHERE user_id = 12345;

重点看 type 和 key 字段。type 是 ALL 说明全表扫描,key 是 NULL 说明没用索引,两种情况都必须处理。

还有一个细节容易被忽略:当UPDATE通过二级索引定位记录时,InnoDB会先给二级索引记录加锁,再通过回表给主键索引记录也加锁。所以即使二级索引过滤后的结果集很大,主键索引的锁也不少。尽量让WHERE条件过滤出来的结果集小,锁范围自然就小。

4.2 事务粒度控制:短事务才是并发利器

长事务是目前生产环境里最常见的锁问题根源之一。一个事务从开启到提交之间,持有的所有锁都不会释放。如果这个事务里包含大量耗时操作——比如远程调用、复杂计算、大批量数据处理——那么锁被占用的时间就会被无限拉长。

优化事务设计有几个确定性原则:

让事务尽可能短。从业务代码的角度看,能在这个事务里做的操作不要带进下一个事务。比如先查询再判断再更新的流程,查询用快照读不加锁,判断逻辑在代码里完成,最后只把需要更新数据的那一段放进事务里。

减少事务中不必要的SELECT ... FOR UPDATE。锁定读的锁要持有到事务结束,能用普通查询就不要用锁定读。我之前优化过一个库存系统,原来所有查库存的SQL都带着 FOR UPDATE,结果两个不相关的商品只要在同一个事务里被操作,就会互相锁等待。改成普通SELECT之后,并发量直接上来了。

合理安排批量任务。大批量的UPDATE/DELETE,应该分批执行。每批处理几百到几千条,提交一次,短暂释放锁,让其他事务有机会插进来。写过一次凌晨跑批把订单表锁了半小时导致白天业务延后的事故后,我就养成了分批操作的习惯。

4.3 隔离级别的选择:RR与RC的权衡

隔离级别是最容易被忽视的锁优化手段。MySQL默认是RR(可重复读),这个级别下会有间隙锁,锁的范围天然就比RC(读已提交)大。

到底选哪个级别?要看业务对一致性的要求。

RR的好处是快照读在事务内多次查询结果一致,天然防幻读,适合对数据一致性要求极高的金融类系统。代价就是间隙锁带来的并发下降:两个事务只要写到相邻区间就会被阻塞,哪怕实际要操作的数据完全不同。

RC级别下不存在间隙锁(准确说是极大弱化),锁的范围更加收敛,并发能力更强。代价是同一个事务里多次查询可能读到不同数据,而且binlog必须设置为row格式才能保证主从复制的一致性。很多互联网业务选择RC,就是冲着更小的锁范围和更高的并发去的。

我的经验是给业务做分级隔离:核心的账户、订单系统用RR,因为数据准确性优先;非核心的日志、统计类系统用RC,因为并发量大且对一致性的容忍度较高。修改隔离级别的方式:

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

注意生产环境修改隔离级别要做完整回归测试,尤其是涉及主从同步的场景,binlog格式必须同步调整。

4.4 热点行更新与排队机制:从业务侧绕开锁冲突

有一种场景是锁优化怎么都绕不开的:热点数据的频繁更新。典型的就是秒杀场景下的库存字段,所有请求都去UPDATE同一行,这行上的X锁就是整个系统的瓶颈。加再多的索引、把隔离级别改成RC,都没用——物理上这一行只能串行更新。

这种问题需要从业务设计层面解决。方案之一是预扣库存:把库存拆分成多个子账户或子库存,每个请求随机分配到一个子库存上更新,把单行热点的锁竞争分散到多行。方案之二是引入异步队列:写操作全部进消息队列,由消费者按顺序更新库存,数据库层面根本不会出现并发写。方案之三是用Redis这样的组件承载瞬时高并发的计数操作,再异步同步到MySQL。

我在项目里见过最成功的方案是把热点账户的余额按业务维度拆分成多个子账户,每个子账户独立加锁。虽然增加了账务合并的复杂度,但把单点串行变成了多路并行,系统的吞吐量提高了好几倍。

4.5 参数调优:合理设置锁等待与死锁相关参数

锁相关的几个关键参数值得根据业务特点做调整:

innodb_lock_wait_timeout控制事务等待锁的最长时间,默认50秒。这个时间太短,业务稍微卡一下就直接报锁等待超时错误;太长,锁故障时堆积的请求会把数据库拖垮。一般建议设置在5到10秒之间,具体根据业务接口的响应时间要求来定。

innodb_deadlock_detect默认开启。如果系统并发极高,死锁检测的代价不可忽视,有些团队会选择关闭它并依赖锁超时来兜底。但关闭后一旦真正出现死锁,最多要等innodb_lock_wait_timeout时间才能释放,业务停顿可能更明显。我的建议是先开检测,如果确实存在性能瓶颈,再做压测试验后谨慎调整。

transaction_isolation前面已经提过。还有innodb_autoinc_lock_mode这个参数,它控制自增主键的加锁策略。设为1是默认值,插入时使用轻量级的互斥锁而不是表级AUTO-INC锁,兼顾并发和自增值的连续性;设为2则完全不用表锁,自增值可能不连续但并发最好。如果业务对自增ID的连续性有硬性要求,需要仔细核对。

5. 常见锁问题排查与避坑技巧实录

5.1 高频问题速查表

问题现象根本原因快速排查方法解决方案
UPDATE执行很慢,大量会话处于Waiting for lock更新条件没走索引,全表加锁查看information_schema.data_lock_waits加索引,缩小锁范围
执行ALTER TABLE卡住长事务持有MDL锁SHOW PROCESSLIST 找Sleep状态的长事务优先kill长事务,设置lock_wait_timeout
出现Deadlock found错误事务间锁循环等待SHOW ENGINE INNODB STATUS查看最近死锁统一事务访问顺序,缩短事务
RR级别下并发下降明显间隙锁导致区间互斥查看引擎状态确认间隙锁评估切换到RC级别
主从延迟突然加剧大事务导致从库回放慢观察主库binlog事件大小拆分大事务为小事务
应用频繁报lock wait timeout锁竞争激烈或等待超时配置过短查看参数值并监控锁等待时长优化SQL和事务,适当调大超时

5.2 我踩过的三个锁相关大坑

第一个坑是MDL锁引发的"雪崩"。某次上线一个加字段的DDL,正好赶上业务高峰期,有个后台任务的事务一直没提交。DDL拿不到MDL写锁,就在队列里等着,但问题是,后续所有对这张表的查询即使只想拿MDL读锁,也得等前面的写锁请求先处理完。结果几十个业务请求全部堆积在那张大表上,数据库连接数被打满。从那以后我给自己定了一条铁律:DDL只能在低峰期执行,且执行前必须先查一下有没有长时间未提交的事务。

第二个坑是同一行数据的并发UPDATE。曾经优化过一个订单状态流转功能,多个服务节点同时更新同一个订单的状态字段,导致频繁的锁等待超时。查下来发现是并发设计的问题——状态机里允许两个节点同时处理同一个订单。解决方案是在应用层增加分布式锁,或者用版本号做乐观锁控制,只有版本号匹配的更新才允许执行,失败就回读最新状态再重试。

第三个坑是批量更新没有分页。一次运营活动要更新几十万条记录的状态,我图省事直接一条UPDATE语句跑。执行了快二十分钟,期间这张表的所有写操作全部阻塞。后来改成按主键范围分批更新,每批1000条,两分钟跑完全部数据,期间其他事务基本无感知。批量任务必须分批,这个原则后来写进了团队的操作规范里。

5.3 锁优化的常规检查清单

系统巡检或者性能优化的时候,我建议按这个清单逐项排查:

  • 慢查询日志里有没有类型为UPDATE/DELETE的大事务,单事务影响行数是否过大
  • 高频SQL的EXPLAIN是否走了预期的索引,type字段是否为非ALL
  • 是否存在同一个事务里既做查询又做远程调用等耗时操作的代码
  • 事务开启到提交的平均耗时,是否有超过秒级的事务在频繁出现
  • 业务是否用到了SELECT ... FOR UPDATE,能否改为普通查询加版本号控制
  • 热点数据更新是否集中在某几行,能否通过分桶设计分散压力
  • 监控里的锁等待次数和平均等待时间,是否存在持续上升趋势

这些项全部过一遍,绝大多数锁相关的隐患都能暴露出来。我每次接手新的业务系统,第一件事就是拉出慢查询和事务统计做这个检查,效果一直很稳定。

6. 从实际案例出发:一次完整的锁问题定位与优化复盘

6.1 故障现象与初步判断

去年接手过一个订单系统的性能优化项目,现象很典型:每天下午业务高峰期,应用侧频繁报Lock wait timeout exceeded,订单提交接口的P99延迟从原本的200毫秒飙到3秒以上。

初步排查时,我先看了数据库的当前锁等待情况:

SELECT * FROM performance_schema.data_lock_waits\G

发现所有等待的锁都集中在订单主表上,持有锁的源头是一个看起来毫不起眼的UPDATE语句,正在修改订单表的remark字段。这条语句每分钟执行次数不多,但每次执行耗时很长,而且长时间持有大量行锁。

6.2 深层原因分析:一条UPDATE引发的锁风暴

进一步查看这条UPDATE的执行计划:

EXPLAIN UPDATE orders SET remark = 'check' WHERE order_no = 'SO1234567890'\G

问题找到了。订单表的索引设计里,order_no本来应该建唯一索引,但实际只有普通索引,而且因为表数据量大、存在大量重复的order_no(业务上允许同一订单号有多个补单记录),MySQL优化器认为通过这个索引要扫很多行,干脆选择了全表扫描。结果这条UPDATE给整张表的所有行都加了X锁,所有其他订单的写操作全部被阻塞。

这里有个反直觉的地方:单看这条SQL,影响行数只有几条,但全表扫描的加锁范围是整张表。这再次印证了一个原则:加锁范围跟扫描行数是直接相关的,SQL优化器选择的执行路径决定了锁的威力。

6.3 优化措施与效果验证

优化方案分三步走:

第一步是SQL层面的紧急修复。把更新条件改为直接用主键定位,通过先查询主键ID再更新,避免全表扫描:

-- 先查主键ID(快照读,不加锁) SELECT id FROM orders WHERE order_no = 'SO1234567890' LIMIT 10; -- 再通过主键精确更新(只锁命中的行) UPDATE orders SET remark = 'check' WHERE id IN (...);

这属于临时应急措施,能立刻缓解锁风暴,但治标不治本。

第二步是结构层面的修复。由于业务上确实允许同一订单号存在多条记录,不能直接建唯一索引。折中方案是建一个(order_no, id)的联合普通索引,把通过order_no定位数据的扫描范围压缩到最小。这样UPDATE语句重新走索引,锁范围就从全表缩小到了几条记录。

第三步是应用层面的规范约束。约定所有修改订单的操作必须基于主键ID执行,不能直接使用非唯一字段作为更新条件。同时把该表的隔离级别从RR降到RC——排查确认业务场景不需要防幻读,去掉间隙锁之后并发能力进一步提升。

优化完成后重新压测,订单提交接口的P99延迟回到200毫秒以内,锁等待错误清零。从发现问题到彻底解决花了两天时间,但真正的根因分析只用了半天——剩下的大部分时间都在确认业务逻辑是否能接受结构变更。

那次复盘给我留下的印象很深。锁问题表面上看起来是数据库的机制问题,但真正的病根往往长在SQL写法和索引设计上。理解了锁的工作方式,就会明白为什么所有MySQL规范里都反复强调"UPDATE/DELETE必须走索引""禁止大事务""禁止热点行高频更新"——这些规定不是教条,每一条背后都有真实的故障案例撑着。我后来带团队做代码评审,都会格外关注事务边界和锁定读的使用,因为这两处是最容易埋雷的地方。

如果你现在正被锁等待超时或者死锁问题困扰,可以按这篇文章的思路排查一遍:先看锁等待状态,再查执行计划,最后审视事务设计。理清楚"锁从哪来、为什么要锁这么多行、怎么能少锁一些"这三个问题,大部分锁相关的性能问题都能找到出路。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进