** 前言:**本节内容是事务里面最难的一部分, 就是理解mvcc快照读和read view。这两个部分需要了解隔离性里面的四种隔离级别。 博主之前讲过,但是担心友友们不了解, 所以这里开头进行了复习。 下面开始我们的学习吧! **ps:本节内容很难, 希望友友们沉下心认真学习哦!**
我们先来回忆一下上接内容讲解的隔离性的四种隔离级别, 以及并发产生的问题:
- 脏读:一个事务在执行中,读到另一个事务的更新但是还没有commit的数据, 这种情况叫做脏读。
- 不可重复读:一个事务提交前和提交后, 另一个事务select查到的数据结果是不一样的。 也就是说一个事务一直select, 结果某一次select的时候, 数据突然变了。
- **幻读:读者读者可能多出来了一条数据,就类似于出现了幻觉。 **
- 读未提交: 会导致脏读,不可重复读, 幻读现象。
- 读提交 :解决了脏读, 但是有不可重复读, 幻读。
- 可重复单独: 解决了脏读, 不可重复读, 但是有些数据库的insert会有幻读现象(mysql解决了幻读)
- 串行化:加锁, 不进行并发执行, 没有上面的问题。
一致性
事务执行的结果,必须使数据库从一个一致性状态,变到另一个一致性状态。当数据库只包含事务成功提交的结果时,数据库处于一种一致性状态。如果系统运行发生中断,某个事务尚未完成而被迫中断,而改变未完成的事务对数据库所做的修改已被写入数据库,此时数据库就处于一种不正确的状态。因此一致性是通过原子性保证的。
三种并发方式的安全隐患
- 读读并发:不存在任何问题,不需要并发控制。
- 读写并发:有线程安全问题,可能造成事务隔离性问题可能遇到脏读,幻读,不可重复读
- 写写并发:有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失
读写并发,也就是一个事务在写,一个事务在读。要保证读到的是原来的,和另一个事务写的不一样。
这个使用的是一种无锁的并发控制MVCC。
- 1、每个事务都要有自己的事务ID,可以根据事务的大小,来决定事务到来的先后顺序。
- 2、mysqld可能会面临处理多个事务的情况。事务在使用者的角度看来是原子的,但是本质上是有过程的。所以就证明事务也有自己的生命周期。一》mysqld要对事务进行管理:先描述,再组织。事务在我看来,mysqld中一定是对应的一个或者一套结构体对象/类对象
- 3、所以,事务也要有自己的结构体。
下面我们都在讲解mvcc以及版本链, 很重要, 很难理解, 需要反复观看。
三个隐藏字段
其实我们之前在建表的时候, mysql也会给我们添加三个默认的,隐藏的字段:
- DB_TRX_ID:6byte,每一条数据后面都要有一个属性,就是DB_TRX_ID,无论是单sql,但是我们手动begin起的事务。都算是事务,都有自己的事务ID,而DB_TRX ID,就是最后操作这条数据的事务ID。
- DB_ROLL_PTR:在MySQL中,对于某一行数据来说,我们要修改他,并不是直接就修改了。 mysql要先将数据保存一份,然后再改 这样的话,我们改完之后,我们也知道未来这个字段历史是谁。所以,为了保证能够找到这个最后的历史信息,就有了这个DB_ROLL_PTR。
- DB_ROW_ID:隐藏的自增ID,如果数据库表中没有主键,In noDB会自动以DB_ROW_ID产生一个聚族索引。大小6byte
Undo日志
上面的红框框就是操作系统, 然后上层就是用户层。里面有我们的mysql的一个**buffer pool **, 然后undo日志就是在这个**buffer pool**里面。**undo log其实就是mysql里面的一段内存缓冲区**。作用**就是用来记录一些mysql里面事务的回滚操作**等。
假如现在有一行数据如下:![](https://i-blog.csdnimg.cn/direct/f0435ac412a248dba7c00edadf0ecb31.png)
这个数据我们知道是在b+树的叶子节点上面。(这个b+树也在buffer pool里面),现在有一个事务10, 想要对这行数据进行修改, 将名字张三改成李四。
因为要修改, 所以先将这条记录加锁, 然后将这条数据记录复制一份到undo log里面:
然后就开始修改, 现存数据name改成李四, 事务ID改成10, 然后回滚指针指向刚刚拷贝到undo log里面的数据。这就叫做历史版本。![](https://i-blog.csdnimg.cn/direct/4f29f0a6956c410ea6cdbf3046b632a3.png)
最后commit, 释放锁。 就行了。 (注意,这里示例是写写并发, 所以串行化, 加锁。)
然后现在又来了一个事务11,也要进行修改, 要将这个age修改成28. 那么就和上面一样, 先拷贝一份数据到undo log里面。
然后修改记录:
所以, 我们就有了一个历史版本链, 如果未来我们不想修改了, 后悔了, 就可以把undo log里面的历史数据拿出来。
mvcc多版本控制
我们把上面的版本链, 这种多版本控制就叫做mvcc多版本控制。而里面的一个一个的版本的数据, 我们就叫做快照。
另外,** undo log是一个临时缓冲区。 他管理的是一个事务内所形成的版本链条**。 什么意思? 就是说, 一个事务里面, 我们多次修改,多次update,才会形成版本链, 当事务commit的时候, undo log 就会被清空, 也就是free掉。 同样的, delete数据也能形成版本链, 因为delete的本质是把改行数据的"flag“置为删除。 所以也可以形成版本链。
问题是!insert可以形成版本链吗?——其实,**insert可以理解为是没有版本链的**,但是insert对应的数据也要放到undo log里面。但是**如果一旦commit, 这个insert对应的版本连也就可以被清空了。 而对于undate和delete来说就不一定, 因为可能也有其他的事务在访问这一条记录。 **
我们上面说的是update和delete, 以及insert。 那么select呢?我们知道, update和delete的对象一定是最新数据, 历史数据他们没有权限修改。 那么select我们怎么知道我们读取的是最新数据还是历史数据呢? 所以就有了当前读和快照读的概念。
- ** 当前读:读取最新的记录, 就是当前读, 增删改,都叫做当前读。select也可能当前读。**
- ** 快照读:读取历史版本,就叫做快照读。**
那么现在再来看我们之前的读写操作。 我们做过实验, 看到的是两个事务, 一个写一个读。 然后两个事务看到的数据是不同的数据。——就是因为我们的读取, 读取的是历史的数据。 而写,写的是当前的数据。 两个事务在访问不同的位置, 就不需要进行加锁, 所以就不会发生并发的问题, 我们就可以并发进行读写操作。所以, 这就是之前我们为什么能够读写并发的原因!!!
** 所以, 隔离性, 本质上是数据上的隔离, 也是版本上面的隔离。这就是不同的隔离级别让我们看到不同的版本!!!**
** 所以, 隔离, 隔离性本质上使用mvcc实现的,我们的事务回滚利用的也是利用mvcc版本控制的。**
read view
read view我们讨论的是rc和rr这两种隔离级别。read view就是事务进行快照读操作的时候生产的读视图,在该事务执行快照读的那一刻,会生成数据库当前的一个快照,记录并维护系统当前活跃事务的ID。(当每个事务开启时,都会被分配给一个ID,这个ID是递增的。所以最新的事务的ID更大)
read view是一个类,与事务的关系就类似于进程地址空间和PCB。两个数据结构之间使用指针连接起来。
当我们进行快照读的时候,对该记录创建一个read view读视图。然后对它填充数据,作为条件。 用来判断当前事务能够看到哪个版本的数据,既可能是当前数据, 也可能是undo log里面的历史数据。下面看一下这个read view结构体:
class Readview
{
private:
trx_id_t m_low_limit_id; //高水位,大于等于这个ID的事务均不可见。
trx_id_t m_up_limit_id; //低水位, 小于这个ID的事务均可见。
trx_id_t m_creator_trx_id; //创建该Read view的事务ID。
ids_t m_ids; //创建视图时的活跃事务id列表。
trx_id_t m_low_limit_no;
bool m_closed; //视图是否关闭
//....省略
};
上面重要的是两个水位线 , 事务id。m_ids就是当前正在打开的事务id列表。
现在一张图我们来理解read view:
在这张图里面,先看已经提交的事务。 已经提交的事务,就是说我们当前的事务read view的时候,如果当前事务ID和我们看到的版本链里面的事务ID一样, 那么就说明我们可以看到这个版本的数据。如果版本链里面的事务ID比m_ids里面最小的事务ID都小, 那么也能看到。 因为ID小, 代表事务启动的早, 那么后启动的事务就应该能看到先启动的事务。
再看快照后来的事务:我们说low_limit_id是当前事务read view后能看到的最晚启动的一个事务。 那么如果版本链里面的事务大于这个low_limit_id, 就不应该看到这个版本的数据。为什么会出现这种情况, 因为事务一定有先有后, 后来的事务ID一定大!
然后就是看正在操作的事务, 这里要注意的是,正在操作的事务不一定是连续的。 因为虽然我们说ID是递增的, 但是事务的执行时间是不同的, 有的事务很快就跑完了, 有的事务很慢才能跑完。 所以就会出现图中的情况。 而我们形成快照后m_ids里面的版本如果和某个版本的数据相同,就说明这个ID的事务还是活跃的, 没有commit, 那么就不能看到。如果没有, 那么就能看到。
** 另外,我们还要注意的是!read view是事务可见性的一个类,不是事务创建出来, 就有read view, 而是这个已经存在的事务, 首次进行快照读的时候,mysql形成read view!**
RC VS RR的本质区别
有了上面的read view的知识点之后, 我们就能谈RC和RR的本质区别了。我们谈论RC和RR的区别, 需要使用一个例子, 现在我们做几个实验。
首先第一个实验:![](https://i-blog.csdnimg.cn/direct/e606d14e72744baab8eb3b5ce96177f4.png)
这是account表。 并且此时的隔离级别是可重复读。然后我们两边都开启事务:
然后两边都进行快照读, 记录当前的read view:
接下来我们左边修改数据,然后commit:
然后我们查一下右边:
很显然, 因为是可重复读, 所以数据没有变化。 这也符合我们的预期。
现在我们来看一下实验二:
同样是隔离级别为可重复读。 但是这次右边不进行快照读。 而是等到左边commit之后,在进行快照读:
查右边:
我们会发现, id = 4这个字段被修改了。 也就是说我们的右边看到了左边的修改。
这里为什么会发生这种情况, 是因为我们的read view是在快照读的时候就生成, 然后RR隔离界别的read view只会生成一次。 就是第一次快照读的时候。 所以在第一个实验中, 我们一开始就使用了快照读。 此时m_ids显示左右两个事务都是活跃事务, 那么我们进行判断的时候, 右边的事务对于左边事务修改的版本就看不见。 实验二的时候, 我们一开始没有快照读, 而是等到左边事务commit后才进行快照读。那么**右边事务的m_ids里面就一定没有左边事务的ID**, 并且,因为**左边事务启动在右边事务快照读之前**, **所以左边事务的事务ID也不可能比右边事务的事务的low_limt_id大**。 那么这个时候左边事务的事务ID只有两种情况,** 一种情况是比右边事务的up_limit_id还要小。或者是在up_limit_id和low_limit_id之间但是没在m_ids里面**。 这**两种情况下右边事务都可以看到左边事务的修改版本!!!所以右边事务就能看到左边事务的修改数据版本!!!**
上面是RR。RC就好说了, **RR是只能生成一次read view, 只能进行一次快照**。 后面的事务无论做什么修改, 都要根据这一个快照进行判断能不能够看到这些版本数据。 但是**RC每次进行快照读都能生成快照, 每次快照读都生成快照**, 那么**low_limit_id就永远是当前系统下最大的事务ID**。 所以就能保证**只要其他事务commit了, 其他事务不是活跃事务**了, 那么就**只剩下了在up_limit_id和low_limit_id之间但是没在m_ids里面这两种情况**。 也就一定能看到了。 所以这就是RC的形成原因。 这就是RR和RC的根本区别!
——————以上就是本节全部内容哦, 如果对友友们有帮助的话可以关注博主, 方便学习更多知识哦!!!
版权归原作者 打鱼又晒网 所有, 如有侵权,请联系我们删除。