MySQL锁分类体系:六条维度详解14种锁
2026/9/13 8:29:58 网站建设 项目流程

1. 先建立锁认知框架:六条分类维度与14种锁的对应关系

一说MySQL锁,很多人第一反应是“听过很多概念,但一用就乱”:共享锁、排他锁、意向锁、间隙锁、Next-Key锁、悲观锁、乐观锁……名词少说也有十几个,面试问起来能背几个定义,可真到死锁排查或者大表更新时,又不知道谁跟谁是一家人。

问题不在记忆力,而在缺少分类轴。锁不是一个“点”,它像一张多维度的网——同一把锁从不同角度看,能同时归属于好几个类别。比如SELECT ... FOR UPDATE加的是行级排他锁,从粒度看它是行锁,从读写语义看它是X锁,从加锁方式看它又是显式锁。你只记住其中一个维度,自然拼不出完整图景。

我的建议是,先建立六条分类轴,再把14种锁一个个挂到对应轴上去。六条轴分别是:

  • 粒度轴:全局锁、表级锁、行级锁
  • 读写轴:共享锁(S锁)、排他锁(X锁)
  • 意图轴:意向共享锁(IS锁)、意向排他锁(IX锁)
  • 行锁实现轴:记录锁、间隙锁、Next-Key锁
  • 并发策略轴:乐观锁、悲观锁
  • 加锁方式轴:隐式锁、显式锁

三条轴加起来正好14种。下面这张表可以先存着,后文逐个拆:

分类维度锁名称核心作用主要出现场景
粒度轴全局锁锁整个实例全库备份(FTWRL)
粒度轴表级锁锁整张表LOCK TABLES、DDL
粒度轴行级锁锁索引记录InnoDB事务读写
读写轴共享锁 S允许多个读并存SELECT ... FOR SHARE
读写轴排他锁 X阻止任何并发读写UPDATEDELETESELECT ... FOR UPDATE
意图轴意向共享锁 IS声明表上有S锁InnoDB加行S锁前
意图轴意向排他锁 IX声明表上有X锁InnoDB加行X锁前
行锁实现轴记录锁 Record Lock锁单条索引记录等值命中唯一索引
行锁实现轴间隙锁 Gap Lock锁索引区间,禁止插入RR隔离级别范围查询
行锁实现轴Next-Key锁记录锁+间隙锁组合RR隔离级别默认行锁
并发策略轴乐观锁更新时校验版本版本号/时间戳CAS
并发策略轴悲观锁操作前先加锁数据库自带行锁
加锁方式轴隐式锁记录自带事务ID形成锁普通INSERT
加锁方式轴显式锁用户主动加锁LOCK TABLESFOR UPDATE

后文每一节讲一条轴,你不必所有细节一次吃透,但至少要知道:问你“MySQL有哪几种锁”的时候,不是让你背14个名词,而是让你说明白了“从哪些维度看锁”。能把维度讲清楚,比多背两个名词有用得多。

2. 粒度轴:全局锁、表级锁与行级锁的工作边界

粒度轴是最好理解的一条轴,它回答的问题是“锁住的范围有多大”。范围从大到小依次是:全局锁 > 表级锁 > 行级锁。锁的范围越大,并发能力越弱,实现越简单;范围越小,并发能力越强,实现越复杂、代价也越高。MySQL之所以要提供三种粒度,本质上就是在“并发吞吐”和“一致性与实现成本”之间做权衡。

2.1 全局锁:用一条命令冻结整个库的写请求

全局锁是粒度最大的一种锁,锁住的是整个MySQL实例。执行下面这条命令后,整个库进入只读状态,所有DML(增删改)和DDL(改表结构)都会阻塞,只有SELECT不受影响:

FLUSH TABLES WITH READ LOCK;

我最早接触全局锁是在做逻辑备份的时候。早期用mysqldump备份InnoDB数据,如果不加任何参数,备份过程中其他事务还在写数据,导出的逻辑数据就会不一致。为了拿到一致性快照,很多初学教程会教你“先执行FTWRL,再备份,最后UNLOCK”,这确实是最朴素也最有效的方案:

