笔记 笔记
首页
  • 开发工具
  • Java Web
  • Java 进阶
  • 容器化技术
  • Java 专栏

    • Java 核心技术面试精讲
    • Java 业务开发常见错误 100 例
  • 数据库专栏

    • MySQL 实战 45 讲
    • Redis 核心技术与实战
  • 安全专栏

    • OAuth 2.0 实战课
  • 计算机系统
  • 程序设计语言
  • 数据结构
  • 知识产权
  • 数据库
  • 面向对象
  • UML
  • 设计模式
  • 操作系统
  • 结构化开发
  • 软件工程
  • 计算机网络
  • 上午题错题
在线工具 (opens new window)

EasT-Duan

Java 开发
首页
  • 开发工具
  • Java Web
  • Java 进阶
  • 容器化技术
  • Java 专栏

    • Java 核心技术面试精讲
    • Java 业务开发常见错误 100 例
  • 数据库专栏

    • MySQL 实战 45 讲
    • Redis 核心技术与实战
  • 安全专栏

    • OAuth 2.0 实战课
  • 计算机系统
  • 程序设计语言
  • 数据结构
  • 知识产权
  • 数据库
  • 面向对象
  • UML
  • 设计模式
  • 操作系统
  • 结构化开发
  • 软件工程
  • 计算机网络
  • 上午题错题
在线工具 (opens new window)

购买兑换码请添加

