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是读已提交,与间隙有冲突。

思考题

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

相关推荐
问道飞鱼1 小时前
【知识科普】认识正则表达式
数据库·mysql·正则表达式
HaiFan.1 小时前
SpringBoot 事务
java·数据库·spring boot·sql·mysql
水根LP491 小时前
linux系统上SQLPLUS的重“大”发现
数据库·oracle
途途途途2 小时前
精选9个自动化任务的Python脚本精选
数据库·python·自动化
04Koi.2 小时前
Redis--常用数据结构和编码方式
数据库·redis·缓存
silver98862 小时前
mongodb和Cassandra
数据库
PersistJiao2 小时前
3.基于 Temporal 的 Couchbase 动态 SQL 执行场景
数据库·sql
上山的月3 小时前
MySQL -函数和约束
数据库·mysql
zhcf3 小时前
【MySQL】十三,关于MySQL的全文索引
数据库·mysql
极限实验室3 小时前
Easysearch Chart Admin 密码自定义
数据库