为什么需要间隙锁
我们来一个简单事务,统计test表查询有12条数据,我们先确认是12条数据。确认后,另外一个会话执行插入数据操作,我们再查看确认一下是否12条数据操作。最后我们把数据数目插入到t1表,提交后, 我们查询t1表,居然查出来是13。
这种情况加上for update也没有用,因为for update是行级锁【记录锁 】,行级锁只能保障多个对象变更同一个数据有序进行。幻读的本质是由于其它用户在插入数据,如何保障当前事务进行时,其它事务没有插入数据,这个就是表级锁的作用。表级锁的牺性太大了,于是MySQL创造了间隙锁。间隙锁的范围禁止一切增、删、查、改操作。
间隙锁原理DEMO
间隙锁创建了一系列行范围 ,在这个范围不允许插入数据,在行范围外可以插入数据,这样大大提高了性能。创建间隙锁命令SELECT * FROM 表名 WHERE 主键 BETWEEN 间隙上限 AND 间隙下限 FOR UPDATE;
,也可以间隙上限 > XX and 间隙下限 < XX来表示。
从业务理解角度,我们以为创建间隙锁,例如10到20之间,不允许插入数据,直接就是,SELECT * FROM 表名 WHERE 主键 BETWEEN 10 AND 20 FOR UPDATE;
完全错了,大错特错,间隙范围必须建立主键索引之上,主键索引才是真正的边界,必须要完成 间隙到主键的映射。
举一个简单的例子。
shell
#创建test表
CREATE TABLE `test` (
`id` int(1) NOT NULL AUTO_INCREMENT,
`id2` int(2) NOT NULL,
`name` varchar(8) DEFAULT NULL,
PRIMARY KEY (`id`),
key key_id2(id2)
);
#插入两行数据
insert into test (id,id2,name) values(2,2,'test2'),(7,7,'test7');
#再插入一行数据
insert into test (id,id2,name) values(15,15,'test15');
# 目前test表间隙范围有3条边界 2、7、15
mysql> select * from test;
+-----+-----+---------+
| id | id2 | name |
+-----+-----+---------+
| 2 | 2 | test2 |
| 7 | 7 | test7 |
| 15 | 15 | test15 |
+-----+-----+---------+
3 rows in set (0.00 sec)
begin;
# 创建一个小于边界15, 我们定义【2,10】, 2是上限,10是下限
SELECT * FROM test WHERE id BETWEEN 2 AND 10 FOR UPDATE;
上述间隙锁的建立,我们本意是2至10的范围内,不允许插入数据,11之后的数据都能插入,结果11到15的数据都无法插入。
insert into test (id,id2,name) values(11,11,'test11'); ERROR 1205 (HY000) block
insert into test (id,id2,name) values(12,12,'test12'); ERROR 1205 (HY000) block
insert into test (id,id2,name) values(13,13,'test13'); ERROR 1205 (HY000) block
insert into test (id,id2,name) values(14,14,'test14'); ERROR 1205 (HY000) block
insert into test (id,id2,name) values(15,15,'test15'); ERROR 1205 (HY000) block
我们建立的间隙最大范围是10,映射到15,15是这个间隙锁最大值。
因为边界15导致12、13、14、15不能进去,反观16、17就能成功。
insert into test (id,id2,name) values(16,16,'test16'); success
insert into test (id,id2,name) values(17,17,'test17'); success
为什么10会映射到15呢?
# 再来一遍,同样是3条边界 2、7、15, 这次我们要大于边界15,我们定义一个【2,20】的间隙锁
begin;
SELECT * FROM test WHERE id BETWEEN 2 AND 20 FOR UPDATE;
insert into test (id,id2,name) values(12,12,'test12'); ERROR 1205 (HY000) block
insert into test (id,id2,name) values(13,13,'test13'); ERROR 1205 (HY000) block
insert into test (id,id2,name) values(14,14,'test14'); ERROR 1205 (HY000) block
insert into test (id,id2,name) values(15,15,'test15'); ERROR 1205 (HY000) block
#发现原来的16、17都无法插入数据
insert into test (id,id2,name) values(16,16,'test16'); ERROR 1205 (HY000) block
insert into test (id,id2,name) values(17,17,'test17'); ERROR 1205 (HY000) block
#试了更大的数据,都无法正常插入,这迹象表明它的间隙下限无限,
insert into test (id,id2,name) values(21,21,'test21'); ERROR 1205 (HY000) block
insert into test (id,id2,name) values(1600,1600,'test1600');ERROR 1205 (HY000) block
insert into test (id,id2,name) values(170,170,'test170');ERROR 1205 (HY000) block
# 但是我们摸到它的上限,【2,10】和【2,20】都能插入数据
insert into test (id,id2,name) values(1,1,'test1'); susssess
间隙表上限与间隙表下限【2,10】和【2,20】 与 主键索引边界的关系 【2、7、15 】
从【2,10】拿出2,与【2、7、15 】对比,2没有比2大,终止于2,
从【2,10】拿出10,与【2、7、15 】对比,10比15小,终止于15,
最后【2,10】转换成 【2,15】,【2,15】的意思是从2到15的范围不允许插入数据,除非事务结束。
从【2,20】拿出2,与【2、7、15 】对比,2没有比2大,终止于2,
从【2,20】拿出10,与【2、7、15 】对比,20比15大,一直没有终止,
最后【2,20】转换成 【2,无限】,【2,无限】的意思是从2到15的范围不允许插入数据,除非事务结束。
# 关键上限的取值一定要比主键索引大,下限的取值一定要比主键索引小。
间隙锁实战
shell
实战一个例子,现在test表主键索引如下,根据主键索引边界有【1,2,7,14,15,120,121,122,150,170】
mysql> select * from test;
+-----+-----+---------+
| id | id2 | name |
+-----+-----+---------+
| 1 | 1 | test1 |
| 2 | 2 | test2 |
| 7 | 7 | test7 |
| 14 | 14 | test0 |
| 15 | 15 | test15 |
| 120 | 120 | test0 |
| 121 | 121 | test121 |
| 122 | 122 | test122 |
| 150 | 150 | test150 |
| 170 | 170 | test170 |
+-----+-----+---------+
10 rows in set (0.00 sec)
#创建一个间隙锁,我们定义【100,130】, 100是上限,130是下限
# 根据 【1,2,7,14,15,120,121,122,150,170】
# 100比【1,2,7,14,15】都大,但是比120小,100终止于15
# 130【1,2,7,14,15,120,121,122】都大,但是比150小,130终止于150
#最后【100,130】转换成 【15,150】,【15,150】的意思是从2到15的范围不允许插入数据,除非事务结束。
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT * FROM test WHERE id BETWEEN 100 AND 130 FOR UPDATE;
+-----+-----+---------+
| id | id2 | name |
+-----+-----+---------+
| 120 | 120 | test0 |
| 121 | 121 | test121 |
| 122 | 122 | test122 |
+-----+-----+---------+
3 rows in set (0.00 sec)
#测试
mysql> insert into test (id,id2,name) values(149,149,'test149');
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into test (id,id2,name) values(99,99,'99');
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into test (id,id2,name) values(16,16,'test16');
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
l
mysql> insert into test (id,id2,name) values(151,15,'test151');
Query OK, 1 row affected (0.00 sec)
mysql> delete from test where id=14;
Query OK, 1 row affected (0.01 sec)
总结
- 间隙的规律,按着边界索引,上限和下限越过最大的,找到比它小的就是它的对应值
- 下限一定要比主键索引小,假如主键索引没有比下限大,或者相等, 索范围就是无限。
- 真实生产环境比测试环境好,主键索引大多数有序递增排列,但是开发体验依然较复杂,因为上限和下限还要依据实际的主键索引做匹对。
- mysql锁分三种,行锁【记录锁】、间隙锁、临键锁【记录+间隙】,通过三种锁完成4种安全程度不同的隔离级别。MysSQL的 RR加上间隙锁可以杜绝幻读,但是基于RC级别是不能实现间隙锁的,因为RC是读已提交,与间隙有冲突。
思考题
为什么分布式数据库不用间隙锁?