目录
前提知识 --
mysql事务 -- 事务的隔离性(测试实验+介绍,脏读,不可重复读,可重复度读,幻读)-CSDN博客mysql事务 -- 事务id介绍,数据库表的隐藏列,undo log的简单介绍,当前读(select的当前读方式),快照读(与隔离性的关系),读视图(三个字段介绍)-CSDN博客
MVCC多版本控制
介绍
多版本并发控制( MVCC )是一种用来解决 读-写冲突 的无锁并发控制
- 为事务分配单向增长的事务ID
- 为每个修改保存一个版本,版本与事务ID关联
- 读操作只读该事务开始前的数据库的快照
所以 MVCC 可以为数据库解决以下问题:
- 在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能
- 同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题
模拟
假设,有一个事务10,要对某条记录做修改
- 修改前,需要对记录加锁保护,并且拷贝一份放入undo log中
- 类似写时拷贝
然后将备份数据的地址填入该记录中的DB_ROLL_PTR中,形成版本链
- 然后再对原记录做修改
最后,更新DB_TRX_ID中的值为当前事务id(也就是10)
事务执行完成后,释放锁资源
版本链
InnoDB 为每一行数据维护的一种数据结构,它记录了数据行的不同版本
- 每当对一行数据进行插入、更新或删除操作时,都会创建一个新的版本,并将其链接到该行的版本链中
现在,又来了一个事务11,也是对这条记录做修改
- 要修改的记录肯定是新数据,历史数据已经是固定的了
和上面的流程一样,加锁->拷贝->更新两个隐藏列数据(回滚指针,事务id)
- 所以,我们继续扩充这个基于链表记录的历史版本链
这些一个一个的版本,称之为一个一个的快照
快照
快照是数据库在某一特定时刻的数据状态的副本
这就是为什么mysql可以实现事务隔离
- 因为保存了多个版本的数据
相反sql的记录
除此之外,它还会在日志中记录当前操作的相反sql,以便回滚
- 比如,执行一条insert,就存放一条对应的delete
- 这样执行回滚操作时,逆向执行新增的sql,数据就恢复了
这些多版本数据,由mysql来维护,称之为MVCC
清理机制
这些版本链会一直持续下去吗,会不会把undo log塞满呢?
undo log是临时缓冲区,保存的是事务运行期间数据的历史版本
- 事务一旦提交,该事务对应的版本链就会被释放掉
- 当然,如果同时还有其他事务正在使用这些版本,就依然保留着
- 总之,没有客户端使用某条记录时,再清理
InnoDB 使用后台线程定期检查和清理不再需要的 Undo Log 记录
不同操作对应的版本链
删除操作
删除操作对应的也有版本链
在删除一行记录时,数据库实际上会创建一个新的版本
- 新版本将逻辑上被标记为“已删除”
- 旧版本仍然可用于其他事务的读取操作,确保并发事务的隔离性
插入操作
insert也有吗?
- 插入的是新数据,没有历史版本的数据与它对应
但是,插入的数据,也还是需要放入undo log中
- 用来在不同隔离级别下让其他客户端看见
- 以及还要将相反sql保存起来,用于后续的回滚操作
如果当前事务提交了,insert对应的历史记录就可以被删除了
- 因为这是新插入的数据,在隔离性的作用下,只有执行插入操作的事务会看到
查询操作
它不会对数据做修改
- 所以维护多版本没有意义
那每个查询操作读取的是新数据还是历史版本呢?
- 取决于隔离级别的设置,以及使用select的方式
可见性判断
引入
目前,我们就有了两个结构
- 读视图(关于当前事务到来时,事务id的分配情况) -- 注意,读视图是在执行快照读后创建的,而不是事务到来就创建
- 历史版本链(包括每个修改该条记录的事务id)
接下来,就是根据[当前事务id]和[版本链中的事务id]做对比,得到自己应该读取的历史版本
- 这就是在进行可见性判断
我能看见的
如果当前要进行快照读的事务,就是更新该记录的事务
- 也就是说,我要查看的这条记录,就是我自己插入/修改的
- 那么我就应该看到这条记录
如果我查看记录时,[创建/修改该记录的事务]比[我查看时正在执行的最早事务]还要早出现,并且没有出现在我维护的活跃列表中
- 存在过且没有在运行,说明[创建/修改该记录的事务]已经提交了,并且是在我查看之前就已经提交了
- 我就应该能看见这条记录
只要我能看到这条数据,也就没必要继续遍历版本链了
- 因为后面的都是更老的数据
不应该看见的
如果一个事务对数据做出的修改/创建,是在我查看后,提交前进行的提交
- 虽然它也会形成版本链,但在我遍历版本链时,我不应该看见这条记录
- 因为事务的隔离性
图示
源码
id就是每条记录中的id
- 所以外层一定是在遍历版本链,然后将每个版本的事务id传进该函数
该函数被封装在readview这个类内部的,所以可以直接访问类内成员
过程
场景
假设有下列场景:
当事务4对数据进行修改,会形成一个该条记录的版本链:
事务2进行快照读时,会为该条记录创建一个读视图:
因为我们需要查看该条记录
- 所以我们需要从最新的数据开始遍历版本链,直到我们能看见这个版本为止
比较步骤
首先要判断,事务2能否看见事务4修改后的数据
- DB_TRX_ID=4
在我执行快照读时,它是否早在[我所能看见的执行中的最早事务]前就已经开启了 -- DB_TRX_ID < up_limit_id ?
- 因为4>1,所以不是
- 它比除我之外的这些执行中的事务要晚开启,是否仍在执行不清楚
在我执行快照读时,它有没有被启动 -- DB_TRX_ID < low_limit_id ?
- 因为4小于5,所以有,并且它的事务id>我的事务id
- 所以它是在我启动之后,进行快照读之前启动的
它是否在我执行快照读时还处于活跃状态(仍在执行) -- DB_TRX_ID 属于 活跃列表 ?
- 因为2不在列表中,所以不是活跃状态
- 也就是说,它在我执行快照读时已经提交了
所以,既然在快照前就已经提交了,那么当前事务可以看到事务4的更改
RC和RR的本质区别
引入
回头看看这个操作过程,事务4在提交后就能被同时执行的事务2看见
- 这不就是我们RC隔离级别下的现象吗
实验
设置为RR模式
- 如果使用快照读,没什么问题
- 如果使用当前读,就可以读出事务1更新后的结果
- 注意,这里我们的快照读操作是在事务a更新前进行的
再来一次测试
- 区别仅在于第二次的快照读是在更新后进行的
- 这次得到的结果是最新的数据
总结
我们仔细思考一下,其实第二次测试的结果也是符合可重复读的特性的
- 因为在我们事务2中,并不知道有没有事务对数据做修改
- 我们能看到的是,在进行第一次查询后,所有查询到的结果都是相同的
- 这不就是可重复读吗?
- 第二次测试,和我们前面介绍的[可见性判断过程]是一样的 -- 在进行快照读前提交,我们是应该看见的,和什么时候开启事务无关
测试1的重点在于:
- 创建读视图时,它将事务a纳入了活跃事务列表中
- 所以不应该看见它更新的数据
而测试2:
- 是在事务a结束后才创建读视图,所以活跃事务列表中并没有事务1
- 那么就应该看见更新后的结果
所以,形成读视图的时机会影响快照读的结果
- 也就是事务的可见性
介绍
在RR级别下:
- 某事务首次使用快照读时,会创建一个读视图,将当前时刻的事务状态记录下来
- 自此之后,该事务使用快照读时,使用的都是同一个读视图,内容是不变的
- 所以该事务的可见性不变
- 那么之后做的任何修改,该事务都看不见了;早于这之前的修改是可见的
而在RC级别下:
- 每次快照读都会生成一个新的读视图,所以它的可见性一直在变化
- 时间一直在向后走,所以每次生成的读视图都是比较新的,它就总是能看见历史提交
- 比如,这次的读视图认为事务a在和它并发执行,看不见 ; 到了下一个时刻,事务a已经提交了,于是就能看见了
- 这就是为什么在RC级别下,事务总是可以看见其他事务提交的更新
- 所以,RC才会有不可重复读的问题
总结
正是因为有了MVCC,我们才能理解为什么可以实现读写并发,为什么隔离性可以让我们看到不同的数据
- 隔离性,回滚本质都是用MVCC实现的