文章

MySQL锁机制详解

MySQL的锁机制是数据库并发控制的核心,用于保证数据一致性和完整性。本文将全面介绍MySQL中的各种锁类型、其目的、工作原理以及应用场景。

一、锁的基本概念

锁是数据库用于控制并发访问的机制,确保在多用户环境下数据的一致性。MySQL中的锁根据粒度、模式和实现方式可分为多种类型。

锁的基本术语

中文名称        | 英文缩写 | 完整英文名称
---------------|---------|-------------
共享锁         | S锁     | Shared Lock
排他锁         | X锁     | Exclusive Lock
意向共享锁     | IS锁    | Intention Shared Lock
意向排他锁     | IX锁    | Intention Exclusive Lock
记录锁         | -       | Record Lock
间隙锁         | -       | Gap Lock
临键锁         | -       | Next-Key Lock
插入意向锁     | II锁    | Insert Intention Lock

二、按锁的粒度分类

1. 全局锁

定义:对整个数据库实例加锁。

获取方式FLUSH TABLES WITH READ LOCK

目的:获取全局一致性视图,主要用于全库逻辑备份。

特点

  • 阻塞所有写操作

  • 阻塞更新表结构的操作

  • 阻塞已经申请但未释放写锁的事务

释放方式

UNLOCK TABLES;

2. 表级锁

2.1 表锁

定义:最基本的锁策略,锁定整张表。

获取方式

-- 读锁(共享锁)
LOCK TABLES 表名 READ;
​
-- 写锁(排他锁)
LOCK TABLES 表名 WRITE;

目的

  • 读锁:允许多个事务同时读取表,但阻止写入

  • 写锁:允许持有锁的事务读写表,阻止其他事务读写

特点

  • 开销小,加锁快

  • 不会出现死锁

  • 锁定粒度大,发生锁冲突的概率高

2.2 元数据锁(MDL锁)

定义:保护表结构的锁。

获取方式:系统自动添加,无需手动干预。

目的:防止在表被访问时修改表结构。

特点

  • 读取数据时自动加MDL读锁

  • 修改表结构时自动加MDL写锁

  • MDL读锁之间不互斥,但与MDL写锁互斥

2.3 意向锁

定义:表明事务想要在表的某些行上加锁的意图。

类型

  • 意向共享锁(IS锁):表示事务想要获取表中某些行的共享锁

  • 意向排他锁(IX锁):表示事务想要获取表中某些行的排他锁

获取方式:当事务需要获取行锁时,InnoDB会自动在表上加相应的意向锁。

目的:避免在加表级锁时逐行检查是否有行锁,提高效率。

特点

  • 意向锁之间不互斥

  • 意向锁与表锁互斥

3. 行级锁

行级锁是InnoDB存储引擎的特色,粒度最小,支持最大并发,但开销也最大。

3.1 记录锁(Record Lock)

定义:锁定索引记录

获取方式

-- 共享记录锁
SELECT * FROM 表名 WHERE 条件 LOCK IN SHARE MODE;
​
-- 排他记录锁
SELECT * FROM 表名 WHERE 条件 FOR UPDATE;

目的:防止其他事务修改或删除记录。

特点

  • 只锁定索引记录,不锁定记录之间的间隙

  • 必须有索引,否则会进行全表扫描,对表中的每一行记录加上锁(可重复读级别下是Next-Key Lock),这会导致锁定范围非常大,效果类似于表锁,性能急剧下降

  • 可以是共享锁或排他锁

3.2 间隙锁(Gap Lock)

定义:锁定索引记录之间的间隙。

获取方式:在REPEATABLE READ隔离级别下,使用范围条件查询时自动应用。

目的:防止其他事务在间隙中插入数据,解决幻读问题。

特点

  • 仅在REPEATABLE READ及以上隔离级别存在

  • 锁定的是开区间,不包含边界记录

  • 间隙锁之间不互斥,但与插入意向锁互斥

3.3 临键锁(Next-Key Lock)

定义:记录锁与间隙锁的组合,锁定记录及其前面的间隙。

获取方式:在REPEATABLE READ隔离级别下,针对范围查询自动应用。

目的:全面防止幻读。

