MySQL 锁机制概述
锁是数据库系统实现并发控制的核心机制,MySQL 通过各种类型的锁保证数据的一致性和完整性。理解 MySQL 的锁机制对于开发高性能、高可靠性的数据库应用至关重要。
在 MySQL 里,根据加锁的范围,可以分为全局锁、表级锁和行锁三类。这三类锁的粒度逐渐变细,加锁范围逐渐变小,并发性能也逐渐提升。
全局锁
全局锁是 MySQL 中粒度最大的锁,它会锁定整个数据库实例的所有表,使整个数据库处于只读状态。
全局锁的使用方法
要使用全局锁,需要执行以下命令:
1 | FLUSH TABLES WITH READ LOCK |
执行后,整个数据库就处于只读状态,这时其他线程执行以下操作,都会被阻塞:
- 对数据的增删改操作,比如 INSERT、DELETE、UPDATE 等语句
- 对表结构的更改操作,比如 ALTER TABLE、DROP TABLE 等语句
全局锁的释放方法
如果要释放全局锁,执行以下命令:
1 | UNLOCK TABLES |
当然,当会话断开时,全局锁会被自动释放。
全局锁的应用场景
全局锁主要应用于全库逻辑备份,这样在备份数据库期间,不会因为数据或表结构的更新,而导致备份文件数据与实际不一致。
不使用全局锁可能带来的问题
假设在全库逻辑备份期间不加全局锁,可能会出现以下情况:
- 先备份了用户表的数据
- 然后有用户发起了购买商品的操作
- 接着再备份商品表的数据
在这种情况下,备份结果会出现:用户表中该用户的余额并没有扣除,而商品表中该商品的库存却被减少了。如果用这个备份文件恢复数据,就会导致用户没花钱就得到了商品。
全局锁的缺点
加上全局锁会导致整个数据库都是只读状态,这会带来以下问题:
- 如果数据库有大量数据,备份会花费很长时间
- 在备份期间,业务只能读数据而不能更新数据,可能导致业务停滞
全局锁的替代方案
对于支持可重复读隔离级别的存储引擎(如 InnoDB),可以采用以下替代方案:
在备份前先开启事务,利用 MVCC 机制,即使其他事务更新了表的数据,也不会影响备份数据时的 Read View。
使用 mysqldump 工具时,可以加上 --single-transaction
参数:
1 | mysqldump --single-transaction -uroot -p database > backup.sql |
这种方法只适用于支持"可重复读隔离级别"的存储引擎。对于 MyISAM 这种不支持事务的引擎,仍需使用全局锁方式备份。
表级锁
**表级锁是 MySQL 中粒度中等的锁,它锁定整张表,而不是表中的某一行或某些行。**MySQL 中的表级锁主要有以下几种:
- 表锁(Table Lock)
- 元数据锁(MDL,Metadata Lock)
- 意向锁(Intention Lock)
- AUTO-INC 锁(自增锁)
表锁
表锁是最基本的锁策略,锁定整张表,分为读锁(共享锁)和写锁(独占锁)。
表锁的使用方法
1 | -- 表级共享锁(读锁) |
表锁的特点
- 表锁会限制其他线程的读写操作,也会限制当前线程接下来的读写操作
- 如果当前线程对表加了共享锁,那么当前线程后续也无法对该表进行写操作
- 表锁粒度较大,会影响并发性能
释放表锁的方法
1 | UNLOCK TABLES |
当会话退出后,也会自动释放所有表锁。
元数据锁(MDL)
**元数据锁是 MySQL 5.5 版本引入的表级锁,用于保护表的结构(元数据)。**它在对表执行操作时自动加锁,无需显式调用。
MDL 锁的类型
- MDL 读锁:对表进行 CRUD 操作时自动加上
- MDL 写锁:对表结构进行变更操作时自动加上
MDL 锁的作用
MDL 的主要目的是防止在表结构变更时,有其他线程正在对表进行 CRUD 操作,避免数据不一致。
- 当有线程在执行 SELECT 语句(MDL 读锁)期间,其他线程要变更表结构(申请 MDL 写锁)会被阻塞
- 当有线程在变更表结构(MDL 写锁)期间,其他线程执行 CRUD 操作(申请 MDL 读锁)会被阻塞
MDL 锁的释放时机
MDL 锁在事务提交后才会释放,这意味着事务执行期间,MDL 是一直持有的。
MDL 锁可能引发的问题
如果数据库有长时间未提交的事务(长事务),而此时有表结构变更操作,可能会引发连锁阻塞:
- 线程 A 开启事务执行 SELECT,获取 MDL 读锁
- 线程 B 也执行 SELECT,也获取 MDL 读锁(读读不冲突)
- 线程 C 要修改表结构,申请 MDL 写锁,被阻塞(读写冲突)
- 之后其他线程执行 SELECT 语句,因为 MDL 写锁请求排队在前,新的 MDL 读锁请求也会被阻塞
这是因为申请 MDL 锁的操作会形成一个队列,写锁获取优先级高于读锁,一旦出现 MDL 写锁等待,会阻塞后续该表的所有操作。
意向锁
意向锁是 InnoDB 存储引擎在表级别上引入的一种锁,用于表示事务稍后在表中的记录上加锁的意向。
意向锁的分类
- 意向共享锁(IS):表示事务意图在表中的某些记录上加共享锁
- 意向独占锁(IX):表示事务意图在表中的某些记录上加独占锁
意向锁的作用机制
- 在对记录加共享锁前,需要先在表级别加上意向共享锁
- 在对记录加独占锁前,需要先在表级别加上意向独占锁
普通的 SELECT 不会加行级锁,而是利用 MVCC 实现一致性读。但以下语句会加锁:
1 | -- 先在表上加意向共享锁,然后对读取的记录加共享锁 |
意向锁的兼容性
意向锁之间不会冲突,只会与表锁(共享表锁和独占表锁)冲突。
意向锁的主要目的是为了快速判断表里是否有记录被加锁,提高加表锁时的效率。如果没有意向锁,加独占表锁时需要遍历表中所有记录,检查是否有记录被加锁,效率很低。
AUTO-INC 锁
AUTO-INC 锁是一种特殊的表级锁,专门用于处理自增主键的分配。
AUTO-INC 锁的工作机制
当表的主键设置了 AUTO_INCREMENT 属性后,插入数据时如果不指定主键值,系统会自动分配一个递增的值。
在插入数据时,MySQL 会先获取 AUTO-INC 锁,然后为 AUTO_INCREMENT 字段赋值,之后立即释放锁(不需要等到事务提交)。
AUTO-INC 锁的性能问题及优化
AUTO-INC 锁在大量数据插入操作时会影响性能,因为其他插入操作需要等待锁释放。
从 MySQL 5.1.22 版本开始,InnoDB 引入了轻量级锁来优化自增锁机制:
- 轻量级锁在获取自增值后立即释放,不等待整个插入语句完成
AUTO-INC 锁的控制参数
InnoDB 提供了 innodb_autoinc_lock_mode
参数来控制自增锁行为:
innodb_autoinc_lock_mode = 0
:使用传统的 AUTO-INC 锁,语句执行完才释放innodb_autoinc_lock_mode = 2
:使用轻量级锁,获取自增值后立即释放innodb_autoinc_lock_mode = 1
:混合模式- 普通 INSERT 语句使用轻量级锁
- 批量插入语句(如 INSERT…SELECT)使用 AUTO-INC 锁
自增锁与主从复制的注意事项
当 innodb_autoinc_lock_mode = 2
并且 binlog_format = statement
时,在主从复制场景中可能会导致数据不一致问题。
主从不一致的案例
当两个会话并发插入数据时:
- Session B 先插入了记录(1,1,1)、(2,2,2)
- 然后 Session A 插入记录(3,5,5)
- 之后 Session B 继续插入记录(4,3,3)、(5,4,4)
这导致 Session B 获得的自增 ID 不连续(1,2,4,5),而中间的 3 被 Session A 获取。
主库发生这种并发插入时,如果 binlog 格式为 statement,主从库执行顺序不同,会导致从库上生成的数据与主库不一致。
正确配置:当 innodb_autoinc_lock_mode = 2
时,应使用 binlog_format = row
,这样从库会使用与主库相同的自增值。
行级锁
**行级锁是 MySQL 中粒度最细的锁,它只锁定特定的行,而不是整张表,这大大提高了并发处理能力。**行级锁只在存储引擎层实现,InnoDB 支持行级锁,而 MyISAM 不支持。
行级锁的使用方法
普通的 SELECT 语句不会对记录加锁。如果需要在查询时加锁,可以使用以下方式:
1 | -- 对读取的记录加共享锁(S锁) |
这两条语句必须在事务中使用,因为事务提交后锁会自动释放。使用时需要加上 BEGIN、START TRANSACTION 或设置 autocommit=0。
行级锁的兼容性
行级锁有共享锁(S 锁)和独占锁(X 锁)之分,它们的兼容关系如下:
- S 锁与 S 锁兼容(读读共享)
- S 锁与 X 锁不兼容(读写互斥)
- X 锁与 X 锁不兼容(写写互斥)
行级锁的类型
InnoDB 实现了以下几种行级锁:
- Record Lock(记录锁):锁定单个行记录
- Gap Lock(间隙锁):锁定一个范围,但不包含记录本身
- Next-Key Lock(临键锁):Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身
- 插入意向锁:一种特殊的间隙锁,表示插入意向
Record Lock(记录锁)
记录锁是最基本的行锁,它锁定的是索引记录,而非记录所在的物理页。
记录锁分为 S 型记录锁和 X 型记录锁:
- 当事务对记录加了 S 型记录锁,其他事务可以继续加 S 锁,但不能加 X 锁
- 当事务对记录加了 X 型记录锁,其他事务既不能加 S 锁,也不能加 X 锁
示例:
1 | BEGIN; |
这条语句会对主键 id=1 的记录加上 X 型记录锁,阻止其他事务修改该记录。
Gap Lock(间隙锁)
间隙锁锁定索引记录之间的间隔,防止其他事务在这个间隔中插入数据,从而避免幻读问题。
间隙锁只在可重复读隔离级别下存在,目的是解决幻读问题。
例如,表中有一个范围 id 为(3,5)的间隙锁,其他事务就无法插入 id=4 的记录,有效防止了幻读。
间隙锁的特殊性:虽然有 S 型和 X 型之分,但间隙锁之间是相互兼容的。两个事务可以同时持有包含共同间隙范围的间隙锁,因为间隙锁的目的只是防止插入操作。
Next-Key Lock(临键锁)
Next-Key Lock 是 Record Lock 和 Gap Lock 的组合,既锁定一个范围,又锁定记录本身。
例如,表中有一个范围 id 为(3,5]的 Next-Key Lock,其他事务既不能插入 id=4 的记录,也不能修改 id=5 的记录。
Next-Key Lock 既能保护记录,又能阻止在记录前的间隙中插入新记录,是 InnoDB 默认的行锁算法。
Next-Key Lock 的冲突规则:由于包含了记录锁,如果一个事务获取了 X 型 Next-Key Lock,其他事务无法获取相同范围的 X 型 Next-Key Lock。
插入意向锁
插入意向锁是一种特殊的间隙锁,表示事务想要在某个区间插入记录的意向。
当一个事务要插入一条记录时,需要检查插入位置是否被其他事务加了间隙锁:
- 如果有间隙锁,插入操作会被阻塞
- 此时会生成一个插入意向锁,状态为等待,直到间隙锁释放
例如,事务 A 已经对表加了(3,5)间隙锁:
当事务 B 尝试插入 id=4 的记录时,会被阻塞并生成一个插入意向锁,直到事务 A 提交。
插入意向锁与间隙锁的关系:
- 插入意向锁是一种特殊的间隙锁,但锁住的是一个点而非区间
- 插入意向锁与间隙锁不兼容:同一时间内,不能一个事务持有间隙锁,另一个事务持有该间隙内的插入意向锁