首先需要明确的就是“幻读”概念:隔离级别是可重复读,在一个事务中前后两次查询,查到了其他事务insert进来的数据。
强调的是读取到了其他事务插入进来的数据。
下面来论证一下可重复读下幻读的解决方案
# 建表语句
CREATE TABLE `test` (
`id` int(11) NOT NULL COMMENT '主键',
`d` int(11) NOT NULL,
`c` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '测试表';
# 插入数据
INSERT INTO `test`(`id`, `d`, `c`) VALUES (0, 0, 0);
INSERT INTO `test`(`id`, `d`, `c`) VALUES (2, 2, 2);
INSERT INTO `test`(`id`, `d`, `c`) VALUES (3, 3, 3);
INSERT INTO `test`(`id`, `d`, `c`) VALUES (4, 4, 4);
INSERT INTO `test`(`id`, `d`, `c`) VALUES (5, 5, 5);
INSERT INTO `test`(`id`, `d`, `c`) VALUES (6, 6, 6);
INSERT INTO `test`(`id`, `d`, `c`) VALUES (7, 7, 7);
INSERT INTO `test`(`id`, `d`, `c`) VALUES (8, 8, 8);
INSERT INTO `test`(`id`, `d`, `c`) VALUES (9, 9, 9);
T1 |
T2 |
T3 |
begin; select * from TABLE where d = 5 for update; |
|
|
update TABLE set d = 5 where id = 0; |
|
select * from TABLE where d = 5 for update; |
|
|
|
|
insert into TABLE values(1,5,1); |
select * from TABLE where d = 5 for update; commit; |
|
|
先明确一下,for update语法就是当前读,也就是查询当前已经提交的数据,并且是带悲观锁的。没有for update就是快照读,也就是根据readView读取的undolog中的数据。
- 当T1开启事务,执行第一条select语句时,查出来的结果如下:
- 假如该查询只锁定一行,也就是id=5的这一行数据,那么在T2阶段id=0并不会被锁定,那么update执行成功后,id=0那行数据被设置为d=5。此时T1阶段第二个select查询结果如下:
- T3阶段执行插入语句后,T1阶段第三个查询得出的结果如下:
如果按照以上猜想,那么整个执行结果就违背了可重复读的隔离级别了。
那么我们再假设select * from TABLE where d = 5 for update;这条语句锁定的是所有被扫描到的数据。
- 按照上述执行逻辑,我们在T1阶段第一个select读取到的数据:
这是因为T2阶段的update会被阻塞住,毕竟所有被扫描到的记录都被锁定了。
- 但是在T3阶段依然会执行,T3阶段做的是insert操作,本身这条记录在表中都不存在的,也就不会被阻塞。那么T1阶段第三个select查询结果如下:
按照上述推理过程,很显然,即使锁定所有扫描到的数据行,也依然存在幻读的情况。违背了可重复读的隔离级别。
针对这个情况,我们要解决幻读的问题,那么就要求针对所有被扫描的记录行以及还不存在的d=5的记录行都给锁住。
- 在T1阶段当执行第一条select语句时,所有被扫描的记录行都锁住,包括d=5的不存在的记录行。那么T2执行的时候,就会被阻塞住,等待T1结束。
- 当执行到T1阶段的第二个select时,因为T2还在等待T1结束,所以查询结果一样
- 当执行T3阶段的insert语句时,因为所有d=5的不存在的记录行也被锁住了,也就是间隙被锁住了,那么T3的insert语句也被阻塞,等待T1结束。那么T1阶段最后一条select语句执行结果如下:
至此,当前查询结果完全满足可重复读的隔离级别。
通过以上推论,我们可以总结一下,在可重复读的隔离级别下,解决幻读除了需要锁定所有扫描到的记录行外,还需要锁定行之间的间隙,也就是通过间隙锁来解决幻读的问题。
网友评论