特点

  • 锁定范围是前开后闭区间,如(1,5]表示锁定间隙(1,5)和记录5

  • InnoDB默认的行锁算法

  • 可以解决幻读问题

3.4 插入意向锁(Insert Intention Lock)

定义:一种特殊的间隙锁,表明插入意图。

获取方式:在执行INSERT操作前自动获取。

目的:提高并发插入性能。

特点

  • 多个事务可以在同一间隙中获取插入意向锁(如果插入位置不同)

  • 与间隙锁和Next-Key Lock互斥

三、按锁的模式分类

1. 共享锁(S锁)

定义:允许多个事务同时读取同一资源。

获取方式

SELECT * FROM 表名 WHERE 条件 LOCK IN SHARE MODE;

目的:允许并发读,阻止写入。

特点

  • 多个事务可以同时持有同一资源的S锁

  • S锁与X锁互斥

2. 排他锁(X锁)

定义:独占锁,阻止其他事务获取任何类型的锁。

获取方式

SELECT * FROM 表名 WHERE 条件 FOR UPDATE;
UPDATE 表名 SET 字段=值 WHERE 条件;
DELETE FROM 表名 WHERE 条件;

目的:确保只有一个事务能修改数据。

特点

  • 一个资源上只能有一个X锁

  • X锁与任何锁都互斥(除了间隙锁)

四、锁的兼容性

1. 表级锁的兼容性矩阵

已持有锁\请求锁 | 意向共享锁(IS) | 意向排他锁(IX) | 共享锁(S) | 排他锁(X)
--------------|--------------|--------------|----------|----------
意向共享锁(IS) |      √       |      √       |    √     |    ×
意向排他锁(IX) |      √       |      √       |    ×     |    ×
共享锁(S)     |      √       |      ×       |    √     |    ×
排他锁(X)     |      ×       |      ×       |    ×     |    ×

说明:

  • ✓表示兼容,可以同时持有

  • × 表示不兼容,会阻塞等待

2. 行级锁的兼容性矩阵

已持有锁\请求锁 | 共享锁(S) | 排他锁(X) | 间隙锁(Gap) | 插入意向锁(II)
--------------|----------|----------|------------|---------------
共享锁(S)     |    √     |    ×     |     √      |       √
排他锁(X)     |    ×     |    ×     |     √      |       ×
间隙锁(Gap)   |    √     |    √     |     √      |       ×
插入意向锁(II) |    √     |    ×     |     ×      |       √

说明:

  • ✓ 表示兼容,可以同时持有

  • × 表示不兼容,会阻塞等待

  • 间隙锁之间是兼容的,这是因为间隙锁的目的是防止插入,而不是防止其他会话再加间隙锁

  • 间隙锁与记录锁(S/X)是兼容的,因为它们锁定的对象不同

  • 插入意向锁与间隙锁互斥,这确保了在有间隙锁的情况下无法插入记录

五、不同隔离级别下的锁行为

1. READ UNCOMMITTED(读未提交)

  • 读操作不加锁

  • 写操作加排他锁

  • 不使用间隙锁和Next-Key Lock

  • 可能出现脏读、不可重复读和幻读

2. READ COMMITTED(读已提交)

  • 读操作使用快照读(MVCC),不加锁

  • 写操作加记录锁,不加间隙锁

  • 每次读取创建新的ReadView

  • 解决脏读,但可能出现不可重复读和幻读

3. REPEATABLE READ(可重复读,InnoDB默认)

  • 读操作使用快照读(MVCC),不加锁

  • 写操作使用Next-Key Lock

  • 事务第一次读取时创建ReadView并复用

  • 解决脏读和不可重复读,默认也能解决大部分幻读情况

4. SERIALIZABLE(串行化)

  • 读操作自动转换为SELECT ... LOCK IN SHARE MODE

  • 写操作加Next-Key Lock

  • 完全串行化执行

  • 解决所有并发问题,但性能最差

六、行锁实例分析

1. 记录锁示例

假设有表users(id PK, name),数据为:(1,'张三')、(5,'李四')、(10,'王五')

-- 事务A
BEGIN;
SELECT * FROM users WHERE id = 5 FOR UPDATE;
-- 此时id=5的记录上有排他锁
​
-- 事务B
BEGIN;
SELECT * FROM users WHERE id = 5 FOR UPDATE; -- 会被阻塞
SELECT * FROM users WHERE id = 1 FOR UPDATE; -- 可以执行,因为id=1没有被锁定

