MySQL事务初识中,我们了解到不同的事务隔离级别会引发不同的问题,如在 RR 级别下会出现幻读。但如果将存储引擎选为 InnoDB ,在 RR 级别下,幻读的问题就会被解决。在这篇文章中,会先介绍什么是幻读、幻读会带来引起那些问题以及 InnoDB 解决幻读的思路。

实验环境:RR,MySQL 5.7.27

为了后面实验方便,假设在数据库中有这样一张表以及数据,注意这里的 d 列并没索引:

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);

什么是幻读?

幻读:是指在同一个事务中,前后两次查询相同范围时,得到的结果不一致,后一次查询到新插入的行。

这里需要注意的是,由于在 RR 级别下,普通的读是快照读(一致性读),所以幻读仅发生在当前读的基础上

举例来说:

select * from t where d=0 就是快照读,对于同一个事务来说,每次读到的结果是一样的。

select * from t where d=0 in share mode 或 select * from t where d=0 for update 就是当前读,总是读取当前数据行的最新版本,关于数据行版本问题可参考事务究竟有没有被隔离

回到幻读,有如下 Session:

Session A Session B
begin:  
select * from t where d=5 for update;  
  insert into t values(1,1,5);
select * from t where d=5 for update;  
commit;  

Session A 第一个 select 结果是:(5,5,5),第二个 select 结果是(1,1,5)和(5,5,5)。由于两次当前读的结果不一致,这就表明出现了幻读。有一点需要说明,你在尝试 Session B 会被阻塞,因为在 RR 级别下,默认已经将幻读的问题的解决,这里仅作为思考的过程。

幻读带来的问题?

为了更好的展现幻读带来的问题,为 Session A,B 添加一条 SQL:

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

1. 破坏了语义

新的 Session B 中,除了添加一条新记录外,还修改了新记录的 d 值。这就破坏了 A 的语义, Session A 的目的就是锁住所有 d=5 的行,不让其被操作。

2. 数据一致性的问题

锁的存在就是为了避免在并发条件下,出现的数据一致性的问题。这里我们看下 A,B 提交后数据库的数据结果:

id=1 插入了一条新的记录,id=5 的记录 d 被修改成 100.

(0,0,0),
(1,5,5);
(5,5,100),
(10,10,10),
(15,15,15),
(20,20,20),
(25,25,25);

上面的结果看似没有问题,这里看下生成的 binlog 的执行逻辑,由于 Session B 先提交,所以对应语句在前:

# Session B 先执行
insert into t values(1,1,5); /*(1,1,5)*/
update t set c=5 where id=1; /*(1,5,5)*/ # Session A 后执行
update t set d=100 where d=5;/*所有d=5的行,d改成100*/

如果拿此 binlog 进行数据恢复,可见 id=1 的这样行被修改成了(1,5,100),这就出现了数据一致性的问题。

如何解决幻读?

对于 select * from t where d=5 for update; 来说,锁住d=5对应的行或者锁住扫描过程中所有的行都是没有用的, 因为插入并不影响之前行的操作,所以 InnoDB 为了解决幻读,引入了新的锁 - 间隙锁。

间隙锁,会将行之间的空隙锁住。比如,初始化是插入的 6 个值,就会产生 7 个空隙。

当再执行select * from t where d=5 for update;时,不但会将全表的数据行锁住,还会将间隙锁住。

这里提一下,由于 d 没有索引,所以走全表扫描,会将整个表锁住。

事务是否隔离这篇文章中知道,行锁(Record Lock)按照类型分为读锁和写锁,并且行锁与行锁在不同的事务间是互斥的。

但间隙锁不同,正由于它解决的是幻读插入的问题,所以间隙锁仅仅对插入操作本身互斥,不同事务之间的间隙锁并不互斥。

比如下面这两个事务:

Session A Session B
begin:  
select * from t where c=7 lock in share mode;  
update t set d=100 where d=5; begin;
  select * from t where c=7 lock in share mode;

由于 c=7 这条记录并不存在,出于共同的目的,防止其他值的插入。Session B 不会被阻塞。Session A 和 Session B 都会为其加上(5,10)的间隙锁。

为了加锁时的方便,间隙锁和行锁的合集称为 next-key lock.行锁锁住的是存在的记录行,间隙锁锁住的是行之间的空隙。而 next-key lock 锁住的是两者之和,比如 select * from t for update 锁住的就是 (-∞,0]、(0,5]、(5,10]、(10,15]、(15,20]、(20, 25]、(25, +supremum]。