FLUSH TABLES WITH READ LOCK; -- 执行备份动作 mysqldump -uroot -p test_db > test_db.sql UNLOCK TABLES;

但实际生产环境里,我几乎不用FTWRL对付InnoDB,原因就一个字:伤。全局锁一上,业务写请求瞬间跌零,如果备份脚本跑半小时,业务就被阻塞半小时。对InnoDB引擎来说,更好的做法是用mysqldump --single-transaction,它基于MVCC生成一致性快照,备份期间不阻塞业务写入。全局锁现在更多用在主从切换、整库只读维护这类极端场景下,日常开发基本碰不到。

还有一点值得注意:FLUSH TABLES WITH READ LOCK的“只读”不是SESSION级的,而是全局的,所以执行完一定要记得UNLOCK TABLES释放,否则后续所有业务写操作都会卡住。实际操作中,建议把备份脚本里获取全局锁和释放全局锁写成close配对,避免脚本中断后锁残留。

2.2 表级锁:整表读写互斥,InnoDB下尽量别主动用

表级锁锁住的是整张表,分两种:表读锁(所有会话可读,写阻塞)和表写锁(当前会话可读写,其他会话全部阻塞)。手动加表锁的语法是:

LOCK TABLES orders READ; -- 加表读锁 LOCK TABLES orders WRITE; -- 加表写锁 -- 业务操作... UNLOCK TABLES;

在MyISAM时代,表锁是主流方案,但到了InnoDB时代,行锁已经能提供细粒度并发控制,主动加表锁反而容易制造瓶颈。最典型的反面案例是:有人在InnoDB表上执行LOCK TABLES orders WRITE做批量更新,结果所有访问orders表的业务全被堵死,DBA一查锁等待,全是Waiting for table level lock

如果非要用表锁,有两条经验:

  1. 必须在SET autocommit=0的前提下使用。因为UNLOCK TABLES会隐式提交事务,如果你在事务中间用表锁,事务边界很容易乱掉。
  2. 表锁不参与InnoDB的行锁冲突检测。InnoDB往往感知不到表锁的存在,这在混合使用场景下容易引发很难排查的“假死锁”。

此外,LOCK TABLES本身并不区分存储引擎的锁机制——它走的是MySQL Server层的元数据锁逻辑,而InnoDB还有另一套自己的表级锁(后面说的MDL锁就是)。两者经常被混为一谈,实际是两个层次的东西。

2.3 行级锁:InnoDB的看家本领,底层锁在索引上

行级锁是InnoDB与MyISAM最大的区别,也是支撑高并发OLTP的核心。InnoDB的行锁并不是“锁住一行数据”这么简单,它的实现细节极其讲究:行锁实际上是加在索引记录上的,而且必须加在索引项上才能生效。

这句话展开说就是:

  • 如果查询走的是主键索引,行锁就锁在主键索引的叶子节点上。
  • 如果查询走的是二级索引,InnoDB会先锁二级索引记录,再回表锁对应的主键索引记录。
  • 如果查询没有走索引(全表扫描),InnoDB会退化为锁住表中所有记录——注意,这不等同于表锁,但效果上跟表锁没区别,同样会堵死其他写操作。

第二个细节特别值得警惕。很多人以为“我有行锁,随便写”,结果一张表数据量不大、查询条件又没有索引,一执行UPDATE就把整张表的所有行都锁了。我当时排查过一个线上事故:某个运营后台的更新SQL没走索引,每次执行都锁全表,偏偏这SQL又跑在事务里,事务提交前所有其他更新全部排队。最后解决方式很简单:给WHERE条件字段加上索引,锁的粒度瞬间从“全表”收缩到“几行”。

另外,行锁是有代价的:每多一把行锁,InnoDB都要在内存里维护对应的锁结构,所以事务里大量无脑更新(比如循环几万次逐行UPDATE)会带来肉眼可见的性能下降,甚至触发锁等待超时。能用一条SQL批量更新,就不要开循环逐条更新。

