本文对RR事务隔离级别下的select语句进行加锁处理分析。
表结构如下:
mysql> show create table lock_test\G
*************************** 1. row ***************************
Table: lock_test
Create Table: CREATE TABLE `lock_test` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`age` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `age` (`age`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4
1 row in set (0.00 sec)
表中数据
mysql> select * from lock_test;
+----+------+
| id | age |
+----+------+
| 2 | 5 |
| 3 | 10 |
| 4 | 20 |
| 5 | 30 |
| 7 | 30 |
+----+------+
5 rows in set (0.00 sec)
按照heap_no排序的话,如下图
但是为了方便对加锁方式的说明,使其按照如下逻辑排列方式
把查询加锁方式,对应到如下几种组合来说明。
组合 | 隔离级别 | where条件 |
---|---|---|
组合一 | RR | = |
组合二 | RR | >= |
组合三 | RR | <= |
每一个组合又可以根据不通的条件取值进行细分
表中数据,并没有age=2的数据,我们看如何加锁
2018-08-31T09:58:43.441420+08:00 6 [Note] InnoDB:
trx_id: 323102 create a record lock and add it to lock hash table, //显示行锁,有显示的锁结构,并且加入到hash表中
space_id: 148 //表的space id
page_no: 4 //记录所在的page id
heap_no: 2 //记录在page内部的heap no
n_bits: 72 //number of bits in the lock bitmap;
primary key: 0 //是否是主键索引上的锁
is record lock: 1 //是否是行锁
is waiting: 0 //是不是没有加锁成功,处于等待
is gap: 1 //是不是间隙锁
is record not gap: 0 //是不是只是锁定记录
is insert intention: 0 //是不是插入意向
lock_mode: 3 (0:LOCK_IS, 1:LOCK_IX, 2:LOCK_S, 3:LOCK_X, 4:LOCK_AUTO_INC, 5:LOCK_NONE)
通过日志显示,此查询操作加显示的排他行锁,并且带有gap属性,只锁gap,不锁记录。
同理对比1.1 select * from lock_test where age=2 lock in share mode
2018-08-31T12:51:57.224886+08:00 7 [Note] InnoDB:
trx_id: 0 create a record lock and add it to lock hash table,
space_id: 157
page_no: 4
heap_no: 2
n_bits: 72
primary key: 0
is record lock: 1
is waiting: 0
is gap: 1
is record not gap: 0
is insert intention: 0
lock_mode: 2 (0:LOCK_IS, 1:LOCK_IX, 2:LOCK_S, 3:LOCK_X, 4:LOCK_AUTO_INC, 5:LOCK_NONE)
对比发现,for update
和`lock in share共性如下:
- 都是间隙锁
- 间隙锁加在相同的行数据上,heap_no=2,这也是此数据叶中的第一条用户数据,age=5之前的间隙
区别如下:
- 后者是不会被分配事务id的
- 锁类型前者为LOCK_X,后者为LOCK_S.
根据锁兼容性矩阵,这两个sql是可以共存的,因为间隙锁不阻塞间隙锁,可以参照下图
那么到底影响哪些其他的sql操作呢
首先我们需要确定这两条SQL间隙锁范围,Innodb间隙锁指的是锁定记录之前的空隙,此例子中,记录为age=2,那么间隙锁定的范围应该是(-∞,5),那么记录age=5在二级索引上到底有没有被锁定呢?
答案是否定的,可以尝试对age=5的记录进行加锁测试,比如下面两条
select * from lock_test where age=5 for update;
//select * from lock_test where age=5 lock in share mode;
拿select * from lock_test where age=5 for update;
来说明,其一共有三把锁,分别为
第一,二级索引age=5的next key lock(记录+间隙)
2018-08-31T13:05:49.834989+08:00 7 [Note] InnoDB: trx_id: 323338 create a record lock and add it to lock hash table,
space_id: 157
page_no: 4
heap_no: 2
n_bits: 72
primary key: 0
is record lock: 1
is waiting: 0
is gap: 0
is record not gap: 0
is insert intention: 0
lock_mode: 3 (0:LOCK_IS, 1:LOCK_IX, 2:LOCK_S, 3:LOCK_X, 4:LOCK_AUTO_INC, 5:LOCK_NONE)
第二,对主键索引加锁,可以直接忽略。
2018-08-31T13:05:49.835167+08:00 7 [Note] InnoDB: trx_id: 323338 create a record lock and add it to lock hash table,
space_id: 157
page_no: 3
heap_no: 2
n_bits: 72
primary key: 1
is record lock: 1
is waiting: 0
is gap: 0
is record not gap: 1 //主键只是记录锁,不存在gap属性
is insert intention: 0
lock_mode: 3 (0:LOCK_IS, 1:LOCK_IX, 2:LOCK_S, 3:LOCK_X, 4:LOCK_AUTO_INC, 5:LOCK_NONE)
第三,二级索引age=5的下一条记录age=10的间隙锁,注意,只锁间隙,不锁记录。
2018-08-31T13:05:49.835355+08:00 7 [Note] InnoDB: trx_id: 323338 create a record lock and add it to lock hash table,
space_id: 157
page_no: 4
heap_no: 3
n_bits: 72
primary key: 0
is record lock: 1
is waiting: 0
is gap: 1
is record not gap: 0
is insert intention: 0
lock_mode: 3 (0:LOCK_IS, 1:LOCK_IX, 2:LOCK_S, 3:LOCK_X, 4:LOCK_AUTO_INC, 5:LOCK_NONE)
对于第二和第三来讲,和select * from lock_test where age=2 lock in share mode
加锁范围都是不冲突的,模糊的是第一把锁,第一把锁为5的记录锁+5之前的间隙,根据锁的兼容性矩阵来讲,间隙锁锁不阻塞间隙锁,所以这两条sql可以无阻塞的执行。
如下图所示
那么问题来了,1.1 select * from lock_test where age = 2 for update,到底阻塞谁呢?
答案就是,插入意向锁。来看个例子
insert into lock_test(age) select 4;
如果你正在测试,会发现,这条sql会被select * from lock_test where age = 2 for update
阻塞。来看下这条插入sql的加锁情况
2018-08-31T13:22:19.388272+08:00 7 [Note] InnoDB: trx_id: 323339 create a record lock and add it to lock hash table,
space_id: 157
page_no: 4
heap_no: 2
n_bits: 72
primary key: 0
is record lock: 1
is waiting: 1
is gap: 1
is record not gap: 0
is insert intention: 1
lock_mode: 3 (0:LOCK_IS, 1:LOCK_IX, 2:LOCK_S, 3:LOCK_X, 4:LOCK_AUTO_INC, 5:LOCK_NONE)
加锁情况为heap_no=2 也就是age=5 行排他锁,只是gap锁,不锁记录,并且带有插入意向属性。根据锁的兼容性矩阵,插入意向锁和间隙锁是互斥的,所以这条插入操作会被阻塞。如下如所示
换个值插一下,比如
insert into lock_test(age) select 5; //会不会阻塞呢?
好像怎么插都不会阻塞,为什么呢?
要解释这个问题,需要再另外的文章中对insert加锁过程进行详细描述了。
下面来看select * from lock_test where age = 5 for update这条语句如何加锁,以及如何阻塞别的sql语句。
第一,对于二级索引 head_no=2,age=5,加next key lock
2018-08-31T13:59:12.492326+08:00 6 [Note] InnoDB: trx_id: 323389 create a record lock and add it to lock hash table,
space_id: 158
page_no: 4
heap_no: 2
n_bits: 72
primary key: 0
is record lock: 1
is waiting: 0
is gap: 0
is record not gap: 0
is insert intention: 0
lock_mode: 3 (0:LOCK_IS, 1:LOCK_IX, 2:LOCK_S, 3:LOCK_X, 4:LOCK_AUTO_INC, 5:LOCK_NONE)
第二,主键记录排他锁,无gap属性
2018-08-31T13:59:12.492449+08:00 6 [Note] InnoDB: trx_id: 323389 create a record lock and add it to lock hash table,
space_id: 158
page_no: 3
heap_no: 2
n_bits: 72
primary key: 1
is record lock: 1
is waiting: 0
is gap: 0
is record not gap: 1
is insert intention: 0
lock_mode: 3 (0:LOCK_IS, 1:LOCK_IX, 2:LOCK_S, 3:LOCK_X, 4:LOCK_AUTO_INC, 5:LOCK_NONE)
第三,二级索引heap_no=3,age=10的间隙锁。
2018-08-31T13:59:12.492557+08:00 6 [Note] InnoDB: trx_id: 323389 create a record lock and add it to lock hash table,
space_id: 158
page_no: 4
heap_no: 3
n_bits: 72
primary key: 0
is record lock: 1
is waiting: 0
is gap: 1
is record not gap: 0
is insert intention: 0
lock_mode: 3 (0:LOCK_IS, 1:LOCK_IX, 2:LOCK_S, 3:LOCK_X, 4:LOCK_AUTO_INC, 5:LOCK_NONE)
其阻塞情况,大家自己可以分析下,哪些sql会阻塞,哪些不会阻塞?以及为什么?