(-∞,0],由间隙锁 (-∞,0]) 和行锁 0 组成,其他类似。

+supremum 表示 InnoDB 给每个索引加了一个不存在的最大值。

next -key lock 影响并发怎么办?

间隙锁的引入,虽然解决了幻读的问题,但同时也降低了并发度。

比如下面的业务逻辑,锁住一行,如果该行不存在就插入否则就更新:

begin;
select * from t where id=N for update; /*如果行不存在*/
insert into t values(N,N,N);
/*如果行存在*/
update t set d=N set id=N; commit;

当查询一条不存在的记录时,会给所在 id 的间隙加上间隙锁。假如同时出现并发的情况,由于间隙锁之间不冲突,两个事务都会加上间隙锁。之后执行插入时,每个事务的插入操作与另外事务的间隙锁出现冲突,进而引发死锁。

由此看见,间隙锁的引入导致同样的语句锁住更大的范围,降低了并发度。

假如业务需求并不需要间隙锁怎么办,这时可以将隔离级别 RC,在此级别下就不存在间隙锁了。由此引出一个问题,为什么一般在 RC 下,binlog 的格式要设置成 row 呢?

为什么 在 RC 级别下,binlog 格式要设置成 row?

先来看下 binlog 的三种格式:

  • --binlog-format=STATEMENT :在 Master 向 Slave 同步时,会以原生的 SQL 语句进行同步。
  • --binlog-format=ROW :Master 会把被操作后的表中的行记录在日志中, 向 Slave 同步。简单来说同步的就是表中的数据。
  • --binlog-format=MIXED :默认会以 STATEMENT 的方式记录,但在一些情况下可以自动的切换成 ROW 方式,比如执行用户自定义的函数 UUID.

这里采用反证法,如果在 RC 级别下,将 binlog 的格式设置成 Statement 会发生什么?

还是使用之前 RR 级别下幻读的例子:

Session A Session B
begin:  
update t set d=100 where d=5;  
   
  insert into t values(1,1,5);
  update t set d=5 where id=1;
   
commit;  

得到的结果是一样的,Binlog 日志中 Session B 先执行,Session A 后执行,A 会把 id=1 中 d 的值改为 100,出现了 binlog 和 数据库数据不一致的现象。

而基于 ROW 格式则不同,binlog 日志中记录的是被操作后的数据,不是重新执行 SQL 自然就没有这个问题。

可重复读隔离级别下:next_key_lock 锁住sessionB,保证数据一致

RC隔离级别下,session A只有行锁,不会锁到session B,只会改到session A,binlog为row格式,同步A、B的数据到从库。如果是statement格式则会有问题。

总结

在这篇文章中,主要介绍了幻读的问题,知道了 InnoDB 为了在 RR 级别上解决该问题,引入了间隙锁。并知道了间隙锁会降低并发率,增加死锁情况的发生。还了解到 next-key lock 其实就是行锁(Record Lock)和间隙锁的合集。

在业务不需要 RR 支持下,如果想提高并发率,可以将隔离级别设置成 RC 并将 binlog 格式设置成 row.

