从锁的机制解释Repeatable Read为什么是Mysql默认事务隔离级别

1,286 阅读7分钟

今天中午准备吃饭的时候,随便走进了一家餐厅里,找了一个位置就坐了下来,随手一招就把服务员喊过来问道

菜单呢?

这时候服务员愣了一下 开始找菜单去哪儿了。结果发现被另外一个客人A拿在手上了。

我说你这么大的餐厅不会只有一份菜单吧?

这时服务员从抽屉里面不慌不忙的拿出另外一个备份的菜单给我,这个场景怎么样? 有没有似曾相识与mysql的一致性非锁定读是不是基本一致?

一致性非锁定读

其主要的意思就是在讲MVVC多版本并发的情况下,怎么让读不会被阻塞。在上面的场景中客户A是一个事务的角色,而我也是一个事务的角色菜单是某行的记录, 当 服务员(Mysql服务器) 发现某个记录被锁定的时候,依旧可以让你读到数据,只不过在这时候你读的不是该行数据,而是行记录的快照数据,也就是最新事务提交过后的版本。

应对这上面的场景,也就是服务员把备份的菜单给我,因为备份的菜单并不会有客户拿得到 (不会有其他事务会去锁定快照数据)

但是在一致性非锁定读的环境下有2种情况需要注意

  1. REPEATABLE READ
  2. READ COMMITED 这2种情况在一致性非锁定读的表现是不一样的。我们可以看个例子。

我们先创建一张表,并且插入几条数据

image.png 我们先查看一下当前的事务隔离级别

image.png 也就是mysql 默认的事务隔离级别 repeatable read,这时候我们新开2个connection,分别单独开启事务

image.png 这时候我们在左边的窗口插入一条数据,提交事务, 再到右边窗口看看

image.png

依旧是3条数据。这就是repeatable read的特性,在当前事务下,并不会读到其他事务依旧提交的值。

接下来我们把事务隔离级别切换成read commited

image.png

然后我们再开启事务,并且执行查询语句

image.png

这时候,我们在左边窗口来一个插入,提交,再到右边窗口看看

image.png 这时候,你会发现 右边窗口 居然能直接把左边事务提交的值给查询出来。

也就是说在Read Committed的事务隔离级别之下,当前事务是采取读取最新的一份快照数据

而在Repeatable Read的事务隔离级别下,当前事务采取的是读取事务开始时候的数据版本

一致性锁定读

对于一致性锁定读的操作有2中

select ... For Update

select ... lock in share mode

这两者的区别主要在于前者是对行记录添加X锁, 该锁会与任何其他类型的锁冲突,阻塞。而后者只是添加了一个S锁,而其他事务也可以在该行记录上添加S锁

举个栗子~还是餐厅的情景。你在餐厅里面只能阅读菜单,但你却无法修改菜单。而菜单一旦在修改的情况下。你是无法阅读了,因为菜单这时候被收了回去 统一修改。

这时候相当于服务员(事务A) 对菜单添加了X锁导致无法看到菜单(该行记录) 那请问我现在能不能看到这行记录呢(对这行记录添加S锁)? 结合刚才的思路 想一想?

答案是可以看,但不能加锁。你可以看到菜单的备份(数据快照版本) 服务员会把备份的菜单给你,你可以先看着。 等菜单修改完毕(事务把锁释放),这些备份的就会收回来换成新的。记得锁都是在事务的开启下进行的

在讲解锁的算法之前,我觉得得先探讨2个问题,锁的存在是为了解决什么问题?其他有几种问题,但我这边只探讨面试比较常见的2种

锁的问题

  1. 脏读(dirty read)
  2. 不可重复读 (phantom problem)

脏读

什么是脏读?, 如果你想知道什么是脏读,现在就带你研究。

脏读是指事务对缓存池中记录的修改,并且还没提交。 直白点,就是骚年你开的事务还没commit阿 而另外一个事务却能读到你更新的内容。这显然就是违反了事务的隔离性

序号sessionAsessionB
1SET @@tx_isolation='read-uncommitted'
2SET @@tx_isolation='read-uncommitted'
31begin;
41select * from read
----记录----
1,2
3,4
5insert into read value (7,8)
61select * from read
----记录----
1,2
3,4
7,8

很明显就是在seesionB里面可以直接读取到sessionA插入的数据,这是不正确的。但为什么你不会遇到呢?

就是因为这是在事务隔离级别read uncommited 下才会发生,我们的mysql innodb默认并不是它。

不可重复读(幻读)

幻读是指在一个事务下,两次同样的sql语句,查询出来的确实不一样的结果,也就是可能在第二次读的时候返回了一些第一次不存在的行。我相信说完这个,你就很清晰的知道为什么mysql innodb默认是采用repeatable read作为默认的事务隔离级别了

对于幻读,我们来演示一下? 我们先创建一张表

image.png 并且随便撒点数据上去,开启2个session

image.png 然后我们把事务等级修改成为read committed 然后对数据加锁。把 a>5的数据全部加锁,这时候再往后插入10,11这一行数据,我们会发现在sessionA的结果如下

image.png

我们依旧可以读到我们加了锁的数据,这是为什么? 接下来我们就讲解mysql锁的机制,去解决这些问题。

锁的算法

  1. Record Lock 单个行记录上的锁
  2. Gap Lock 间隙锁
  3. Next-Key Lock 范围锁定,并且锁定记录本身

Record Lock

对于record lock的定义很简单, 它就是针对表的隐式主键做来进行锁定的。 我们来看个例子证明一下

image.png 我们会发现, sessionB 卡在里了。然后我们看一下目前锁的记录

image.png 并且我们在事务表中也能发现

image.png 当前事务正在等待锁的释放。

Gap Lock 间隙锁

间隙锁通常是与 next-key lock 一起使用。我们直接讲next-key lock并且顺便讲gap lock

Next-Key Lock

这个锁的设计,主要也是为了解决我们刚才所说的幻读的问题,也就是对某个范围进行加锁。我们只要记得next-key lock就是对某个范围的锁然后我们直接上例子来观察。

image.png

然后我们随便插入点数据。这时候 a 是主键索引b是辅助索引

image.png 这时候,在sessionA里面 对b=3的数据加一个X锁。 然后在sessionB里面对a=5的数据再加一个S锁,这时候会发现 sessionB 就卡在那里了。

这时候有人会说 那不是废话吗。 b=3的时候 a=5阿, 那肯定 不能对a=5加锁吖。

那我们再看一个例子?

image.png

这时候。我还是锁住了b=3的记录,然后我在sessionB里面插入了一条数据 2,2 然后居然被阻塞了? 这是为什么?

其实这就是next-key lock的作用,对于辅助所以,其实next-key lock锁住的记录是 (1,3],和 (3,6] 这是什么意思?

这里是指,如果你插入的数据

例子1. 主键数据a在 (3,5) 且 b在 [1,3]

例子2. 主键数据a在 (5,7) 且 b在 [3,6]

这两个范围内的话。都是会被锁定的。对于例子1, 其实是next-key lock的作用,对于例子2, 是 gap lock的作用 这里需要特别注意一下, 因为innodb引擎默认会对辅助索引的下一个键值加上一个gap lock, 也就是例子2

总结

其实以上的内容就是今天所有的内容了。主要是讲解为什么我们要使用repeatable read作为默认的mysql innodb事务隔离级别, 原因其实很明了了。为了解决幻读,脏读等问题。以及让各种并发不会产生异常问题。因此才选择了repeatable read