MySQL 锁相关

2025/06/06

Categories: 技术 Tags: MySQL

数据库锁设计的初衷是处理并发问题。作为多用户共享的资源,当出现并发访问的时候,数据库需要合理地控制资源的访问规则。而锁就是用来实现这些访问规则的重要数据结构。

MySQL 锁的分类

根据加锁的范围,MySQL 里面的锁大致可以分成全局锁、表级锁和行级锁三类。

全局锁

全局锁就是对整个数据库实例加锁。可以通过下面命令操作全局锁:

-- 全局读锁
FLUSH TABLES WITH READ LOCK;

-- 释放锁
UNLOCK TABLES;

加全局读锁后,整个库就处于只读状态了,之后其他线程的以下语句会被阻塞:

全局锁主要应用于做全库逻辑备份,这样在备份数据库期间,不会因为数据或表结构的更新,而出现备份文件的数据与预期的不一样。

加上全局锁后,整个数据库都是只读状态,这期间业务只能读取而不能更新数据,会导致业务停滞。如果数据库引擎支持事务的『可重复读隔离级别』,在备份数据库之前开启事务,会先创建读视图,整个事务执行期间都在用这个读视图,由于 MVCC 的支持,备份期间业务依然可以对数据进行更新操作。

备份数据库的工具是 mysqldump,在使用 mysqldump 时加上 -single-transaction 参数的时候,就会在备份数据库之前先开启事务。这种方法只适用于支持『可重复读隔离级别的事务』的存储引擎。InnoDB 存储引擎默认就是可重复读隔离级别,可以采用这种方式来备份数据库。对于不支持事务的存储引擎如 MyISAM,在备份时只能使用全局锁的方式。

表级锁

MySQL 里面表级别的锁有两种:一种是表锁,一种是元数据锁(METADATA LOCK)。

表锁

可以通过下面命令操作表锁:

-- 表级别的共享锁,也就是读锁;允许当前会话读取被锁定的表,但阻止其他会话对这些表进行写操作。
LOCK TABLES table_name READ;

-- 表级别的独占锁,也就是写锁;允许当前会话对表进行读写操作,但阻止其他会话对这些表进行任何操作(读或写)。
LOCK TABLES table_name WRITE;

-- 释放当前会话的所有表锁。
UNLOCK TABLES;

在还没有出现更细粒度的锁的时候,表锁是最常用的处理并发的方式,不过尽量避免在使用 InnoDB 引擎的表使用表锁,因为表锁的颗粒度太大,会影响并发性能。InnoDB 有更细粒度的行级锁支持。

元数据锁 MDL

MDL 不需要显式使用,在访问一个表的时候会被自动加上:

MDL 的作用是保证读写的正确性。当用户对表执行增删改查操作时,防止其他线程对这个表结构做了变更。

行级锁

MySQL中的行锁是 InnoDB 引擎实现高并发控制的核心机制,主要作用于单行数据。根据锁定的范围和目的,行锁可分为以下类型:

记录锁(Record Locks)

记录锁锁住的是一条记录,有共享锁(S 锁)和独占锁(X 锁)之分。S 锁满足读读共享,读写互斥;X 锁满足写写互斥、读写互斥。

普通的 SELECT 语句是不会对记录加锁的(MVCC 机制保证),因为它属于快照读。如果要在查询时对记录加行锁,可以使用下面这两个方式,这种查询会加锁的语句称为锁定读。注意这两条语句必须在事务中,因为当事务提交了,锁就会被释放。

-- 对读取的记录加共享锁 (S 锁)
SELECT ... LOCK IN SHARE MODE;

-- 对读取的记录加独占锁(X 锁)
SELECT ... FOR UPDATE;

S 锁和 X 锁的兼容性如下:

X 型记录锁 S 型记录锁
X 型记录锁 不兼容 不兼容
S 型记录锁 不兼容 兼容

间隙锁(Gap Locks)