幻读在 InnoDB 中是被如何解决的?(转)的更多相关文章

  1. 幻读在 InnoDB 中是被如何解决的?

    在MySQL事务初识中,我们了解到不同的事务隔离级别会引发不同的问题,如在 RR 级别下会出现幻读.但如果将存储引擎选为 InnoDB ,在 RR 级别下,幻读的问题就会被解决.在这篇文章中,会先介绍 ...

  2. InnoDB的锁机制浅析(三)—幻读

    文章总共分为五个部分: InnoDB的锁机制浅析(一)-基本概念/兼容矩阵 InnoDB的锁机制浅析(二)-探索InnoDB中的锁(Record锁/Gap锁/Next-key锁/插入意向锁) Inno ...

  3. MVCC 能解决幻读吗?

    MySQL通过MVCC(解决读写并发问题)和间隙锁(解决写写并发问题)来解决幻读 MySQL InnoDB事务的隔离级别有四级,默认是“可重复读”(REPEATABLE READ). 未提交读(REA ...

  4. 幻读:听说有人认为我是被MVCC干掉的

    @ 目录 前言 系列文章 一.我是谁? 二.为什么有人会认为我是被MVCC干掉的 三.我真的是被MVCC解决的? 四.再聊当前读.快照读 当前读 快照读 五.告诉你们吧!当前读的情况下我是被next- ...

  5. MySQL到底有没有解决幻读问题?这篇文章彻底给你解答

    MySQL InnoDB引擎在Repeatable Read(可重复读)隔离级别下,到底有没有解决幻读的问题? 网上众说纷纭,有的说解决了,有的说没解决,甚至有些大v的意见都无法达成统一. 今天就深入 ...

  6. ACID特性及幻读的理解

    事务是关系型数据库的重要特性.它是一个包含了一条或多条SQL语句的逻辑原子单元.一个事务包含的SQL要么全部提交,要么全部回滚. Oracle 官方文档中对事务的描述如下: A transaction ...

  7. 【Mysql】数据库事务,脏读、幻读、不可重复读

    一.什么是数据库事务 数据库事务( transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位.事务由事务开始与事务结束之间 ...

  8. Innodb 中 RR 隔离级别能否防止幻读?

    问题引出 我之前的一篇博客 数据库并发不一致分析 有提到过事务隔离级别以及相应加锁方式.能够解决的并发问题. 标准情况下,在 RR(Repeatable Read) 隔离级别下能解决不可重复读(当行修 ...

  9. MySQL InnoDB四个事务级别 与 脏读、不反复读、幻读

    MySQL InnoDB事务隔离级别脏读.可反复读.幻读 希望通过本文.能够加深读者对ySQL InnoDB的四个事务隔离级别.以及脏读.不反复读.幻读的理解. MySQL InnoDB事务的隔离级别 ...

随机推荐

  1. 多测师讲解接口测试 _理论基础知识001_高级讲师肖sir

    前言: 我们今天进入接口测试的学习! 今天学习的内容是偏向理论 接口理论 了解接口测试(1) 一.什么是接口测试? 接口统称api,即程序与程序之间的对接.交接.交互.是测试系统组件间接口的一种测试. ...

  2. python写文件时遇到UnicodeEncodeError: 'gbk' codec can't encode character的解决方式

    在window平台,文件的默认编码是gbk, 此时如果写入的字符串的编码是utf-8就会引发这种错误,打开文件的编码必须与字符串的编码一致 with open('content.txt','w',en ...

  3. 你不知道的MySQL,以及MariaDB初体验

    MySQL 是一个跨世纪的伟大产品,它最早诞生于 1979 年,距今已经有 40 多年的历史了,而如今比较主流的 Java 语言也只是 1991 年才诞生的,也就是说 MySQL 要比 Java 的诞 ...

  4. qemu-kvm安装and配置桥接和SR-IOV

    kvm和docker的区别:kvm是全虚拟化,需要模拟各种硬件,docker是容器,共享宿主机的CPU,内存,swap等.本文安装的qemu-kvm属于kvm虚拟化,其中:kvm负责cpu虚拟化和内存 ...

  5. Linux命令行history

    概述 当执行命令后,系统默认会在内存记录执行过的命令 当用户正常退出时,会将内存的命令历史存放对应历史文件中,默认是~/.bash_history 登录shell时,会读取命令历史文件中记录下的命令加 ...

  6. sql左连接查询+右表带有条件的实现

    select * from A表 a left join B表 b on a.id=b.a_id and b.字段='/*条件*/' ; 可查出左表所有数据 select * from A表 a le ...

  7. 建议你吃透python这68个内置函数!

    内置函数就是Python给你提供的, 拿来直接用的函数,比如print,input等. 截止到python版本3.6.2 ,一共提供了68个内置函数,具体如下 abs() dict() help() ...

  8. Python基础及爬虫入门

    **写在前面**我们在学习任何一门技术的时候,往往都会看很多技术博客,很多程序员也会写自己的技术博客.但是我想写的这些不是纯技术博客,我暂时也没有这个能力写出 Python 或者爬虫相关的技术博客来. ...

  9. JS的各种数据类型

    Number js与其他编程不一样,不管是整数还是浮点,都称为数字类型(Number) 例:123,1.11111,-960 当该类型结果不存在时,即表示为 NaN (Not a Number) In ...

  10. 支持向量机SVM基本问题

    1.SVM的原理是什么? SVM是一种二类分类模型.它的基本模型是在特征空间中寻找间隔最大化的分离超平面的线性分类器.(间隔最大是它有别于感知机) 试图寻找一个超平面来对样本分割,把样本中的正例和反例 ...