MySQL间隙锁实战,消灭幻读

为什么需要间隙锁

我们来一个简单事务,统计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是读已提交,与间隙有冲突。

思考题

为什么分布式数据库不用间隙锁?

相关推荐
vvvae12348 小时前
分布式数据库
数据库
雪域迷影8 小时前
PostgreSQL Docker Error – 5432: 地址已被占用
数据库·docker·postgresql
bug菌¹9 小时前
滚雪球学Oracle[4.2讲]:PL/SQL基础语法
数据库·oracle
逸巽散人9 小时前
SQL基础教程
数据库·sql·oracle
月空MoonSky9 小时前
Oracle中TRUNC()函数详解
数据库·sql·oracle
momo小菜pa9 小时前
【MySQL 06】表的增删查改
数据库·mysql
向上的车轮10 小时前
Django学习笔记二:数据库操作详解
数据库·django
编程老船长10 小时前
第26章 Java操作Mongodb实现数据持久化
数据库·后端·mongodb
全栈师11 小时前
SQL Server中关于个性化需求批量删除表的做法
数据库·oracle
Data 31711 小时前
Hive数仓操作(十七)
大数据·数据库·数据仓库·hive·hadoop