面试官:事务隔离级别和锁有什么关系(下)|8月更文挑战

1,135 阅读6分钟

这是我参与8月更文挑战的第2天,活动详情查看:8月更文挑战

前文

这是系列文章的下半部分,前半部分在这里

面试官:事务隔离级别和锁有什么关系(上)

那么我们接着聊聊事务隔离级别和锁的关系

悲观锁和乐观锁的运用

我们锁的区分里面,可以通过锁的机制把锁分成悲观锁乐观锁的区别

悲观锁

悲观锁顾名思义就是对数据修改的态度是一种保守的态度,就是每次对数据进行处理的时候,都对其进行加锁,一般上来说悲观锁的实现都是基于数据库本身的一个锁的机制

例如,在一个事务更改数据的时候,就不允许其他事务对这个数据进行修改或者读取,同样的,当一个事务正在读取一个数据的时候,也不允许其他事务对这个数据进行修改。

其实对于悲观锁对读写的两种方式也可以把它叫做读锁写锁,读锁之间是相互兼容的,写锁跟读写锁都是不兼容的。

乐观锁

反观乐观锁,则是采用了一种更加宽松的机制。悲观锁采用的是数据库本身锁的机制,这种好处就是很好的保证了一个锁的独占性,但随着而来的问题就是会带来较大的性能开销,特别是当事务的流程较长的时候,这种开销是非常大的。

乐观锁则采用的是一种版本控制的方式,也就是在创建数据行的时候,会额外的维护一个version的数据列,用来表示当前的版本,当读取数据的时候,将版本号也读取出来,当对数据进行更新的时候,同时也会使得当前的version加一,此时提交数据库的信息,当提交的表单数据的version数字小于或者等于当前表单行的版本号的时候,就说明这是一个过期的数据,也就不会被提交。

MySQL中Innodb的Mvcc

我们知道的,对于Innodb存储引擎来说,它所采用的一个并发控制的方式也是同乐观锁的思想有点类似。

在每次创建一个数据行的时候,它还会同时创建并且维护两个版本号,一个为创建版本号,一个为删除版本号,每次开启一个事务的时候,版本号都会进行一次递增,对于Innodb可重复读这个隔离级别来说:

  1. Select的时候,读取创建本版好<=当前版本号,删除版本号为空,或者是大于当前删除版本号的时候,则会把数据读出。

  2. Insert的时候,保存当前事务的版本号为创建版本号。

  3. Update的时候,插入一条新的记录,保存当前的版本号为创建版本号,同时当前版本号保存为原来数据的删除版本号。

  4. Delete的时候,保存当前版本号为删除版本号。

这里可能有些绕,让我们来看一幅图来解释一下

4444.png

在上一篇文章的时候,我提到了一个对于可重复读这个事务隔离级别来说,是会存在一个幻读的问题的,但是在MySQL的Innodb存储引擎下却没有这个问题,这就是Innodb的Mvcc的关系。

我们可以看到,虽然事务A在事务B,C提交了之后又二次的去读id=1这条数据(其中事务C是Insert),但是却并没有变化,可以看出来对于Innodb的这个RR隔离级别下,是解决了一个幻读的问题的,而这也就是通过Mvcc来控制的

当然,这里的读其实也是有区别的。

两种不同的读

我们由上面那张图可以看出,在Select的时候,其实读取到的并不是最新的一个数据,而对于这种读取历史数据的方式,一般把它叫做快照读,而另一种与之相对的,则是当前读

MySQL为了增加并发性,也就是减少锁的使用,所以在Select的情况下,采用的是一个快照读的方式,这样就在读取数据的时候,不需要去加一个读锁。

但是对于读取当前数据版本的方式,也就是Insert和Update这些,则是采用了一个当前读,也就是读取当前的一个版本号。

写数据(当前读)

其实这里说的读,并不是说读取数据的意思,而是读取一个版本号,也就是说,在写数据的时候,也就是Insert的时候,对于RR级别的来说,是会出现一个幻读的问题的,而Mysql为了解决在当前读的情况下的一个幻读的问题,就引入了一种锁的机制,也就是间隙锁(Next_key锁)

Next_Key锁

之前的文章里面已经提到了,锁也可以分成表锁跟行锁,而MySQL对于数据的控制采用的是行锁,但是行锁没办法解决其他事务Insert的时候,数据错误的问题。而这就引入了一个间隙锁的概念。

我们来看拿下下面这个例子

事务A:
Select age from User where id = 10; // age = 100
Update User set age = 10 where id = 10;
Select age from User where id = 10;

事务B:
Insert into User values (10,2222);

我们可以看到在事务A执行的过程中,事务B也执行了Insert的操作,对于RR级别的来说,对于事务A在Update的时候,事务B的Insert是会被阻塞的,而这一步用的就是一个间隙锁的方法。

也就是在Update的时候,除了锁住本身那个数据行以外,还会锁住周围的一些数据行,这样就避免了在Insert的时候的一个幻读的问题。

所以我们可以知道,行锁解决了其它事务对于本事务下数据行的改和删除的一个问题,而Gap锁则解决了数据的新增的一个问题。

注意

这里有一个需要注意点,就是如果说当前where的条件没有加索引的话,对于Update来说,它的间隙锁是会锁住所有行的,同时优化器也没有办法对它进行提前释放。

最后

一篇文章,分成两篇来写,也是有点被逼无奈,希望下次面试的时候,再被问道同样的问题,我可以跟面试官背头一波。

大家好,我是薛定谔的狗,希望大家喜欢我的文章。

image.png