3. 读写轴:共享锁与排他锁的兼容性游戏

粒度轴解决了“锁多大”的问题,读写轴解决的是“锁住之后别人还能干什么”。这一条轴上只有两个锁:共享锁(S锁,Shared Lock)和排他锁(X锁,Exclusive Lock)。

3.1 S锁和X锁的语义边界

共享锁也叫读锁,含义是“我允许别人继续读同一份数据,但不允许别人写”。多个S锁可以同时加在同一行数据上,彼此不冲突。

排他锁也叫写锁,含义是“这份数据我先占住了,读和写都不许别人碰”。X锁和任何其他锁(不管是S还是X)都互斥。

两者组合的兼容性矩阵如下:

已持有锁请求S锁请求X锁
S锁兼容冲突
X锁冲突冲突

这个矩阵是所有锁冲突判断的基础,意向锁的兼容性判断也是基于它扩展的。

3.2 加锁语句与常用场景

S锁在InnoDB里除了事务隔离机制自动加之外,也可以手动加,旧版写法是:

SELECT * FROM orders WHERE id = 100 LOCK IN SHARE MODE;

MySQL 8.0开始推荐用更直观的FOR SHARE

SELECT * FROM orders WHERE id = 100 FOR SHARE;

X锁的加锁途径更常见,所有UPDATEDELETEINSERT ... ON DUPLICATE KEY UPDATE都会对涉及的行加X锁。手动加X锁的典型场景是“先查后改”:

SELECT * FROM orders WHERE id = 100 FOR UPDATE; -- 拿到结果后,在业务层做判断 -- 再执行 UPDATE orders SET status = 2 WHERE id = 100;

为什么要手动加FOR UPDATE?因为默认MVCC下,SELECT是不加锁的快照读,两条事务可以同时读到同一行旧数据,然后各自基于旧状态做更新,最后后提交的把先提交的覆盖掉——这就是丢失更新。加FOR UPDATE之后,第二个事务读到这行时会被阻塞,直到第一个事务提交,才能基于新值继续操作。

这里有一个非常容易踩的坑:FOR UPDATE加到没命中索引的查询上,会把全表记录全部锁住,而且这个行为在InnoDB里是“合法”的,不会给你任何报警。结合上一节说的行锁实现方式,你就明白为什么我一直强调“UPDATE和FOR UPDATE的WHERE条件必须走索引”了。

3.3 当前读与快照读的锁差异

理解S/X锁,还要知道一个潜规则:不是所有SELECT都会加锁。普通SELECT是快照读,走MVCC版本链,不加任何锁;只有FOR SHAREFOR UPDATEUPDATEDELETE才是当前读,必须加锁。这个差异直接影响你对“锁冲突”的判断。比如两个事务同时执行普通的SELECT,完全不会互相阻塞,但如果你把其中一个改成SELECT ... FOR UPDATE,冲突立刻出现。很多初学MySQL并发的人,就是没分清楚“读”和“加了锁的读”完全是两回事。

4. 意图轴:IS锁与IX锁,InnoDB为表级协作埋下的信号灯

很多人在行锁、表锁里绕了很久之后,第一次听说意向锁(Intention Lock)都会觉得陌生。意向锁是InnoDB特有的表级锁,但它本质上是为行锁服务的。它只有两种:

  • 意向共享锁(IS):事务准备在表的某些行上加S锁时,需先在表上加IS锁。
  • 意向排他锁(IX):事务准备在表的某些行上加X锁时,需先在表上加IX锁。

注意措辞:“准备”。也就是说,意向锁是一个声明,不是真把整张表锁住了。你可以把它理解成酒店房门上的“请勿打扰”灯牌——挂上牌子不代表房间被你锁死,只是告诉别人“里面的住客现在不想被打扰”。

4.1 意向锁到底解决什么问题