添加时候请写好备注,否则无法通过。

  • 20-幻读是什么,幻读有什么问题?

    • 幻读是什么?
      • 幻读有什么问题?
        • 语义上的问题
        • 数据一致性问题
      • 如何解决这个问题?
        • 间隙锁
        • 优点
        • 弊端
    EasT-Duan
    2024-03-16
    随笔
    目录

    20-幻读是什么,幻读有什么问题?

    欢迎来到我的 ChatGPT 中转站,极具性价比,为付费不方便的朋友提供便利,有需求的可以添加左侧 QQ 二维码,另外,邀请新用户能获取余额哦!最后说一句,那啥:请自觉遵守《生成式人工智能服务管理暂行办法》。

    正文

    # 幻读是什么?

    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
    10

    如果只在 d = 5 这一行加锁其他的行不加锁。假设如下的场景,只是说明,如下场景在真实 Mysql 中无法复现

    Session A Session B Session C
    begin;
    select * from t where d = 5 for update;//(5,5,5) update t set d =5 where id = 0;
    select * from t where d = 5 for update;//(0,0,5),(5,5,5)
    insert into t values(1,1,5);
    select * from t where d = 5 for update;//(0,0,5),(5,5,5),(1,1,5)
    commit;

    会发现,在次读取后 Session A 的结果都会变多。这就是幻读:一个事务在前后多次查询同一范围的数据,看到了上次查询没看到的行。

    注意

    • 在可重复度的隔离前提下,默认的查询是快照读,是无法看到其他事务提交的数据的,幻读只会在当前读的前提下发生。
    • 幻读特指的是新增的数据,而删除和更新指的是不可重复读。

    # 幻读有什么问题?

    # 语义上的问题

    首先 Session A 在一开始就声明了我把所有 d=5 的行锁住,其他的事务都不能来进行读写。而实际上这个语义被破坏了。

    Session A Session B Session C
    begin;
    select * from t where d = 5 for update;//(5,5,5)
    update t set d =5 where id = 0;
    update t set c =5 where id = 0;
    select * from t where d = 5 for update;//(0,0,5),(5,5,5)
    insert into t values(1,1,5);
    update t set c = 5 where id =1 ;
    select * from t where d = 5 for update;//(0,0,5),(5,5,5),(1,1,5)
    commit;

    Session B 将 id=0 的 d 改成了 5,理应被锁住,但是紧接着又将 c 的值改成了 5 故这个语义被破坏了。

    # 数据一致性问题

    Session A Session B Session C
    begin;
    select * from t where d = 5 for update;//(5,5,5)
    update t set d = 100 where d = 5 ;
    update t set d =5 where id = 0;
    update t set c =5 where id = 0;
    select * from t where d = 5 for update;//(0,0,5),(5,5,5)
    insert into t values(1,1,5);
    update t set c = 5 where id =1 ;
    select * from t where d = 5 for update;//(0,0,5),(5,5,5),(1,1,5)
    commit;

    分析一下:最后的结果为多少?

    1. Session A 将 d 改为 100,id=5 这一行变成 (5,5,100),但是这个是在最后一步提交的。
    2. Session B 将 id = 0 这行变成 (0,5,5);
    3. Session C 插入一行 (1,1,5),有将这行改成 (1,5,5)

    根据这个看看 binlog 日志(binlog 的写入是在 commit 之后)

    update t set d = 5 where id = 0; /**(0,0,5)**/
    update t set c = 5 where id = 0;/**(0,5,5)**/
    insert into t values (1,1,5);/**(1,1,5)**/
    update t set c = 5 where id = 1;/**(1,5,5)**/
    update t set d = 100 where d = 5;/**把所d=5的都给更新成100了**/
    
    1
    2
    3
    4
    5

    如果这个 binlog 日志拿去恢复,会造成严重的数据不一致问题。

    将扫描的行全部加上写锁

    Session A Session B Session C
    begin;
    select * from t where d = 5 for update;
    update t set d = 100 where d = 5 ;
    update t set d =5 where id = 0;// 从这里开始就会加锁阻塞。
    update t set c =5 where id = 0;
    select * from t where d = 5 for update;
    insert into t values(1,1,5);
    update t set c = 5 where id =1 ;
    select * from t where d = 5 for update;
    commit;

    这里可以说明的是加的锁并不会导致新增的行被阻塞,binlog 如下

    insert into t values(1,1,5);		/**(1,1,5)**/
    update t set c = 5 where id = 1;	/**(1,5,5)**/
    update t set d = 100 where d = 5;	/**(1,5,100)**/	/**(5,5,100)**/
    update t set d = 5 where id = 0;	/**(0,0,5)**/
    update t set c =5 where id = 0;		/**(0,5,5)**/
    
    1
    2
    3
    4
    5

    可以发现 id = 0 的最后结果已经是 (0,5,5) 与预期结果一致,但是 id = 1 的在数据库中为 (1,5,5),而 binlog 中为 (1,5,100),即使在锁住全部数据的情况下还是无法阻止幻读的产生。但是 InnoDB 解决了这个问题。

    # 如何解决这个问题?

    # 间隙锁

    InnoDB 为了解决幻读引入了新的锁:间隙锁。

    • 间隙锁
      • 间隙锁锁的是两个值之间的空隙,但是并不会锁住两个值,可以理解为开区间。
      • 间隙锁之间有冲突的只是在同一个间隙内插入这个动作,而间隙锁之间没有冲突。

    锁冲突如何理解?比如行锁:除了读读不冲突,读写,写写之间都存在冲突。然而,如果两个事务试图在不同的间隙内插入新行,则它们不会发生冲突。

    Session A Session B
    begin;
    select * from t where c =7 lcok in share mode;
    begin;
    select * from t where c =7 lcok in share mode;

    间隙锁在 (5,10),Session A 查询的这个记录不存在,Session B 也同样,但是两者都是同一个任务:保护区间内不被插入新的值,所以两者不冲突。

    只有在可重复读的隔离级别下,才会有间隙锁。读已提交的隔离级别下不会有间隙锁。

    # 优点

    在上面说了间隙锁并不会对当前的值进行加锁,那么 InnoDB 用 next-key lock 来解决这个问题,next-key lock 是间隙锁的一种变体,可以理解为行锁 + 间隙锁,锁的是间隙 + 右边界行(左开右闭),对上面插入来说就是 (-∞,0]、(0,5]、(5,10]、(10,15]、(15,20]、(20, 25]、(25, +supremum]。这个 supremum 是 InnoDB 为每个索引创建了一个不存在的最大值。来保证这个左开右闭。

    # 弊端

    next-key lock 确实解决了间隙锁的问题,但是也引入了一些困扰,例如。如果一个事务获得了对某个间隙的 next-key lock,则它可以新增更新或删除该间隙内的任何行。然而,如果另一个事务随后在该间隙中插入新行,则第一个事务的新增更新或删除操作可能会失败。

    Session A Session B
    begin;
    select * from t where id = 9 for update ;
    begin;
    select * from t where id = 9 for update ;
    insert into t values(9,9,9);
    blocking
    insert into t values(9,9,9);
    死锁

    这里就印证了上面说的两个间隙锁是同一个任务的时候并不会冲突,Session A 锁住了 (5,10) 防止有插入,Session B 也是,AB 都获取到了间隙锁,然后都在该间隙中插入新行,B 需要等待 A,A 需要等待 B,所以就死锁了。

    #MySQL
    上次更新: 2025/04/12, 07:54:33
    最近更新
    01
    Reactor 核心
    02-24
    02
    前置条件
    10-30
    03
    计算机网络
    09-13
    更多文章>
    Theme by Vdoing | Copyright © 2019-2025 powered by Vdoing
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式