目录
5.5.2 RC、RR级别下的InnoDB快照读有什么不同?
1、什么是MVCC
MVCC,全称是 Multi-Version Concurrency Control,多版本并发控制。MVCC是一种并发控制的方法,一般情况下,在数据库管理系统中实现对数据库的并发访问。【MVCC解释-来自百度】
MVCC 在MySQL InnoDB 存储引擎中的实现,主要是为了提高数据库的并发性能,用更好的方式来处理【读-写冲突】:即 在并发读-写数据库时,可以做到在读操作时不用阻塞写操作,在写操作时也不用阻塞读操作,提高了数据库并发读写的性能。
2、什么是InnoDB存储引擎下的当前读和快照读
(1)当前读:像 【 select ... lock in share mode (共享锁)、select ... for update (排它锁)、update / insert / delete (排它锁) 】这些SQL操作都是一种当前读。为什么叫当前读?就是因为它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。当前读,实际上是一种加锁的操作,是悲观锁的实现。
(2)快照读:像【 不显示加锁的 select ... 操作 】就是快照读,即不加锁的非阻塞读。快照读的前提 是事务的隔离级别不是串行隔离级别,串行隔离级别下的快照读会退化成当前读。之所以会出现快照读,是基于提高数据库并发性能的考虑,快照读的实现是基于多版本并发控制 即 MVCC,可以认为 MVCC 是行锁的一个变种,但是,它在很多情况下 避免了加锁操作,降低了开销。既然是基于多版本并发控制,那么 快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。说白了,快照读就是MVCC思想在MySQL中的具体的非阻塞读功能的实现,整个MVCC多版本并发控制的目的 就是为了实现【读-写冲突不加锁】,提高数据库的并发读写性能,而这个读指的就是快照读。
3、当前读、快照读与MVCC的关系
(1)准确的说,MVCC 多版本并发控制指的是:“ 维持一个数据的多个版本,使得【读-写操作】没有冲突 ” 的这么一个概念。仅仅是一个概念而已。
(2)而在MySQL中,要实现这么一个MVCC的概念,就需要MySQL提供具体的逻辑功能去实现它,而 快照读 就是MySQL为我们实现MVCC概念模型的一个具体的非阻塞读功能的实现。当前读 则是MySQL中悲观锁的一个具体功能实现。
(3)MVCC模型在MySQL中的具体实现则是由【 3个隐式字段、undo 回滚日志 、ReadView 读视图】去完成的,具体可以看下面的MVCC实现原理。
4、MVCC能够解决什么问题
4.1 数据库中的三种并发场景
在数据库中,并发场景主要有三种,分别是:
(1)读-读 并发:不存在任何问题,也不需要并发控制。
(2)读-写 并发:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读、幻读、不可重复读 问题。
(3)写-写 并发:有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失、第二类更新丢失。
在MySQL中,如果没有做好并发控制,就有可能会出现上述问题。
4.2 MVCC能够解决的问题:读-写冲突
4.2.1 悲观并发控制、乐观并发控制 与MVCC的关系
(1)悲观并发控制(PCC),是一种用来解决【读-写冲突】和【写-写冲突】的加锁并发控制,为每个操作都加锁,同一时间下,只有获得该锁的事务才能有权利对该数据进行操作,没有获得锁的事务只能等待其他事务释放锁。所以,可以解决 脏读、幻读、不可重复读、第一类更新丢失、第二类更新丢失的问题。
(2)乐观并发控制(OCC),是一种用来解决【写-写冲突】的无锁并发控制,它认为事务之间的数据争用没有那么多,所以先进行修改,在提交事务前,检查一下事务开始后,有没有数据修改被提交,如果没有就提交,如果有就放弃并重试。乐观并发控制类似于自旋锁。乐观并发控制适用于低数据争用、写冲突比较少的场景。无法解决脏读、幻读、不可重复读,但是可以解决更新丢失类问题。
(3)多版本并发控制(MVCC),是一种用来解决【读-写冲突】的无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改都保存一个版本,版本与事务的时间戳相关联,读操作只读该事务开始前的数据库的快照。 这样,在读操作时就不用阻塞写操作,写操作也不用阻塞读操作。不仅可以提高并发性能,还可以解决脏读、幻读、不可重复读等事务问题,更新丢失类问题除外。
4.2.2 MVCC能够解决什么问题
(1)MVCC 的含义:MVCC,也就是 多版本并发控制,是一种用来 解决【读-写冲突】的无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务的时间戳相关联,读操作只能够读取该事务开始前的数据库的快照。
(2)所以,MVCC可以为数据库解决以下问题:
(2.1)在并发读-写数据库时,可以做到在读操作时不用阻塞写操作,在写操作时也不用阻塞读操作,提高了数据库并发读写的性能。
(2.2)同时,MVCC 还可以解决 脏读、不可重复读、幻读 等事务隔离问题,但不能解决更新丢失类问题。
(3)总的来说:MVCC的出现就是 数据库不满 用悲观锁去解决读-写冲突问题 因性能不高而提出的一种解决方案,所以在数据库中,我们可以形成两个组合:
(1)MVCC + 悲观锁:MVCC解决 读-写冲突,悲观锁解决 写-写冲突。
(2)MVCC + 乐观锁:MVCC解决 读-写冲突,乐观锁解决 写-写冲突。
这2种组合的方式,就可以最大程度的提高数据库并发性能,并解决【读-写冲突】和【写-写冲突】导致的问题。【参考:乐观锁和MVCC的关系】
5、MVCC的实现原理
MVCC的含义就是多版本并发控制,在数据库中的实现,其目的就是为了解决数据库中的【读-写冲突】问题
。它的实现原理主要是依赖记录中的【 三个隐式字段、undo 回滚日志 、ReadView 读视图】 来实现的。所以,我们逐一看看这个三个关键的概念。
5.1 三个隐式字段
在MySQL中,每行记录除了我们自己定义的字段之外,还有数据库隐式定义的一些字段:DB_TRX_ID、DB_ROLL_PTR、DB_ROW_ID 等字段。
(1)DB_TRX_ID:数据行事务版本号,大小为6byte,记录 最近修改事务ID(这里的修改包含:insert / update ),记录创建这条记录 / 最后一次修改这条记录的事务ID。
(2)DB_ROLL_PTR:回滚指针,大小为7byte,记录 回滚指针,指向这条记录的上一个版本(存储于 rollback segment 里)。
(3)DB_ROW_ID:行记录隐式ID,大小为6byte,每行记录都存在着一个隐含的自增主键ID(隐藏自增主键,并不是我们在表中创建的自增主键ID)。如果我们在创建数据表时没有创建主键ID,那么,InnoDB存储引擎会自动的以 DB_ROW_ID 产生一个聚簇索引来作为自增主键ID。
解释:如上图所示,DB_ROW_ID 是数据库默认为该行记录生成的唯一隐式主键,DB_TRX_ID 是当前操作该记录的事务ID,DB_ROLL_PTR 是一个回滚指针,用于配合undo日志,指向该条记录的上一个旧版本。
5.2 undo log 回滚日志
undo log,它是MySQL中的回滚日志,主要分为两种:
(1)insert undo log:代表事务在 insert 新记录时产生的 undo log,只在事务回滚时需要,并且在事务提交之后可以被立即丢弃。
(2)update undo log:代表事务在进行 update 或者 delete 时产生的 undo log,不仅在事务回滚时需要,在快照读时也需要,所以不能随便删除。
另外,对purge线程做一些解释:
从前面的分析可以看出,为了实现InnoDB的MVCC机制,更新操作 update 或者 删除操作 delete 都只是设置一下老记录的 deleted_bit 为 true 而已,并不是真正将过时的记录删除。
为了节省磁盘空间,InnoDB有专门的purge线程来清理 deleted_bit 为true的记录。
为了不影响MVCC的正常工作,purge线程自己也维护了一个 ReadView(这个ReadView相当于系统中最老活跃事务的ReadView):如果某个记录的 deleted_bit 为 true,并且 DB_TRX_ID相对于purge线程的ReadView可见,那么,这条记录就一定是可以被安全清除的。
5.2.1 undo log 生成举例
对MVCC有帮助的实质是 update undo log ,undo log 实际上就是存在 rollback segment 中的旧记录链,它生成的执行流程如下:
【一】比如,有一个事务在person表中插入了一条新记录,记录如下:name
为Jerry、age
为24岁,隐式主键
是1,事务ID
和回滚指针
,我们假设为NULL:
【二】 现在,来了一个事务1 对该记录的name做出了修改,改为Tom:
(1)首先,在事务1 修改该行记录时,数据库会先对该行加排他锁;
(2)然后,把该行数据拷贝到 undo log 中,作为旧记录,即在 undo log 中有当前行的拷贝副本;
(3)拷贝完毕后,修改该行name为Tom,并且修改隐藏字段的事务ID为当前事务的事务ID,为1,回滚指针 为指向拷贝到undo log的副本记录,即表示我的上一个版本就是它。
(4)事务1 提交后,释放该行的排他锁。
【三】 这时,又来了一个事务2,它修改person表的同一个记录,将age修改为30岁:
(1)首先,在事务2 修改该行数据时,数据库也先为该行加排它锁;
(2)然后,把该行数据拷贝到 undo log 中,作为旧记录,发现该行记录已经有 undo log 了,那么,最新的旧数据就作为链表的表头,插在该行记录的 undo log 最前面。
(3)修改该行age为30岁,并且修改隐藏字段的事务ID为当前事务的事务ID,为2,回滚指针 为指向刚刚拷贝到 undo log 的副本记录,即表示我的上一个版本就是它。
(4)事务2 提交之后,释放该行的排他锁。
从上面我们可以看出:不同事务或者相同事务的对同一条记录的修改,会导致该条记录的 undo log 成为一条记录版本的链表,undo log 的链首就是最新的旧记录,undo log 的链尾就是最早的旧记录。当然,就像之前说的该 undo log 的节点可能会被purge线程清除掉,像图中的第一条 insert undo log,其实在插入数据的事务提交之后可能就被删除了,不过这里为了演示,所以还放在这里。
5.3 ReadView 读视图
什么是 ReadView?说白了,ReadView 就是事务在进行快照读操作的时候生成的一个读视图(ReadView)。在该事务执行快照读的那一刻,会为当前的数据库系统生成一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID,这个ID是单向递增的,所以最新的事务,ID值越大)。
所以,我们知道 ReadView 主要是用来做数据可见性判断的。即 当某个事务执行快照读的时候,对该记录创建一个ReadView读视图,把它当作条件用来判断当前事务能够看到该条记录的哪个版本,既可能是”该条记录“当前最新的数据,也有可能是”该条记录“的 undo log 里面的某个旧版本的数据。
ReadView 遵循一个可见性算法,主要是将要被修改的数据的最新记录中的 DB_TRX_ID(当前事务ID)取出来,与系统当前其他活跃事务的ID列表去对比(由ReadView维护)。如果 DB_TRX_ID 跟 ReadView 的属性做了某些比较,不符合可见性,那就通过 DB_ROLL_PTR 回滚指针去取出 undo log 中上一个版本中的 DB_TRX_ID 再比较,即遍历版本链的 DB_TRX_ID( 从链尾到链首,即从最近的一次修改查起),直到找到满足特定条件的 DB_TRX_ID,那么,这个 DB_TRX_ID 所在的旧记录就是当前事务所能看见的最新的老版本。
那么这个判断条件是什么呢?
这里盗窃 【呵呵一笑百媚生】 的一张源码图,如上所示:它是MySQL判断可见性的一段源码,即changes_visible方法,该方法展示了拿DB_TRX_ID去跟ReadView某些属性进行怎样的比较。
在解释之前,先简化一下ReadView,可以把ReadView简单的理解成有三个全局属性:
(1)trx_list:一个ID列表,用来维护 ReadView 生成时刻系统正处于活跃状态的事务ID列表。
(2)up_limit_id:记录 trx_list 列表中事务ID最小的那个ID。
(3)low_limit_id:ReadView 生成时刻系统尚未分配的下一个事务ID,也就是目前已出现过的最大事务ID的值+1。
DB_TRX_ID (当前事务ID)跟 ReadView 这三个全局属性进行比较的流程是:
(步骤 1)首先,比较 DB_TRX_ID 是否小于 up_limit_id:如果小于,则当前事务仅能看到 DB_TRX_ID 所在的记录;如果大于等于,则进入下一个判断。
(步骤 2)接下来判断 DB_TRX_ID 是否大于等于 low_limit_id:如果大于等于,则代表 DB_TRX_ID 所在的记录在 ReadView 生成之后才出现,对当前事务肯定不可见;如果小于,则进入下一个判断。
(步骤 3)DB_TRX_ID 是否在活跃事务ID列表之中,trx_list.contains(DB_TRX_ID):如果在,则代表 我的ReadView 生成时刻,你这个事务还在活跃,还没有 commit,你修改的数据 我当前事务当然看不见;如果不在,则说明你这个事务在 ReadView 生成之前就已经 commit 了,你修改的结果我当前事务能看见。
5.4 MVCC的整体流程
在了解了 【 三个隐式字段、undo 回滚日志 、ReadView 读视图】的概念之后,就可以来看看MVCC实现的整体流程了。MVCC的整体流程是怎样的?可以模拟一下。
(1)当
事务2
对某行数据执行了快照读
,数据库为该行数据生成一个ReadView
读视图,假设当前事务的事务ID为2
。此时,还有事务1
和事务3 处
在活跃中,事务4
在事务2
快照读的前一刻提交更新了,所以 事务2 中 该数据行的ReadView读视图 记录了当前系统活跃的 事务1 和 事务3 的ID,维护在一个列表上,假设称为trx_list=[1,3]。
(2)ReadView 不仅仅会通过一个列表 trx_list 来记录 事务2 执行快照读的那一刻 系统正活跃的事务ID列表,还会有两个属性 up_limit_id(记录 trx_list 列表中事务ID最小的ID)和 low_limit_id(记录快照读那一刻 系统尚未分配的下一个事务ID,也就是目前系统中已经出现过的事务ID的最大值+1) 。所以,在例子中 up_limit_id=1、low_limit_id=4 + 1 = 5、trx_list=[ 1,3],ReadView 如下图:
(3)在例子中,只有 事务4 修改过该行记录,并在 事务2 执行快照读之前 就提交了事务,所以,当前该行当前数据的 undo log 如下图所示。
事务2 在快照读该行记录的时候,就会拿该行记录的 DB_TRX_ID 去跟 up_limit_id、low_limit_id、活跃事务ID列表(trx_list)进行比较,判断 当前事务2 能看到该记录的版本是哪个。
(4)所以,在该行记录的版本链中,首先拿该记录 DB_TRX_ID 字段记录的 事务ID 4 去跟 ReadView 的 up_limit_id 比较,看 4 是否小于 up_limit_id (1),不符合条件;所以继续判断 4 是否大于等于 low_limit_id (5),也不符合条件;所以最后判断 4 是否处于 trx_list 中的活跃事务ID列表中,发现 事务ID=4 的事务 也不在当前活跃事务ID列表中。所以,根据可见性条件,事务4 修改后提交的最新结果对 事务2 的快照读 可见,所以,事务2 能读到的最新数据记录是 事务4 所提交的版本,而 事务4 提交的版本也是全局角度上最新的版本。
也正是 ReadView 生成时机的不同,从而造成 RC、RR 隔离级别下 快照读的结果的不同。
5.5 MVCC的相关问题
5.5.1 RR是如何在RC的基础上解决不可重复读的?
当前读和快照读在 RR 隔离级别下的区别,如下的表1和表2 分别做演示。
解释:在表1的SQL顺序下,事务B 在 事务A 提交修改后的快照读是旧版本数据 500,而当前读是实时新数据 400。
解释:在表2的SQL顺序下,事务B 在事务A 提交后的快照读和当前读都是实时的新数据 400,这是为什么呢?
表2与表1的唯一区别:仅仅是表1的 事务B 在 事务A 修改金额前快照读过一次金额数据,而表2的 事务B 在 事务A 修改金额前没有进行过快照读。
所以,我们知道 事务中快照读的结果是非常依赖该事务首次出现快照读的位置的,即 某个事务中首次出现快照读的位置非常关键,它有能力决定该事务后续快照读的结果。
我们这里测试的是 更新update,同时存在 删除delete和更新update 也是一样的,如果 事务B 的快照读是在 事务A 的操作之后进行的,那么,事务B 的快照读也是能读取到最新的数据的。
5.5.2 RC、RR级别下的InnoDB快照读有什么不同?
正是 ReadView 生成时机的不同,从而造成 RC、RR 隔离级别下 快照读 结果的不同。
(1)在RR隔离级别下,某个事务中对某条记录的第一次快照读会创建一个快照以及ReadView,它会将当前系统中处于活跃的其他事务ID记录下来,此后,在调用快照读的时候,使用的仍然是同一个ReadView。所以,只要当前事务在其他事务提交更新之前使用过快照读,那么,当前事务中之后的快照读使用的都是同一个ReadView,所以,其他事务的之后的修改 对当前事务 均不可见。
即:RR隔离级别 解决了不可重复读问题。
(2)在RC隔离级别下,在某个事务中,每次快照读都会生成一个新的快照和ReadView,这就是我们在RC隔离级别下的事务中可以看到其他事务提交的更新的原因。
即:RC隔离级别 不能解决不可重复读问题。
总结:
在RC隔离级别下,每个快照读都会重新创建并获取最新的ReadView。
在RR隔离级别下,则是同一个事务中的第一个快照读才会创建ReadView,之后的快照读使用的都是同一个ReadView。
99、参考
(1)由浅入深全面理解MVCC:https://blog.csdn.net/cmm0401/article/details/115867006
(2)由浅入深全面分析乐观锁、悲观锁、MVCC:https://blog.csdn.net/cmm0401/article/details/115816459
(3)数据库事务:https://blog.csdn.net/cmm0401/article/details/115655095
(4)https://www.zhihu.com/question/66320138/answer/241418502
(5)http://m.imooc.com/article/details?article_id=17290
(6)https://www.cnblogs.com/nick-huang/p/6653996.html
(7)https://blog.csdn.net/SnailMann/article/details/88299127