前言
在前几篇中,我们对MySQL的内部结构进行了介绍,对InnoDB的内部结构和核心机制进行了了解,本篇,我们继续深入InnoDB引擎,对InnoDB的锁机制进行简要的介绍。
InnoDB锁类型
在前面的文章中,我们介绍过,为了高性能的支持,InnoDB
实现了标准的行级锁。行级锁的意思代表着仅对指定的记录进行加锁,这样其它进程还是可以对同一个表中的其它记录进行操作。因此,InnoDB可以支持较高的并发性。
InnoDB可以支持的锁的类型如下:
共享锁
排他锁
意向锁
记录锁
区间锁/间隙锁
临键锁
插入意向锁
自增锁
那么下面,我们对这些类型的锁进行依次介绍。
共享锁与排他锁(Shared and Exclusive Locks)
InnoDB 实现了标准的行级锁,分为两种类型:共享(S)锁 和 排他(X)锁。
共享(S) 锁允许持有该锁的事务读取行。
排他(X) 锁允许持有该锁的事务更新或删除行。
如果事务T1在行A上持有共享(S)锁,则其他事务T2对行A的锁的请求按如下方式处理:
可以立即授予T2对S锁的请求。结果,T1和T2都在r上持有S锁。
T2的X锁定请求不能立即授予。
如果事务T1在行B上持有排他(X)锁,则不能立即授予其他事务T2对行B上任何类型的锁请求。
相反,T2必须等待T1释放其对行B的锁定。
总结一下,如果对一行记录加共享锁,其是可以对多个事务进行操作共享的。而如果对一行记录加排他锁,那么该行记录的操作权只可以由一个事务进行持有,如果其他事务同样希望操作该行记录,则需要进行等待。
意向锁(Intention Locks)
InnoDB支持多种粒度的锁,允许行锁和表锁共存。 例如可以使用语法在指定的表上持有排他锁(X 锁)或共享锁(S锁)。也就是所谓的表级锁。
LOCK TABLES
tbl_name [[AS] alias] lock_type
[, tbl_name [[AS] alias] lock_type] ...
lock_type: {
READ [LOCAL]
| [LOW_PRIORITY] WRITE
}
UNLOCK TABLES复制
InnoDB用意向锁来实现多粒度级别的锁。意向锁是表级锁,表示事务稍后对表中的行进行加相关类型的锁(共享或排他)。意向锁有两种类型:
意向共享锁(intention shared lock, IS)表示事务打算在表中的各个行上设置共享锁。
意向排他锁(intention exclusive lock, IX)表示事务打算在表中的各个行上设置排他锁。
例如 SELECT … LOCK IN SHARE MODE 设置IS锁, SELECT … FOR UPDATE 设置IX锁定。
意向锁的规则如下:
事务在获取表中某行的共享锁之前,必须先获取表上的 IS 锁或类似的锁。
事务在获取表中某行的排他锁之前,必须先获取表上的 IX 锁。
下面是表级锁类型兼容性总结:
当前事务现有的锁 其他事务的锁请求 | X | IX | S | IS |
---|---|---|---|---|
X | 冲突 | 冲突 | 冲突 | 冲突 |
IX | 冲突 | 兼容 | 冲突 | 兼容 |
S | 冲突 | 冲突 | 兼容 | 兼容 |
IS | 冲突 | 兼容 | 兼容 | 兼容 |
如果事务请求的锁与现有锁兼容就授予锁,如果冲突则不会。事务会一直等到现有的锁释放。如果因为事务请求的锁与现有的锁冲突而无法授予,则可能会导致发生死锁错误。
除全表请求之外,意向锁不会阻塞任何事务(例如 LOCK TABLES … WRITE)。
意向锁的主要目的是表示某人正在锁定行,或将要锁定表中的行。
需要强调一下,意向锁是一种不与行级锁冲突的表级锁。
意向锁是有数据引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享 排他锁之前,InooDB 会先获取该数据行所在在数据表的对应意向锁。
意向锁存在的意义
说了这么多,你可能有疑问,意向锁到底有什么作用呢?
它的意义可以用一句话进行一下总结:
如果另一个任务试图在该表级别上应用共享或排它锁,则受到由第一个任务控制的表级别
意向锁的阻塞。
第二个任务在锁定该表前不必检查各个页或行锁,而只需检查表上的意向锁。
复制
我们来举一个栗子:
事务 A 获取了某一行的排他锁,并未提交:
SELECT * FROM users WHERE id = 6 FOR UPDATE;
复制
事务 B 想要获取 users
表的表锁:
LOCK TABLES users READ;
复制
因为共享锁与排他锁互斥,所以事务 B 在视图对 users
表加共享锁的时候,必须保证:
当前没有其他事务持有
users
表的排他锁。当前没有其他事务持有
users
表中任意一行的排他锁 。
为了检测是否满足第二个条件,事务 B 必须在确保 users
表不存在任何排他锁的前提下,去检测表中的每一行是否存在排他锁。很明显这是一个效率很差的做法,但是有了意向锁之后,情况就不一样了。
当事务B希望获取 user
表的锁时,此时事务 B 检测事务 A 持有 users
表的意向排他锁,就可以得知事务 A 必然持有该表中某些数据行的排他锁,那么事务 B 对 users 表的加锁请求就会被排斥(阻塞),而无需去检测表中的每一行数据是否存在排他锁。
由此,可以大大提高效率。
意向锁总结
最后,我们来总结一下意向锁的特征:
InnoDB 支持多粒度锁,特定场景下,行级锁可以与表级锁共存。
意向锁之间互不排斥,但除了 IS 与 S 兼容外,意向锁会与 共享锁 排他锁 互斥。
IX,IS是表级锁,不会和行级的X,S锁发生冲突。只会和表级的X,S发生冲突。
意向锁在保证并发性的前提下,实现了行锁和表锁共存且满足事务隔离性的要求。
记录锁(Record Locks)
记录锁是对索引记录的锁定。例如,SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE; 防止任何其他事务插入,更新或删除 t.c1 的值为 10 的行。也就是我们所说的一般意义的行锁。
即使表没有定义索引,记录锁也能锁定索引记录。因为 InnoDB创建了一个隐藏的聚簇索引并使用此索引做记录锁。
记录锁的事务数据在SHOW ENGINE INNODB STATUS和InnoDB监视器输出中显示类似于以下内容:
RECORD LOCKS space id 58 page no 3 n bits 72 index `PRIMARY` of table `test`.`t`
trx id 10078 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
0: len 4; hex 8000000a; asc ;;
1: len 6; hex 00000000274f; asc 'O;;
2: len 7; hex b60000019d0110; asc ;;复制
区间锁 间隙锁(Gap Locks)
区间锁是锁定索引记录之间的区间,或锁定在第一个或最后一个索引记录之前的区间上。
例如,SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE;
阻止其他事务将值15插入到列 t.c1 中,无论列中是否存在任何此类值,因为该范围内所有现有值之间的区间都被锁定。
区间可能跨越单个索引值,多个索引值,甚至可能为空。
区间锁是性能和并发之间权衡的一部分,用于某些事务隔离级别而不是其他级别。
使用唯一索引(unique index )锁定行以搜索唯一行的语句不需要区间锁。(这不包括搜索条件仅包括多列唯一索引的一些列的情况; 在这种情况下,确实会发生区间锁定。)例如,如果id
列具有唯一索引,则以下语句仅对id
值为100的行使用索引记录锁,并且其他会话是否在前一个间隙中插入行无关紧要:
SELECT * FROM child WHERE id = 100;
复制
如果id
未编入索引或具有非唯一索引,则该语句会锁定前一个区间。
如何去理解这句话呢?我们来看下面的栗子。
间隙锁的锁定区间
产生间隙锁的条件(RR事务隔离级别下;):
使用普通索引锁定;
使用多列唯一索引;
使用唯一索引锁定多行记录。
当执行SQL语句去锁定指定的一段记录区间时,会将此区间的前置区间与后置区间同时进行锁定,我们来看下面的栗子:
我们有一个数据表test
/* 开启事务1 */
BEGIN;
/* 查询 id 在 7 - 11 范围的数据并加记录锁 */
SELECT * FROM `test` WHERE `id` BETWEEN 5 AND 7 FOR UPDATE;
/* 延迟30秒执行,防止锁释放 */
SELECT SLEEP(30);
# 注意:以下的语句不是放在一个事务中执行,而是分开多次执行,每次事务中只有一条添加语句
/* 事务2插入一条 id = 3,name = '小张1' 的数据 */
INSERT INTO `test` (`id`, `name`) VALUES (3, '小张1'); # 正常执行
/* 事务3插入一条 id = 4,name = '小白' 的数据 */
INSERT INTO `test` (`id`, `name`) VALUES (4, '小白'); # 正常执行
/* 事务4插入一条 id = 6,name = '小东' 的数据 */
INSERT INTO `test` (`id`, `name`) VALUES (6, '小东'); # 阻塞
/* 事务5插入一条 id = 8, name = '大罗' 的数据 */
INSERT INTO `test` (`id`, `name`) VALUES (8, '大罗'); # 阻塞
/* 事务6插入一条 id = 9, name = '大东' 的数据 */
INSERT INTO `test` (`id`, `name`) VALUES (9, '大东'); # 阻塞
/* 事务7插入一条 id = 11, name = '李西' 的数据 */
INSERT INTO `test` (`id`, `name`) VALUES (11, '李西'); # 阻塞
/* 事务8插入一条 id = 12, name = '张三' 的数据 */
INSERT INTO `test` (`id`, `name`) VALUES (12, '张三'); # 正常执行
/* 提交事务1,释放事务1的锁 */
COMMIT;复制
从上面我们可以看到,(5, 7]、(7, 11] 这两个区间,都不可插入数据,其它区间,都可以正常插入数据。所以我们可以得出结论:当我们给 (5, 7] 这个区间加锁的时候,会锁住 (5, 7]、(7, 11] 这两个区间。
注意
这里还值得注意的是,不同的事务在相同区间上可以存在相互冲突的锁类型。
例如,事务A可以在区间上持有共享区间锁(区间S锁),而事务B在同一区间上持有排他区间锁(区间X锁)。
允许区间锁冲突的原因是,如果从索引中清除记录,则必须合并由不同事务保留在记录上的区间锁。
如果将事务隔离级别更改为READ COMMITTED
或启用系统变量innodb_locks_unsafe_for_binlog
,就可以明确禁用区间锁。此时,搜索和索引扫描会禁用区间锁定,只能用于外键约束检查和重复键检查。
间隙锁的意义
间隙锁的目的是为了防止幻读,其主要通过两个方面实现这个目的:
防止间隙内有新数据被插入
防止已存在的数据,更新成间隙内的数据(例如防止numer=3的记录通过update变成number=5)
临键锁(Next-Key)
临键锁锁是索引记录上的记录锁和索引记录之前的区间上的区间锁的组合。
InnoDB用以下方式执行行级锁定:当它搜索或扫描表索引时,它会在遇到的索引记录上设置共享锁或排它锁。因此,行级锁实际上是索引记录锁。索引记录上的Next-Key锁也会影响该索引记录之前的“区间”。也就是说,Next-Key锁是索引记录锁加上索引记录之前的区间上的区间锁。如果一个会话在索引记录R上具有共享锁或排他锁,则另一个会话不能在索引顺序中的R之前的区间中插入新的索引记录。
假设索引包含值10,11,13和20。此索引的可能的Next-Key锁定包括以下间隔,其中圆括号表示排除区间端点,方括号表示包含端点:
(-∞, 10]
(10, 11]
(11, 13]
(13, 20]
(20, +∞)复制
对于最后一个区间,Next-Key 锁将区间锁定在索引中最大值之上,而“supremum”伪记录的值高于索引中实际的任何值。supremum不是真正的索引记录,因此,实际上,此Next-Key锁仅锁定最大索引值之后的间隙。
临键锁的意义
临键锁的主要目的,也是为了避免幻读(Phantom Read)。如果把事务的隔离级别降级为RC,临键锁则也会失效。
插入意向锁
插入意向锁是在行插入之前由插入操作设置的一种间隙锁。与上面介绍的间隙锁类似。
这个锁表示插入的意图,如果插入到同一索引间隙中的多个事务没有插入到间隙中的同一位置,那么它们不需要等待对方。假设有值为4和7的索引记录。尝试分别插入值5和6的单独事务,每个事务在获取插入行的排他锁之前使用插入意图锁锁定4和7之间的间隙,但因为行不冲突所以不会相互阻塞。
我们来举一个栗子演示一下:
一个事务,它在获取插入记录的排他锁之前获取一个插入意向锁。
这个例子涉及两个客户端(clients),A和B。客户端 A 创建一个包含两个索引记录(90和102)的表,然后启动一个事务,该事务对ID大于100的索引记录放置一个排它锁。排它锁包括记录102之前的间隙锁:
mysql> INSERT INTO child (id) values (90),(102);
mysql> START TRANSACTION;
mysql> SELECT * FROM child WHERE id > 100 FOR UPDATE;
+-----+
| id |
+-----+
| 102 |
+-----+复制
客户端B开始一个事务,将一个记录插入GAP中。事务在等待获取排他锁时接受插入意向锁。可以成功插入。
mysql> START TRANSACTION;
mysql> INSERT INTO child (id) VALUES (101);复制
插入意向锁的可以增加SQL的并发性,即使某一段记录被其他事务进行排他锁的锁定,但是通过插入意向锁,其他事务依然可以成功插入记录。
自增锁(AUTO-INC Locks)
在InnoDB中,每个含有自增列的表都有一个自增长计数器。
当对含有自增长计数器的表进行插入时,首先会执行 select max(auto_inc_col) from t for update
来得到计数器的值,然后再将这个值加1赋予自增长列。我们将这种方式称之为AUTO_INC Lock
。
AUTO_INC Lock
是一种特殊的表锁,它在完成对自增长值插入的SQL语句后立即释放,所以性能会比事务完成后释放锁要高。由于是表级别的锁,所以在并发环境下其依然存在性能问题。
可以通过配置项 innodb_autoinc_lock_mode
调整自增锁算法,
例如:调整自增序列和插入操作的最大并发。
结语
本篇我们对InnoDB中的几种锁类型进行了介绍,我们来进行一下大体的总结:
记录锁、间隙锁、临键锁,都属于排它锁;
唯一索引只有锁住多条记录或者一条不存在的记录的时候,才会产生间隙锁,指定给某条存在的记录加锁的时候,只会加记录锁,不会产生间隙锁;普通索引不管是锁住单条,还是多条记录,都会产生间隙锁;
间隙锁会封锁该条记录相邻两个键之间的空白区域,防止其它事务在这个区域内插入、修改、删除数据,这是为了防止出现幻读现象;
插入意向锁可以在一定程度上提供并发性能
本篇的内容就到这里,下一篇中,我将会对InnoDB的索引结构进行详细的介绍,了解一下InnoDB的索引的数据结构是如何构成的,敬请期待!