2. 间隙锁示例

-- 事务A
BEGIN;
SELECT * FROM users WHERE id > 1 AND id < 10 FOR UPDATE;
-- 此时(1,5)和(5,10)上有间隙锁
​
-- 事务B
BEGIN;
INSERT INTO users VALUES (3, '赵六'); -- 会被阻塞,因为(1,5)上有间隙锁
INSERT INTO users VALUES (11, '钱七'); -- 可以执行,因为(10,正无穷)上没有间隙锁

3. Next-Key Lock示例

-- 事务A
BEGIN;
SELECT * FROM users WHERE id >= 5 AND id <= 10 FOR UPDATE;
-- 此时(1,5]和(5,10]上有Next-Key Lock
​
-- 事务B
BEGIN;
INSERT INTO users VALUES (3, '赵六'); -- 可以执行,因为(1,5)上只有(1,5]的Next-Key Lock,不包含3
INSERT INTO users VALUES (7, '钱七'); -- 会被阻塞,因为(5,10]上有Next-Key Lock

七、特殊锁类型

1. 自增锁(Auto-increment Lock)

定义:专门针对自增列的特殊表级锁。

控制参数innodb_autoinc_lock_mode

  • 0:传统模式,使用表级锁

  • 1:连续模式,批量插入使用表锁,单行插入使用轻量级锁

  • 2:交错模式,所有插入使用轻量级锁

目的:保证自增值的唯一性和连续性。

2. 隐式锁(Implicit Lock)

定义:InnoDB自动添加的锁,不在锁管理器中显式记录。

目的:减少锁开销,提高并发性能。

工作原理

  • 插入新记录时,不立即在内存中创建锁结构

  • 其他事务尝试锁定该记录时,检查记录的事务ID

  • 如果记录由活跃事务创建,则将隐式锁转换为显式锁并等待

八、锁的监控与分析

1. 查看当前锁状态

-- 查看当前事务
SELECT * FROM information_schema.innodb_trx;
​
-- 查看当前锁
SELECT * FROM performance_schema.data_locks;
​
-- 查看锁等待
SELECT * FROM performance_schema.data_lock_waits;
​
-- 查看InnoDB状态,包含最近的死锁信息
SHOW ENGINE INNODB STATUS;

2. 死锁分析

死锁是指两个或多个事务互相持有对方需要的锁,导致都无法继续执行的情况。

死锁检测与处理

  • InnoDB自动检测死锁

  • 发现死锁后,会回滚undo量最小的事务

  • 可通过SHOW ENGINE INNODB STATUS查看死锁信息

避免死锁的方法

  1. 按固定顺序访问表和行

  2. 减少事务的大小和持续时间

  3. 使用合理的索引

  4. 降低隔离级别

  5. 避免使用LOCK IN SHARE MODEFOR UPDATE

九、锁优化策略

1. 索引优化

  • 确保查询条件使用索引,避免全表扫描导致的表锁

  • 避免索引失效:如使用函数、类型转换、不等于、or等

2. 事务优化

  • 控制事务大小,减少锁定时间

  • 将大事务拆分为小事务

  • 按固定顺序访问表和行,避免死锁

3. 隔离级别选择

  • 根据业务需求选择合适的隔离级别

  • 不需要防止幻读的场景可以使用READ COMMITTED

4. 参数调优

  • innodb_lock_wait_timeout:锁等待超时时间(默认50秒)

  • innodb_deadlock_detect:死锁检测(默认开启)

  • innodb_autoinc_lock_mode:自增锁模式

十、总结

MySQL的锁机制是保证数据一致性的核心机制,特别是InnoDB的行锁机制,通过多种类型的锁实现了高并发下的数据保护。理解不同类型锁的目的、行为和兼容性,对于设计高性能、高可靠性的数据库应用至关重要。

在实际应用中,应根据业务特点选择合适的锁策略,平衡数据一致性和系统性能的需求,通过合理的索引设计、事务控制和监控分析,最大限度地发挥MySQL锁机制的优势。

License:  CC BY 4.0