在MySQL事务初识中,咱们了解到不一样的事务隔离级别会引起不一样的问题,如在 RR 级别下会出现幻读。但若是将存储引擎选为 InnoDB ,在 RR 级别下,幻读的问题就会被解决。在这篇文章中,会先介绍什么是幻读、幻读会带来引发那些问题以及 InnoDB 解决幻读的思路。html
实验环境:RR,MySQL 5.7.27mysql
为了后面实验方便,假设在数据库中有这样一张表以及数据,注意这里的 d 列并没索引:sql
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
就是当前读,老是读取当前数据行的最新版本,关于数据行版本问题可参考事务究竟有没有被隔离ui
回到幻读,有以下 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 级别下,默认已经将幻读的问题的解决,这里仅做为思考的过程。code
为了更好的展示幻读带来的问题,为 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;
时,不但会将全表的数据行锁住,还会将间隙锁住。
这里提一下,若是对为何锁住全表的数据有疑问,能够看下以后关于如何加锁的原则这篇。
在事务是否隔离这篇文章中知道,行锁(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 给每一个索引加了一个不存在的最大值。
间歇锁的引入,虽然解决了幻读的问题,但同时也下降了并发度。
好比下面的业务逻辑,锁住一行,若是该行不存在就插入不然就更新:
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 呢?
先来看下 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 天然就没有这个问题。
在这篇文章中,主要介绍了幻读的问题,知道了 InnoDB 为了在 RR 级别上解决该问题,引入了间歇锁。并知道了间歇锁会下降并发率,增长死锁状况的发生。还了解到 next-key lock 其实就是行锁(Record Lock)和间隙锁的合集。
在业务不须要 RR 支持下,若是想提升并发率,能够将隔离级别设置成 RC 并将 binlog 格式设置成 row.