假设没有意向锁,出现这样的场景:事务A给表里第1、3、5行加了X锁,事务B这时候想给整张表加一个表级X锁(比如执行LOCK TABLES orders WRITE)。InnoDB怎么判断“表里有没有行锁”呢?它只能一行一行去扫描所有索引记录,看有没有锁冲突。这张表如果有几百万行,扫描成本简直不敢想。

有了意向锁之后,事务A给行加X锁前,先会在表上留下一把IX锁。事务B想加表级X锁时,只要看到表上存在IX锁,就知道“里面有行锁,X锁加不了”,直接阻塞,不需要逐行扫描。这就是意向锁最核心的价值:把“行锁和表锁之间的冲突检测”从O(N)的扫描降到了O(1)的判断

4.2 意向锁的兼容性矩阵

意向锁并不阻止其他事务获取不同的意向锁,它主要跟表级S/X锁发生冲突:

已持有ISIXSX
IS兼容兼容兼容冲突
IX兼容兼容冲突冲突
S兼容冲突兼容冲突
X冲突冲突冲突冲突

从表格里能看出来:IX锁和IX锁是兼容的,这意味着多个事务给不同行加写锁时,不需要互相在表级等待,真正阻塞只会发生在具体行上。如果你看到一条SQL持有IX锁,不要立刻断定“整张表被锁了”,它只是声明“某几行有写锁”。

实际排查锁等待时,这条信息特别有用。SHOW ENGINE INNODB STATUS里经常看到某个事务持有IX锁,另一事务也在等IX锁,新手容易误判为“表锁冲突”,其实两个IX锁根本不相干,真正卡住它们的是某条具体的行记录。必须进一步查performance_schema.data_locks去定位是哪些索引记录冲突,而不是被表级的IX锁带偏思路。

5. 行锁实现轴:记录锁、间隙锁、Next-Key锁如何联手挡住幻读

这一条轴是InnoDB在RR(可重复读)隔离级别下,处理并发读写的精髓所在。表面上你只看到“行锁”,实际InnoDB内部会依据查询条件,生成三种不同形态的锁:记录锁(Record Lock)、间隙锁(Gap Lock)、Next-Key锁。它们的出现不是随机的,而是围绕一个核心目标:解决幻读。

我先给出这三大锁的定位差异,再逐个展开:

锁类型锁定范围目的典型场景
记录锁单条索引记录(如id=10)锁住已存在记录等值命中唯一索引
间隙锁两个索引记录之间的区间(如id在5到10之间)阻止区间内插入新记录范围查询/等值未命中
Next-Key锁左开右闭区间(如(5,10]),含记录本身阻止区间插入+锁住已存在记录RR默认行锁策略

5.1 记录锁:只锁“那一条”的精准打击

记录锁很好理解,就是锁住某一条索引记录。执行:

SELECT * FROM orders WHERE id = 100 FOR UPDATE;

如果id是主键且记录存在,InnoDB给id=100这一条主键索引记录加上X锁,其他事务再更新或删除这一条就会被阻塞,但插入id=101完全不冲突。

记录锁有两种,对应S/X两个方向:LOCK_REC_NOT_GAP表示“只锁记录本身,不锁间隙”。这条信息在SHOW ENGINE INNODB STATUS里经常出现,看到这个标识,基本可以判断是等值查询且命中了索引记录。

需要注意:WHERE id = 100如果命中的是二级索引,InnoDB不是只锁二级索引记录,还会回表给主键索引记录也加一把X锁。所以两个不同二级索引指向同一行时,也会产生锁竞争。

5.2 间隙锁:锁住“不存在”的数据

间隙锁锁的是两个索引记录之间的“空洞”。比如索引列上有记录5和10,那么(5, 10)这个开区间就是间隙。两个事务可以同时持有(5,10)的间隙锁,但谁也别想在这个区间里插入新记录,比如插一条id=7,会一直被阻塞。

间隙锁最反直觉的地方在于:它锁的是“不存在的东西”。你一个SELECT ... WHERE id BETWEEN 6 AND 9 FOR UPDATE,可能一行记录都没查到,但它却在你查询范围对应的索引间隙上落了锁,阻止其他事务插入id=7、8等值。很多业务里,明明UPDATE没影响任何行,却导致别人INSERT等待超时,就是这个原因。

