MySQL 锁怎么理解:全局锁、表锁、MDL、行锁与 next-key lock
MySQL 锁章节,讲清锁为什么是隔离级别的实现手段,以及全局锁、表级锁、MDL、意向锁、AUTO-INC 锁、记录锁、间隙锁、next-key lock、共享锁和排他锁的用途。
相关工具
锁是并发正确性的代价
事务讲完以后,锁就自然出现了。多个事务并发执行时,会遇到脏读、不可重复读、幻读、丢失更新等问题。文中说,为了不同程度地解决这些问题,才出现了不同隔离级别,而锁就是实现隔离级别的一种方式。
这句话很关键。锁不是 MySQL 故意让系统变慢,而是在并发场景里保护数据正确性的工具。没有锁,两个事务可能同时修改同一行,谁覆盖谁都说不清;没有范围锁,一个事务正在按条件检查数据,另一个事务可能突然插入一条满足条件的新记录。
理解锁,最实用的角度不是背名字,而是问三个问题:锁住的范围有多大,锁住以后别人还能不能读写,锁什么时候释放。全局锁影响整个实例,表级锁影响整张表,行级锁通常只影响命中的记录或范围。范围越小,并发越好;范围越大,控制越简单,但阻塞也更明显。

从全局锁、表级锁到行级锁,锁粒度逐渐变小,并发能力通常逐渐提高。
全局锁会让整个库进入只读状态
资料 对全局锁的定义很直接:全局锁就是对整个数据库实例加锁。典型使用场景是全库逻辑备份,也就是把整个库的表都 `select` 出来存成文本。MySQL 提供的命令是 `flush tables with read lock`,常缩写为 FTWRL。
一旦执行 FTWRL,整个库会处于只读状态。后续数据更新语句会被阻塞,数据定义语句也会被阻塞,更新类事务的提交语句同样会被阻塞。它保护的是备份时的数据一致性,但代价也非常明显:对业务写入影响很大。
所以全局锁不是日常业务里随手使用的锁。它适合少数需要全库一致视图的管理操作。在线业务场景下,如果数据库还在承接写请求,贸然使用全局锁很容易造成写入堆积。看到 FTWRL,脑子里应该立刻出现两个词:整库和只读。
表锁粒度粗,但概念很直观
表级锁比全局锁小一层,但仍然很粗。文中说,表锁每次操作锁住整张表,开销小、加锁快,但并发度最低。它的语法是 `lock tables ... read/write`,可以用 `unlock tables` 主动释放,也会在客户端断开时自动释放。
表读锁会限制写入,表写锁会让其他事务连查询和修改都要等待。同时特别提醒,`lock tables` 不只限制别的线程读写,也限定本线程接下来的操作对象。也就是说,你锁了某张表以后,自己后续能做什么也会被这条语句约束。
在 InnoDB 里,大多数正常业务读写不应该依赖表锁。后文也说,对于 InnoDB,绝大部分情况应该使用行锁。只有表很大且事务需要更新全部或大部分数据,或者事务涉及多个表、非常复杂、可能引起大量死锁回滚时,才可能考虑表锁这类更粗的方式。
MDL 保护的是表结构
MDL 是 metadata lock,元数据锁。它和普通行数据不一样,保护的是表结构这类元数据。文中说,MDL 不需要显式使用,访问一张表时会自动加上。它的作用是保证读写正确性。
为什么查数据还要保护表结构?已有例子了一个很直观的场景:一个查询正在遍历表数据,另一个线程如果同时修改表结构,比如删除一列,查询线程拿到的结果就可能和表结构对不上。为了避免这种混乱,MySQL 在 5.5 引入 MDL。
对表做增删改查时,会加 MDL 读锁;对表做结构变更时,会加 MDL 写锁。读锁之间不互斥,所以多个线程可以同时查询或修改数据;读写锁之间、写锁之间互斥,用来保证改表结构安全。最容易踩坑的是,事务中的 MDL 锁在语句开始时申请,但语句结束后不会立刻释放,而是等整个事务提交后才释放。长事务拖住 MDL,是很多线上 DDL 卡住的根源。
意向锁是在表上挂一个提示牌
意向锁听起来抽象,其实可以理解成表级别的提示牌。文中说,对某些记录加共享锁之前,需要先在表级别加意向共享锁;对某些记录加独占锁之前,需要先在表级别加意向独占锁。
它的目的不是直接锁住某一行,而是快速判断表里是否已有记录被加锁。后文换了一个更好懂的说法:如果你只锁了房子里的某个房间,可以在房子门口挂一个标识,告诉别人里面已经有人占用了某个局部。别人想拿整套房子的控制权时,不需要一个房间一个房间检查,只看门口标识就知道要不要等待。
意向共享锁和意向独占锁本身是表级锁,但它们不会和行级共享锁、行级独占锁冲突,意向锁之间也不会互相冲突。它们主要和共享表锁、独占表锁发生关系。这个设计让 InnoDB 同时支持表锁和行锁时,能更快判断锁兼容性。
AUTO-INC 锁服务于自增主键
很多表会把主键设置成自增。插入数据时不指定主键,数据库会自动分配递增值。文中说,这个过程通过 AUTO-INC 锁实现。早期方式是在插入数据时加一个表级别的 AUTO-INC 锁,为自增字段赋值,等插入语句执行完成后才释放。
这样能保证自增值连续递增,但缺点也明显:大量插入时,其他事务的插入会被阻塞,影响写入性能。文中也提到,InnoDB 后来提供了更轻量的方式:插入时给 AUTO_INCREMENT 字段加轻量级锁,分配完自增值就释放,不必等整个插入语句执行完。
这类锁平时不一定直接出现在业务代码里,但它解释了为什么批量插入、自增主键、并发写入之间会有性能关系。设计高写入表时,主键生成方式、插入顺序、批量写入策略,都可能影响锁竞争。
行锁粒度小,并发度最高
行锁是针对数据表中行记录的锁。相关的例子很朴素:事务 A 更新了一行,事务 B 也要更新同一行,事务 B 必须等事务 A 完成后才能继续。相比表锁,行锁每次只锁住一行或一个范围,开销更大、加锁更慢,但发生锁冲突的概率最低,并发度最高。
InnoDB 的行锁并不只是一种。Record Lock 是记录锁,只锁某一条记录,可以分为共享锁和排他锁。Gap Lock 是间隙锁,锁定一个范围,但不包含记录本身,主要用于可重复读隔离级别下解决幻读。Next-Key Lock 是记录锁和间隙锁的组合,既锁住记录,也锁住记录前面的间隙。
这也解释了为什么同一条 SQL 在不同索引条件下,锁住的范围可能不一样。用唯一索引等值查询命中一行,锁可能退化为行锁;范围查询或非唯一索引条件下,可能锁住一段索引区间。锁不是抽象地“锁表”或“锁行”,它和索引访问路径紧密相关。
next-key lock 为什么能处理幻读
前一篇讲事务时提到,InnoDB 在可重复读下,快照读用 MVCC,当前读用 next-key lock 处理幻读。现在把它再说具体一点。Record Lock 只能锁已有记录,如果某个范围里还没有记录,只锁记录就挡不住别人往这个空隙里插入新行。
Gap Lock 锁的是记录之间的间隙,可以防止其他事务在这个范围内插入新记录。但它不锁记录本身。Next-Key Lock 把记录锁和间隙锁组合起来,锁定一个范围,并锁定记录本身。文中说,它既能保护记录,又能阻止其他事务把新记录插入到被保护记录前面的间隙中。
在可重复读隔离级别下,next-key lock 的基本单位是前开后闭区间。同时列了两个优化:唯一索引等值查询时,next-key lock 可以退化为行锁;索引等值查询向右遍历且最后一个值不满足等值条件时,可以退化为间隙锁。这里不用死记所有规则,先抓住主线:InnoDB 是沿着索引访问路径加锁,访问到的对象才会加锁。
共享锁和排他锁决定别人能不能动
从数据库角度看,资料 又把锁分成共享锁和排他锁。共享锁也叫读锁或 S 锁,被共享锁锁定的资源可以被其他事务读取,但不能修改。比如想给某一行加共享锁,可以使用 `lock in share mode`。
排他锁也叫独占锁、写锁或 X 锁。被排他锁锁定的数据,只允许持锁事务使用,其他事务无法对这部分数据进行查询或修改。比如 `select ... for update` 会给目标行加排他锁;当执行 `insert`、`delete`、`update` 时,数据库也会自动使用排他锁,防止其他事务同时操作同一行。
共享锁和排他锁也可以放到表上,比如 `lock table ... read` 或 `lock table ... write`。但实际业务里,更常见的是通过事务和 SQL 语句隐式触发行级锁。你不一定手写加锁命令,但只要写了会修改数据的 SQL,就要意识到它会影响其他事务。
乐观锁和悲观锁是两种设计思路
资料 也从从事技术工作的人角度区分了乐观锁和悲观锁。乐观锁认为同一数据的并发冲突不是总会发生,所以不一定每次都依赖数据库锁。常见做法是版本号或时间戳:更新时带上旧版本号,只有版本号没变才更新成功。失败了说明有人先改过,再由业务决定重试或提示用户。
悲观锁则更保守,认为数据很可能被别人改,所以先通过数据库锁拿到排他控制权,再执行操作。比如库存扣减、账户余额修改这类写冲突较多的场景,经常要更谨慎地使用悲观锁或其他强约束方式。
两者没有绝对好坏。相关的判断也很实用:乐观锁适合读多写少,优点是程序实现,不容易带来数据库死锁,但防不住程序之外的数据库操作;悲观锁适合写多场景,能在数据库层阻止读写和写写冲突,但锁时间长时并发会变差。
写事务时把容易冲突的锁放后面
文中有一句对编码很有用:InnoDB 事务中,行锁是在需要的时候才加上的,但不是不需要了就立刻释放,而是要等事务结束时才释放。这就是两阶段锁协议。理解这点以后,写事务就不能只看 SQL 对不对,还要看加锁顺序。
如果一个事务里要操作多行数据,最可能造成锁冲突、最可能影响并发的操作,尽量往后放。比如先做不加锁的参数检查、数据准备,再尽快执行关键更新并提交。不要在拿到行锁以后,又去调用外部接口、做复杂计算、等待用户输入或执行无关查询。
排查锁问题时,也按范围去看:是不是全局锁让库只读了;是不是表锁或 MDL 卡住了 DDL;是不是长事务迟迟不提交;是不是某条范围查询通过 next-key lock 锁住了比预期更大的区间。锁的名字很多,但落到线上,核心就是谁持有、谁等待、锁住了多大范围、什么时候释放。
常见问题
MySQL 全局锁一般用在什么场景?
典型场景是全库逻辑备份。执行 FTWRL 后,整个实例进入只读状态,更新、DDL 和更新类事务提交都会被阻塞,所以线上使用要非常谨慎。
MDL 为什么会导致 DDL 卡住?
事务访问表时会自动加 MDL 读锁,表结构变更需要 MDL 写锁。事务中的 MDL 锁要等事务提交后才释放,如果有长事务一直不结束,DDL 就可能一直等待。
Record Lock、Gap Lock、Next-Key Lock 有什么区别?
Record Lock 锁已有记录;Gap Lock 锁记录之间的间隙,不含记录本身;Next-Key Lock 是记录锁加间隙锁,既锁记录,也锁范围,用于阻止并发插入导致的幻读。
乐观锁和悲观锁怎么选?
读多写少、冲突概率低时可以考虑乐观锁,用版本号或时间戳控制更新;写冲突多、正确性要求高时更适合悲观锁或数据库约束,但要控制锁持有时间。