本文主要介绍MySQL InnoDB引擎中的各种锁策略和锁类别,并针对记录锁做演示以便于理解。
以下内容适用于MySQL 8.0版本。
读写锁
处理并发读/写访问的系统通常实现一个由两种锁类型组成的锁系统。这两种锁通常被称为共享锁(shared lock)和排他锁(exclusive lock),也叫读锁(read lock)和写锁(write lock)。
读锁
资源上的读锁是共享的,或者说是相互不阻塞的。多个客户端可以同时读取同一个资源而互不干扰。
写锁
写锁则是排他的,也就是说,一个写锁既会阻塞读锁也会阻塞其他的写锁,这是出于安全策略的考虑,只有这样才能确保在特定的时间点只有一个客户端能执行写入,并防止其他客户端读取正在写入的资源。
锁粒度
一种提高共享资源并发性的方式就是让锁定对象更有选择性。尽量只锁定包含需要修改的部分数据,而不是所有的资源。但是锁的范围越小,管理锁的逻辑就会越复杂,开销也越大。因此锁定策略是锁开销和数据安全性之间的平衡,这种平衡会影响性能。MySQL提供了两种粒度的锁。
表锁
InnoDB的表级别锁包含五种锁模式:LOCK_IS、LOCK_IX、LOCK_X、LOCK_S以及LOCK_AUTO_INC锁。
LOCK_IS/LOCK_IX
也就是所谓的意向锁,这实际上可以理解为一种“暗示”未来需要什么样行级锁,IS表示未来可能需要在这个表的某些记录上加共享锁,IX表示未来可能需要在这个表的某些记录上加排他锁。意向锁是表级别的,IS和IX锁之间相互并不冲突,但与表级S/X锁冲突。
在对记录加S锁或者X锁时,必须保证其在相同的表上有对应的意向锁或者锁强度更高的表级锁。
LOCK_X
当加了LOCK_X表级锁时,所有其他的表级锁请求都需要等待。X锁的几个情况:
-
DDL操作的最后一个阶段(
ha_innobase::commit_inlace_alter_table
)对表上加LOCK_X锁,以确保没有别的事务持有表级锁。通常情况下Server层MDL锁已经能保证这一点了,在DDL的commit 阶段是加了排他的MDL锁的。但诸如外键检查或者刚从崩溃恢复的事务正在进行某些操作,这些操作都是直接InnoDB自治的,不走server层,也就无法通过MDL所保护; -
当设置会话的autocommit变量为OFF时,执行
LOCK TABLE tbname WRITE
这样的操作会加表级的LOCK_X锁(ha_innobase::external_lock
); -
对某个表空间执行discard或者import操作时,需要加LOCK_X锁(
ha_innobase::discard_or_import_tablespace
)。
LOCK_S
-
在DDL的第一个阶段,如果当前DDL不能通过ONLINE的方式执行,则对表加LOCK_S锁(
prepare_inplace_alter_table_dict
); -
设置会话的autocommit为OFF,执行LOCK TABLE tbname READ时,会加LOCK_S锁(
ha_innobase::external_lock
)。
当客户端想对表进行写操作(插入、删除、更新等)时,需要先获得一个写锁,这会阻塞其他客户端对该表的所有读写操作。只有没有人执行写操作时,其他读取的客户端才能获得读锁,读锁之间不会相互阻塞。
表锁的冲突和兼容情况
X | IX | S | IS | |
X | Conflict | Conflict | Conflict | Conflict |
IX | Conflict | Compatible | Conflict | Compatible |
S | Conflict | Conflict | Compatible | Compatible |
IS | Conflict | Compatible | Compatible | Compatible |
从上面的描述我们可以看到LOCK_X及LOCK_S锁在实际的大部分负载中都很少会遇到。主要还是互相不冲突的LOCK_IS及LOCK_IX锁。一个有趣的问题是,每次加表锁时,却总是要扫描表上所有的表级锁对象,检查是否有冲突的锁。很显然,如果我们在同一张表上的更新并发度很高,这个链表就会非常长。
基于大多数表锁不冲突的事实,我们在RDS MYSQL中对各种表锁对象进行计数,在检查是否有冲突时,例如当前申请的是意向锁,如果此时LOCK_S和LOCK_X的锁计数都是0,就可以认为没有冲突,直接忽略检查。由于检查是在持有全局大锁lock_sys->mutex
下进行的。在单表大并发下,这个优化的效果还是非常明显的,可以减少持有全局大锁的时间。
LOCK_AUTO_INC
AUTO_INC锁加在表级别,和AUTO_INC、表级S锁以及X锁不相容。锁的范围为SQL级别,SQL结束后即释放。
行锁
使用行级锁(row lock)可以最大程度地支持并发处理(也带来了最大的锁开销)。行级锁是在存储引擎而不是服务器中实现的。行锁依赖索引加锁,如果表没有索引,InnoDB会将锁加在隐藏的聚簇索引上。
Record Locks(行记录锁)
表示这个锁对象只是单纯的锁在记录上,不会锁记录之前的 GAP。比如利用唯一索引查询表中存在的一条记录。
Gap Locks(间隙锁)
间隙锁,锁定一个范围,但不包含这个范围中的行记录。
Next-Key Locks
上面两种锁的结合,锁定一个范围和这个范围内的行记录本身。目标是解决幻读问题(Phantom Problem)。
Record Locks演示
表结构
CREATE TABLE `tx_demo` (
`id` bigint NOT NULL AUTO_INCREMENT,
`age` int DEFAULT NULL,
`name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_age` (`age`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
数据
id | age | name |
15 | 5 | agc |
16 | 8 | agc |
17 | 12 | agc |
19 | 13 | agc |
行记录锁演示
-
开始事务后执行SQL
-- 开始事务后执行SQL select * from tx_demo where id = 19 for update;
-
查看持有锁的情况
-- 查看持有锁的情况 SELECT thread_id, INDEX_NAME, LOCK_TYPE, LOCK_MODE, LOCK_STATUS, LOCK_DATA FROM performance_schema.data_locks;
- 持有锁的情况
如上所示,该事务当前持有的锁是索引记录19,LOCK_MODE是X,REC_NOT_GAP。原因是该索引是唯一索引,并且该查询能检索出唯一一条数据。
Gap Locks演示
-
开始事务后执行SQL
-- 开始事务后执行SQL select * from tx_demo where age = 10 for update;
-
查看持有锁的情况
-- 查看持有锁的情况 SELECT thread_id, INDEX_NAME, LOCK_TYPE, LOCK_MODE, LOCK_STATUS, LOCK_DATA FROM performance_schema.data_locks;
- 持有锁的情况
如上所示,因为表中没有age=10的记录,因此Gap Locks锁定了(8,12)这个区间,注意,不包括12个索引记录。
Next-Key Locks演示
-
开始事务后执行SQL
-- 开始事务后执行SQL select * from tx_demo where age = 8 for update;
- 查看持有锁的情况
-- 查看持有锁的情况 SELECT thread_id, INDEX_NAME, LOCK_TYPE, LOCK_MODE, LOCK_STATUS, LOCK_DATA FROM performance_schema.data_locks;
- 持有锁的情况
如上所示,在索引idx_age上,这里涉及到的Record Locks是索引记录8,涉及到的Gap Locks有两个,分别是(5,8)、(8,12),因此结合起来就是Next-Key Locks (5,8]和Gap Locks(8,12)。 表中Next-Key Locks (5,8]的表示就是LOCK_DATA为8,16那一行,LOCK_MODE是X
;表中Gap Locks(8,12)的表示就是LOCK_DATA为12,17那一行,LOCK_MODE是X,REC_NOT_GAP
。
LOCK_MODE的含义
MySQL表performance_schema.data_locks中LOCK_MODE列的含义
-
LOCK_MODE =
X
,说明是 X 型的 next-key 锁; -
LOCK_MODE =
X, REC_NOT_GAP
,说明是 X 型的记录锁; -
LOCK_MODE =
X, GAP
,说明是 X 型的间隙锁; -
LOCK_MODE =
AUTO_INC
,说明是表级自增锁; -
LOCK_MODE =
UNKNOWN
,说明是未知模式锁;
参考文档
https://dev.mysql.com/doc/refman/8.0/en/innodb-locking.html
https://dev.mysql.com/doc/refman/8.0/en/performance-schema-data-locks-table.html
https://xiaolincoding.com/mysql/lock/show_lock.html#%E5%87%86%E5%A4%87%E5%B7%A5%E4%BD%9C
http://mysql.taobao.org/monthly/2016/01/01/