在RR隔离级别下,等值查询未命中记录时,也会在两侧记录之间产生间隙锁。比如执行SELECT * FROM orders WHERE id = 7 FOR UPDATE,表里没有id=7,但存在id=5和id=10,那么它会给(5,10)加间隙锁,防止insert id=7造成“幻读”式的数据变化。

5.3 Next-Key锁:记录+间隙的组合锁定

Next-Key锁是记录锁+间隙锁的组合,锁定的范围是左开右闭的区间。例如表里已经有id=5和id=10的记录,InnoDB在RR下执行SELECT * FROM orders WHERE id BETWEEN 5 AND 10 FOR UPDATE,锁定的范围大约是(负无穷, 5](5, 10]以及(10, 正无穷)中最靠近查询范围的几个区间。

精确描述是:InnoDB会扫描到id=5和id=10两条记录,并给它们所在的索引单元分别加Next-Key锁,具体为:

  • 对于id=5这条记录,加(-∞, 5]的Next-Key锁
  • 对于id=10这条记录,加(5, 10]的Next-Key锁

这意味着什么?区间里所有可能插入的新值都被挡住了,同时已存在的5和10也被锁住了。这就是RR下InnoDB解决幻读的根本机制:不仅锁已有记录,还锁住插入新记录的通道。

Next-Key锁是死锁的高发区。最经典的一个例子:两个事务分别持有(5, 10]的一部分区间,然后各自想往对方区间里插入数据,就很容易形成环路等待。实际排查时,我见过大量死锁日志里都有LOCK_X | LOCK_GAPLOCK_X | LOCK_REC_NOT_GAP的混搭状态。区分这两种状态的方法很简单:LOCK_GAP表示间隙锁,不包含记录本身;LOCK_REC_NOT_GAP表示记录锁,不含间隙;两者都带则是Next-Key锁。

补充一个使用层面的经验:如果你的业务允许,把隔离级别降到RC(读已提交),间隙锁基本就不再出现(只在外键检查和重复键检查短暂存在),Next-Key锁也随之大量减少,死锁概率会明显下降。代价是可能要自己在业务层处理一部分幻读问题——这就是架构取舍,没有银弹。

6. 并发控制轴:乐观锁与悲观锁的选择不只是数据库的事

有了前面这么多数据库内置锁,为什么还要单独讨论乐观锁和悲观锁?因为它们已经脱离了InnoDB的实现细节,变成了一种并发控制的策略选择。悲观锁的思想是“我认定你会跟我抢,所以先加锁占住”;乐观锁的思想是“我赌你不会跟我抢,所以最后提交时再校验”。

6.1 悲观锁:先锁后写,数据库天然支持

悲观锁在MySQL里基本就是依赖前文讲过的S锁、X锁、FOR UPDATE这些机制。它的流程是:读取记录前先加锁,处理完业务逻辑后提交释放。我举一个最常见的库存扣减场景:

BEGIN; SELECT stock FROM product WHERE id = 1 FOR UPDATE; -- 业务判断stock > 0 UPDATE product SET stock = stock - 1 WHERE id = 1; COMMIT;

FOR UPDATE保证了从读取到更新这段过程中,其他事务无法修改这一行,天然避开了超卖。代价是并发下降:同一时间只有一个事务能操作这行数据,其余全部排队。对库存这类强一致场景,这是最稳的方案,而且完全是数据库能力,无需改表结构。

悲观锁最大的坑是忘记提交或事务里做了慢操作。锁的持有时间跟事务生命周期绑定,一个事务里做完一次远程HTTP调用再提交,行锁就被白白占了几百毫秒甚至几秒,QPS一高,锁等待超时立刻飙起来。所以用悲观锁时,事务块要尽量短,远程调用、消息推送、文件处理这些耗时操作一定要挪到事务外面。

6.2 乐观锁:版本号CAS,适合读多写少

