• 幻读是什么,幻读有什么问题


    1. 引言

    首先,我们通过下面的SQL语句建立一张表,并插入5行数据:

    CREATE TABLE T(
        id int(11) not null,
        c int(11) default null,
        d int(11) default null,
        primary key (id),
        key c(c)
    )ENGINE=InnoDB;
    
    insert into t values(0,0,0),(5,5,5),(10,10,10),(15,15.15),(20,20,20),(25,25,25);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    接着,我们执行下面这条语句:

    begin
    select * from t where d=5 for update;
    commit;
    
    • 1
    • 2
    • 3

    for update 会为d=5的行加上一个锁,这个锁会在commit语句执行时被释放。

    由于字段d上没有索引,所以会进行全表扫描。但是,在进行全表扫描的时候,不满足d=5的行会不会被加锁呢?

    因为InnoDB的默认事务隔离级别是可重复读,所以下面的讨论都基于可重复读隔离级别。

    2. 幻读是什么?

    我们先来假设,仅仅只在d=5的行行加锁,而其他行不加锁的话,会怎么样?

    假设有这样下面这样一个场景:
    在这里插入图片描述

    Session A的三次查询我们分别定义为Q1,Q2,Q3。因为都使用了for update,所以是当前读,且会对d=5的行进行加锁。我们来分析下三次查询的结果:

    1. Q1只会返回id=5的行。
    2. 由于在T2时刻,session B将id=0的d值修改成了5,因此Q2查询出来id=0和id=5这两行。
    3. 由于在T4时刻,session C插入一行(1,1,5),因此Q3查询出来id=0,id=1和id=5三行。

    其中,Q3查询到id=1这种现象,我们称之为“幻读”。幻读是指一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行

    有几个需要注意的点:

    1. 在可重复读隔离级别下,普通的查询时快照读,是不会看到其他事务插入的数据的。因此,幻读只会在“当前读”才会出现
    2. 在Q2中,session B修改了结果被session A的当前读看到,我们不称为时幻读。幻读专指“新插入的行”

    但是,上面的场景其实是存在问题的。

    第一个问题是语义。 session A在T1时刻就执行了for update,也就是语义上会将所有d=5的行锁住,不准别的事务进行读写操作。但是实际上语义就被破坏了。

    又如下面这个场景:
    在这里插入图片描述

    理论上session A会在T1时刻将所有d=5的行锁住。但是session B和sessionC在后来分别修改和插入了d=5的行,但是这两行并没有被锁住,所以就违背了语义。

    第二个问题是数据一致性。 数据一致性包括数据库内部数据状态的一致性、数据和日志在逻辑上的一致性。

    我们来看下面这个场景:
    在这里插入图片描述

    这个场景在T1时刻的session A中加入了update语句,会把锁了的d=5的行的值修改成100。数据库的执行如下:

    1. 经过T1时刻,id=5这一行变成(5,5,100),当然这个结果会在T6时刻才提交
    2. 经过T2时刻,id=0这一行变成(0,5,5)
    3. 经过T4时刻,表里面多了一行(1,5,5)

    我们再来看binlog里面的内容:

    1. T2时刻,session B事务提交,写入了两条语句
    2. T4时刻,session C事务提交,写入了两条语句
    3. T6时刻,session A事务提交,写入了update t set d=100 where d=5这条语句

    所以在binlog中,由于T6时刻session A执行了update t set d=100 where d=5这条语句,所以三行变成了(0,5,100),(1,5,100),(5,5,100)。我们就会发现binlog和数据库的不一致性。

    因此仅仅只在d=5的行行加锁,而其他行不加锁的假设是不合理的

    如果是会在所有扫描过的行都加锁的话,我们再看下执行效果:
    在这里插入图片描述

    由于session A的Q1会将所有的行锁住,所以session B会被阻塞,直到session A提交事务才会继续执行,这样id=0的结果就会是(0,5,5),保证了数据库和binlog的一致性。

    但是对所有的行加锁, 并不能阻止session C中的幻读因为在Q1语句进行加锁的时候,id=1这一行还不存在,所以没法进行加锁

    3. 如何解决幻读?

    产生幻读的原因是因为行锁只能锁住行,但是新插入记录这个动作,是在行的间隙中的。因此,为了解决幻读,InnoDB引入了间隙锁(Gap Lock)

    在表t中,初始有6个记录,也就有7个间隙,如下图:
    在这里插入图片描述

    当执行for update的时候,不止对6个记录加入了行锁,还同时加了7个间隙锁,这就保证了无法插入新的记录。

    我们之前学习过行锁,行锁是分为读锁和写锁的,这两种锁可能会存在冲突关系:
    在这里插入图片描述

    但是,间隙锁之间是不存在冲突关系的。例如下面这个场景:
    在这里插入图片描述

    session A和session B都执行了for update语句,都会加上间隙锁,但是它们具有共同的目标:保护这个间隙,不允许插入新值,因此它们之间是不会冲突的。

    间隙锁和行锁合称next-key lock,每个next-key lock是前开后闭区间。next-key lock就是锁住满足条件的行和这些行前面的间隙。

    间隙锁的引入解决了幻读问题,但是也存在一些问题。如下面这个问题:
    在这里插入图片描述

    我们来分析上面场景的执行流程:
    我们假设id=9这一行起初是不存在的

    1. session A执行for update语句,由于id=9不存在,因此加上间隙锁(5,10)
    2. session B执行for update语句,同样加上间隙锁(5,10)
    3. session B试图插入一行(9,9,9),被session A的间隙锁挡住,进入等待
    4. session A试图插入一行(9,9,9),被session B的间隙锁挡住,进入等待

    这样就出现了死锁的问题。只能让session A进行回滚了。

    4. 总结

    对于for update语句,它是一种悲观锁:

    • 如果查询的是主键\索引列,则是行锁。
    • 如果查询的是没有主键\索引的话,则是表锁。

    对于for update加的是行锁还是表锁,可以看面试官问:select…for update会锁表还是锁行?

    来源:自己整理的MySQL实战45讲笔记

  • 相关阅读:
    《牛客题霸-算法篇》刷题之NC57 反转数字
    perl:BigInt 计算 斐波那契数列
    区块链 - 基础知识 - 第一讲
    高成本获客时代,企业如何通过营销自动化实现突围?
    2023秋招——快手数据研发一、二面面经
    SOM网络2: 代码的实现
    武汉新时标文化传媒有限公司短视频的传播现状及侵权分析
    【数据结构与算法】ArrayList与顺序表
    Freeswitch学习笔记(三):配置
    大数据Flink(七十三):SQL的滚动窗口(TUMBLE)
  • 原文地址:https://blog.csdn.net/weixin_41799019/article/details/127809667