间隙锁只存在于可重复读隔离级别,作用于索引记录之间的间隙(如 id=5 和 id=10 之间的区间),目的是为了解决可重复读隔离级别下幻读的现象。

间隙锁也存在 X型间隙锁和 S 型间隙锁,间隙锁之间是兼容的。即两个事务可以同时持有包含共同间隙范围的间隙锁,并不存在互斥关系,因为间隙锁的目的是防止插入幻影记录而提出的。

临键锁(Next-Key Locks)

临键锁是 Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。左开右闭区间(如 (5, 10] 表示锁定 5 到 10 的区间和 id=10 的记录),目的是同时防止幻读和当前记录被修改。

插入意向锁(Insert Intention Locks)

一个事务在插入一条记录的时候,需要判断插入位置是否已被其他事务加了间隙锁。如果有的话,插入操作就会发生阻塞,直到拥有间隙锁的那个事务提交为止,在此期间会生成一个插入意向锁,表明有事务想在某个区间插入新记录,但是现在处于等待状态。插入意向锁的目的是提高并发插入效率,允许多个事务在同一间隙不同位置插入。

死锁问题

MySQL死锁是数据库中的经典问题,发生在两个或多个事务彼此等待对方持有的资源(通常是锁),形成一个循环等待链,导致所有相关事务都无法继续执行。当出现死锁以后,有两种策略:

将参数 innodb_deadlock_detect 设置为 on开启死锁检测, InnoDB 引擎会自动检测死锁并回滚代价最小的一个事务(如 Undo 量最小的事务)来打破僵局。

MySQL 的两阶段锁协议

在 InnoDB 事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放这个就是『两阶段锁协议』。

MySQL 死锁出现的典型场景

不同顺序的更新或删除操作

事务 A 和事务 B 都需要修改相同的两行或多行数据,但它们以相反的顺序访问这些行。

  1. 事务A:UPDATE table SET … WHERE id = 1; (获得 id=1 的行锁)
  2. 事务B:UPDATE table SET … WHERE id = 2; (获得 id=2 的行锁)
  3. 事务A:UPDATE table SET … WHERE id = 2; (尝试获取 id=2 的行锁,但被事务 B 持有,事务 A 等待)
  4. 事务B:UPDATE table SET … WHERE id = 1; (尝试获取 id=1 的行锁,但被事务 A 持有,事务 B 等待)

事务 A 等待 B 释放 id=2 的锁,B 等待 A 释放 id=1 的锁,循环等待,死锁发生。

其它相关问题

为什么 MDL 写锁被占用会阻塞后续的所有的读写操作?

事务中的 MDL 锁在语句执行开始时申请,但是语句结束后并不会马上释放,而会等到整个事务提交后再释放。申请 MDL 锁的操作会形成一个队列,队列中写锁获取优先级高于读锁,一旦出现 MDL 写锁等待,会阻塞后续该表的所有读写操作。

所以为了能安全的对表结构进行变更,在对表结构变更前,先要看看数据库中的长事务,是否有事务已经对表加上了 MDL 读锁,如果可以考虑 KILL 掉这个长事务,然后再做表结构的变更。

MySQL DDL 无锁变更的原理和流程?

MySQL DDL 无锁变更旨在避免传统 DDL 操作中因锁表导致的业务中断问题。其核心原理是通过创建临时表、数据同步、逐步切换的方式,在保证数据一致性的前提下实现对表结构的修改。

无锁 DDL 的核心思想是『双表同步』 ,通过以下步骤实现:

  1. 临时表创建:根据新表结构创建临时表,并在其上执行 DDL 操作(如新增字段、修改列类型等)。
  2. 数据同步:将原表的全量数据拷贝到临时表,并捕获原表在拷贝期间的增量数据(如通过 Binlog 或触发器)。
  3. 原子切换:在数据完全同步后,通过原子操作(如重命名表名)将原表与临时表替换,完成结构变更。

参考『阿里云 DMS 无锁结构变更』和『几种无锁结构变更方案的对比』。

参考文档