乐观锁不依赖数据库的锁机制,而是业务层自己实现。最常见的实现方式是加一个version字段:

-- 第一次读取 SELECT id, stock, version FROM product WHERE id = 1; -- 更新时校验版本号 UPDATE product SET stock = stock - 1, version = version + 1 WHERE id = 1 AND version = 5;

UPDATEWHERE条件里带上了version = 5。如果执行后affected rows = 1,说明期间没人改过,更新成功;如果affected rows = 0,说明版本号已被其他事务改掉,需要重试或报错。

这种做法的本质是CAS(Compare And Swap):比较旧版本,一致才更新。优点是完全没有数据库锁等待,并发能力高;缺点是需要业务层处理重试逻辑,而且在高并发下如果冲突频繁,重试次数会直线上升,反而造成更大压力。

乐观锁和悲观锁的选型,我的判断标准很简单:写冲突概率高、对数据一致性要求极严的场景选悲观锁(如库存扣减、账户扣款、订单状态流转);读多写少、冲突概率低、希望最大吞吐的场景选乐观锁(如文章点赞数、浏览数、配置项更新)

还有一个容易出问题的点:CAS更新时必须注意UPDATE语句里的赋值表达式。如果写成SET stock = 5而不是SET stock = stock - 1,并发重试时容易用旧值覆盖新值,丢更新问题依旧存在。正确的乐观锁更新应该是“基于当前值字段做运算”,而不是“基于读出来的旧值做绝对值写入”。

7. 加锁方式轴:隐式锁与显式锁,从自动到手动

最后一条轴跟前五条都不同,它不关心锁的粒度和兼容性,关心的是“锁是怎么加上去的”。MySQL里大部分锁是自动加的,少部分需要你手动下命令,我把它们分成隐式锁和显式锁。

7.1 隐式锁:不用你操心,但要有感知

隐式锁指数据库在DML执行时自动生成的锁。你执行一条UPDATE,InnoDB会自动给目标行加X锁;执行一条INSERT,InnoDB会在插入记录上留下事务ID,这在某种程度上就是一个隐式锁——其他事务访问这条记录时,通过事务ID判断行是否被锁。

之所以叫“隐式”,是因为日常开发中这些锁不需要你写任何SQL去显式声明。但这不代表你可以无视它们。我见过太多线上事故就是这么来的:

  • 一个大事务UPDATE处理了50万行,每行都有隐式X锁,事务不提交,所有涉及这些行的读写全部排队。
  • 删数据时没走索引,隐式锁退化成全表范围的锁,导致业务短时不可用。
  • 唯一键冲突处理不当,插入失败的事务还会持有临时的锁,造成连锁阻塞。

所以隐式锁的正确态度是:不需要你主动加,但你必须能预判“这条SQL执行过程中,自动加了哪些锁”。

7.2 显式锁:手动控制的全部手段

显式锁是指通过SQL主动控制加锁行为的操作,前面提到的LOCK TABLESSELECT ... FOR SHARESELECT ... FOR UPDATE都属于这类。

使用显式锁最大的原则是:一定要在事务内使用,并且保证事务正确结束。很多新手在命令行里直接执行:

SELECT * FROM orders WHERE id = 100 FOR UPDATE;

下一条查询还能正常返回,于是以为锁已经释放了。实际上,在自动提交模式下,这条语句执行完就提交了,锁确实释放了;但一旦你手动BEGIN开启事务而不提交,这个FOR UPDATE锁就会一直保持到你COMMITROLLBACK。这既是显式锁的威力,也是泄漏锁的来源——代码里忘记提交事务,比业务Bug更难发现。

我自己的习惯是,任何使用显式锁的代码,都要在代码评审时重点检查两件事:一是开销事务的方法是否成对出现(BEGIN/COMMIT/ROLLBACK),二是锁的持有范围内不得有网络RPC、睡眠等待、文件IO等耗时逻辑。

7.3 元数据锁(MDL):介于隐式和显式之间的特殊存在

严格来讲,MDL锁(Metadata Lock)是MySQL Server层自动管理的一类锁,它的作用是保护表结构定义在SQL执行期间不被修改。执行任何增删改查时,Server层都会自动对表加MDL读锁;执行ALTER TABLE等DDL时,需要获取MDL写锁。

MDL锁最典型的坑是:某个慢查询一直不结束,它持有的MDL读锁不释放,后边一个ALTER TABLE想加MDL写锁却总是等待;而ALTER TABLE一旦排队,后续所有新查询又会被阻塞。结果就是一张本来没毛病的表,突然全面不可用。遇到这种问题,先把慢查询干掉,DDL才能继续走。

8. 死锁复盘与锁等待排查:几条能直接抄的实战经验

讲完14种锁的理论,最后分享几个我在真实环境里积累的排查经验和排雷技巧。

8.1 死锁的经典复现与根因

死锁的本质是多个事务以不同顺序持有锁,形成循环等待。最典型的是两个事务交叉更新两行数据:

-- 事务A BEGIN; UPDATE orders SET status = 1 WHERE id = 1; UPDATE orders SET status = 2 WHERE id = 2; COMMIT; -- 事务B BEGIN; UPDATE orders SET status = 2 WHERE id = 2; UPDATE orders SET status = 1 WHERE id = 1; COMMIT;

如果A先锁了id=1,B先锁了id=2,然后A想锁id=2、B想锁id=1,就产生了死锁。InnoDB检测到后会自动回滚其中一个事务,并抛出Deadlock found when trying to get lock。这个场景说明了一个好习惯:多行更新时按固定顺序加锁,是规避死锁最廉价的方式。

更隐蔽的死锁来自间隙锁。两个事务同时执行范围查询并准备插入,很容易互相持有对方需要的间隙锁。这类死锁的复现难度高,报错日志里往往看不到具体SQL,只能看到锁16进制记录。排查方式通常是打开innodb_print_all_deadlocks = 1,把死锁详情打到错误日志,再根据事务执行的SQL和锁模式推断。

8.2 锁等待超时的标准排查路径

当你看到Lock wait timeout exceeded; try restarting transaction时,说明有事务持锁时间过长,你的事务等待超过了innodb_lock_wait_timeout(默认50秒)。不要急着重启应用,按下面三步走:

  1. 查当前锁等待关系:
SELECT * FROM performance_schema.data_lock_waits\G

可以看到哪个事务在等谁持的锁。

  1. 查事务状态和持锁详情:
SELECT * FROM performance_schema.data_locks\G SELECT * FROM sys.innodb_lock_waits\G

重点看LOCK_TYPELOCK_MODE,判断是行锁还是间隙锁冲突。

  1. 定位并处理阻塞源:
SELECT * FROM information_schema.innodb_trx\G

找到持锁事务的trx_mysql_thread_id,然后根据情况决定是KILL该连接,还是等它自然提交。如果是业务代码忘了提交,KILL之后还要去修复代码。

8.3 几条从实战里总结出来的锁铁律

结合这些年排查过的案例,我把最值得记住的规则集中列出来,每一条几乎都是血泪换来的:

  • 给高频查询条件建好索引,避免行锁退化成大面积锁。
  • 事务里禁止远程调用和耗时IO,锁的持有时间约等于事务执行时间。
  • 业务量不大但死锁频繁时,优先检查隔离级别和锁顺序,而不是盲目加索引。
  • 唯一键冲突也会加锁,重试逻辑要带上异常捕获,避免无意义的重复插入。
  • 监控里看到Waiting for table metadata lock,赶紧查慢查询或未提交事务,别等表完全不可用才发现。
  • 手动FOR UPDATE后,务必确认事务提交路径上没有提前RETURN的代码分支。

这些经验没有一条来自官方文档的高深理论,全是生产环境里一单一单排查出来的。MySQL的锁体系初看是一堆名词,真正用起来之后你会明白:它不过是一套为了在并发和一致之间找平衡而设计的精密机制。理解六条分类轴,再对照各自场景去实践,比死记硬背十四个定义要可靠得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询