MVCC 当前读 快照读 RC read view RR下事务更新不会丢失

MVCC是多版本并发控制,常用于数据库系统,如MySQL的InnoDB引擎,以提高并发读写的性能。它通过记录的隐式字段、undo日志和ReadView来实现。ReadView是事务进行快照读时生成的,用于判断事务能否看到某个版本的数据。MVCC在READCOMMITTED和REPEATABLEREAD隔离级别下工作,解决了读-写冲突,但不能解决更新丢失问题。不同隔离级别下,ReadView的生成时机不同,影响了可见性规则。MVCC在RR级别下能防止部分幻读,但在RC级别下每次快照读都会生成新的ReadView,可能导致不可重复读。


MVCC(multi-version-concurrent-control)

MVCC是行锁的一个变种,但MVCC在很多情况下它避免了加锁。不是buffer块,而是buffer中的记录行。

MVCC (Multi-Version Concurrency Control) (注:与MVCC相对的,是基于锁的并发控制,Lock-Based Concurrency Control)是一种基于多版本的并发控制协议,只有在InnoDB引擎下存在。

MVCC是为了实现事务的隔离性,通过版本号,避免同一数据在不同事务间的竞争,你可以把它当成基于多版本号的一种乐观锁。当然,这种乐观锁只在事务级别提交读和可重复读有效。(当前读不用mvcc机制,实用锁机制)MVCC最大的好处,相信也是耳熟能详:读不加锁,读写不冲突。在读多写少的OLTP应用中,读写不冲突是非常重要的,极大的增加了系统的并发性能。
 

不仅是MySQL,包括Oracle,PostgreSQL等其他数据库系统也都实现了MVCC,但各自的实现机制不尽相同,因为MVCC没有一个统一的实现标准。

可以认为MVCC是行级锁的一个变种,但是它在很多情况下避免了加锁操作,因此开销更低。虽然实现机制有所不同,但大都实现了非阻塞的读操作,写操作也只锁定必要的行。

MVCC的实现方式有多种,典型的有乐观(optimistic)并发控制 和 悲观(pessimistic)并发控制。

MVCC只在 READ COMMITTED 和 REPEATABLE READ 两个隔离级别下工作。其他两个隔离级别和MVCC不兼容,因为 READ UNCOMMITTED 总是读取最新的数据行,而不是符合当前事务版本的数据行。而 SERIALIZABLE 则会对所有读取的行都加锁。
 

MVCC即多版本并发控制,MVCC是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。

MVCC在MySQL InnoDB中的实现主要是为了提高数据库的并发性能,用更好的方式去处理读-写冲突,做到 即使有读写冲突时,也能做到不加锁,非阻塞并发读 。

什么是当前读和快照读
当前读
就像 select lock in share mode(共享锁),select for update;update,insert,delete(排他锁);这些操作都是一种当前读,为什么叫当前读?因为它读取的记录都是目前数据库中最新的版本,读取时还要保证其它并发事务不能修改当前记录不是buffer级别,记录级别的),所以会对读取数据加锁。

快照读
像不加锁的select操作就是快照读,即不加锁的非阻塞读,快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读。(串行都是当前读:最新buffer,无事务(不是buffer级别,记录级别的),加锁)

之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制(MVCC)。

所以我们可以认为MVCC是行锁的一个变种,但MVCC在很多情况下它避免了加锁,降低了开销,既然是基于多版本的,所以快照读不一定读到的就是最新版本的记录,而是可能为之前的历史版本。

当前读,快照读和MVCC的关系
准确的说,MVCC多版本并发控制是指:“维持一个数据的多个版本,使得读写操作没有冲突”这么一个概念,听起来特别像我们JAVA中的那个写时复制,但这只是一个理想概念。
而在MySQL中,实现这么一个MVCC理想概念,我们就需要MySQL提供具体的功能去实现它,而快照读就是MySQL为我们实现MVCC理想模型的其中一个具体非阻塞读功能不同的快照,可以看作不同的数据版本。而相对而言,当前读就是悲观锁的具体功能实现。
要说得再细致一点,快照读本身也是一个抽象概念,再深入研究。MVCC模型在MySQL中的具体实现则是由四个隐式字段,undo日志,read view 等去完成的。
MVCC能解决什么问题?好处是什么?
数据库并发场景?
当前假设有三种,分别为:

读-读:不存在任何问题,也不需要并发控制。
读-写:有线程安全问题,会体现事务隔离性问题,也就是可能遇到,脏读,不可重复读,幻读等。
写-写:有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失,也会造成一些事务隔离性问题的出现。


MVCC带来的好处是?
**多版本并发控制(MVCC)**是一种用来解决 读-写 冲突的无所并发控制,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,也就是每个事务都有一个对应版本的快照,快照版本按照单向增长的时间戳来决定先后顺序。

在这样的情况下,读操作,我们只读该事务开始前的数据库快照,并不去读取正在修改的数据,我们读取事务开始前的最新版本。

所以解决了数据库在并发读取时的问题,即可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能,同时还可以解决脏读,不可重复读,幻读等事务隔离级别带来的问题。但不能解决更新丢失问题。

小结一下
总之,MVCC就是因为大牛们,不满意只让数据库采用悲观锁这些性能不佳的形式去解决读-写冲突问题,而提出的解决方案,所以在数据库中,因为有了MVCC,所以我们可以形成两个组合:

MVCC + 悲观锁 MVCC解决读写冲突,悲观锁解决写-写冲突。
MVCC + 乐观锁 MVCC解决读写冲突,乐观锁解决写-写冲突。(Tidb之类new sql )


MVCC的实现原理
MVCC的目的就是多版本的并发控制,在数据库中的实现,就是为了解决读-写冲突的问题,它的实现原理主要是依赖记录中的 3个隐式字段、undo日志、read view 来实现的。

隐式字段
每行记录除了我们自定义的字段外,还有数据库隐式定义的DB_TRXID, DB_ROLL, DB_ROW_ID等字段。

DB_ROW_ID:
6byte,隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID生成一个聚簇索引。
DB_TRX_ID:
6byte,最近修改(修改、插入)事务ID:记录创建这条记录以及最后一次修改该记录的事务的ID,是一个指针。
DB_ROLL_PTR:
7byte,回滚指针,指向这条记录的上一个版本(上一个版本存储于,rollback segment里)。
DELETED_BIT:
1byte,记录被更新或删除并不代表真的删除,而是删除flag变了,相当于记录一次逻辑删除。

在这里插入图片描述
就拿上图来解释这几个字段,DB_ROW_ID 是数据库默认为该行记录生成的唯一隐式主键;DB_TRX_ID 是当前操作该条记录的事务的ID;DB_ROLL_PTR 是一个回滚指针,用于配合 undo日志,指向该条记录的上一个版本;DELETED_FLAG 字段没有展示出来。(不是放在页头的?

UNDO日志
InnoDB把这些为了回滚而记录的这些东西称之为 undo log。

值得注意的是,由于查询操作(SELECT)并不会修改任何用户记录,所以在查询操作时,并不需要记录相应的 undo log。

undo log 主要分为以下三种:

insert undo log:
插入一条记录时,至少把这条记录的主键记录下来,之后回滚的时候只需要把主键对应的记录删除即可。
update undo log:
修改一条记录时,至少要把修改这条记录前的旧值都记录下来,在回滚的时候再把这条记录的值更新为旧值就好了。
delete undo log:
删除一条记录时,至少要把这条记录中的全部内容都记录下来,这样在之后回滚的时候再重新将这些内容组成的记录插入到表中就好了。
删除操作都只是设置一下老记录的 DELETE_BIT,并不是真正将其删除,类似于数据库提供的专门的逻辑删除。

-----undo log中存放的一个是sql语句,还有改变的记录变化。到时undo时直接用undo中的一段记录替换buffer中的一段记录。不会真正去执行sql 进行undo


为了节省磁盘空间,InnoDB有专门的 purge(清除)线程来清理 DELETED_BIT 为 true 的记录。
为了不影响MVCC的正常工作,purge线程自己也维护了一个 read view(这个 read view 相当于当前系统中最老活跃的事务的 read view)。
如果某个记录的 DELETED_BIT 为 true,并且 DB_TRX_ID(最后一个操作的事务ID) 相对于 purge线程的 read view 可见,那么这条记录一定是可以被安全清除的。
对 MVCC 有实质上帮助的是 update undo log,undo log 实际上就是存在于 rollback segment 中的旧纪录链。 (buffer中的一段代码 链)

说了这么多,云里雾里的,我们来看一个例子:

比如一个事务往 persion表 中插入了一条新纪录,记录如下,name = jerry,age = 24;

在这里插入图片描述

隐式主键 = 1,事务ID和回滚指针都假设为 NULL;


现在来了另一个事务1对该记录的 name 做出了修改,改为 tom;

在该 事务1 修改该行记录数据的同时,数据库会先对该行加排他锁(InnoDB引擎会自动对DML语言影响的记录上写锁|独占锁)。
上锁完毕后,将该行数据拷贝到 undo log 中,作为旧记录,即在 undo log 中有当前行的拷贝副本。
拷贝完毕后,修改该行的 name 为 tom,并且修改隐藏字段的 事务ID 为当前 事务1的ID,这里我们默认是从1开始递增,回滚指针指向拷贝到 undo log 的副本记录,即表示我的上一个版本就是他。
事务提交后,释放锁。

在这里插入图片描述


又来了一个事务2修改persion表的同一个记录,将 age 修改为 30岁;

在事务2修改该行数据之前,数据库继续给他上排他锁。
上锁完毕之后,把该行数据拷贝到 undo log 中,作为旧记录,发现操作的这行记录已经有undo log 的记录了,那么最新的旧数据作为链表的表头,插在这行记录的 undo log 日志的最前面。
修改该行age为30岁,并且修改隐藏字段的事务ID为当前事务2的ID,那就是2,回滚指针指向刚刚拷贝到 undo log 的副本记录。
事务提交,释放锁。

----图中事务id也是很复杂的, 

在这里插入图片描述


从上面几个例子可以看出,不同事物或者相同事务对同一个记录的修改,会导致该记录的 undo log 成为一条版本记录链。undo log 的链首就是最新的旧记录,尾部就是最旧的记录(当然,就像之前所说的该 undo log 的节点可能是会被 purge线程 清除掉的,像图中的第一条 insert undo log, 其实在事务提交之后可能就被删除丢失了,不过这里为了演示所以还放在这里,假设没被清除)。

Oracle 没有隐藏字段是通过ITL表中 以及undo segment header中找到的 , PG是直接添加一个元组和时间,类似于KV数据库多版本。

Read View(读视图)
什么是 Read View?说白了 Read View 就是==事务进行快照读操作的时候生产的读视图==,在当前事务执行快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID,这个ID是默认递增的,所以事务越新,ID越大)。(读本身不是事务,不产生事务ID,读取的是系统那一刻的ID)

所以我们可以知道 Read View 主要是用来做==可见性判断==的,即当我们某个事物执行快照读的时候,对读取的该记录创建一个 Read View 视图,把它当作条件,用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据(也就是该快照),也可能是该行记录的 undo log 日志里的某个版本的数据。

Read View 遵循一个可见性算法:

事务ID查询就不会新增,只有DML语言才会导致事务ID增加。

主要是将被修改的数据的最新记录中的 DB_TRX_ID(当前事务ID)取出来,与系统当前其它活跃事务的ID去对比(由 Read View 维护),如果 DB_TRX_ID 跟 Read View 的属性做了某些比较之后不符合可见性,那就通过 DB_ROLL_PRT 回滚指针去取出 undo log 中的 DB_TRX_ID 再比较,也就是说遍历 undo log 链表的 DB_TRX_ID 找到特定条件的事务ID的版本,那么这个 DB_TRX_ID 所在的旧记录就是当前事务能看见的最新老版本。(找不到的时候就是快照过旧。。。。。。)

那么这个判断条件是什么呢?

如上,他是一段 MySQL 判断可见性的一段源码。即 changes_visible 方法(不完全,但是能看出大致逻辑),该方法展示了我们拿 DB_TRX_ID 去跟 Read View 某些属性进行怎么样的比较。

在介绍前,我们先简化一下 Read View ,我们可以把 Read View 简单的理解成有三个全局属性:

trx_list:未提交事务 ID 列表,用来维护 Read View 生成时刻系统正处于活跃状态的事务ID。
up_limit_id:记录 trx_list 事务ID列表中 最小的ID,也就是最初修改该记录的事务。
low_limit_id:Read View 生成时刻系统尚未分配的下一个事务ID,也就是等于**目前出现过的最大事务ID + 1**。

在这里插入图片描述
方法大致流程(对比上面代码):

首先判断 DB_TRX_ID < up_limit_id :

大于:进入下一个判断。
小于:则当前事务能看到 DB_TRX_ID 所在记录。
判断 DB_TRX_ID >= low_limit_id:

大于:代表 DB_TRX_ID 所在的记录是在 Read View 生成之后才出现的,那对当前事务肯定不可见。
小于:进入下一个判断。
判断 DB_TRX_ID 是否在活跃事务中 trx_list.contains(DB_TRX_ID):

在:代表 Read View 生成的时候,你这个事务还在活跃状态,并没有 commit,你修改的数据,我当前的事务是看不见的(RR隔离级别)。
不在:说明你这个事务在 Read View 生成之前就已经 commit 了,你修改的结果,我当前事务是看得见的。
可以这样理解 Read View :不应该让当前事务看到的记录版本,这些记录版本对应的事务ID都在Read View 中。

以 Repeatable Read (RR隔离级别)举个例子吧,要求读一个值,一直读都是同一个值:

这种隔离级别下,开启事务的时候开启一个 Read View ,在当前事务执行的整个过程中都用这个 Read View。(不用显式开启事务)


当前 事务ID = 10,ReadView 就是(4,8, 10),因为当前事务10正在执行,所以自己也活跃,此时 up_limit_id=4,low_limit_id=11。
如果 **当前事务10 **读到一个数据的 事务ID = 1,小于 活跃列表的最小值(up_limit_id=4),可见。
为什么?
因为在 事务10 开启的时候生成的 Read View ,除了4,8,10,其他事务都已经提交了(不处于活跃状态了),所以事务1的版本 < 事务4的版本,以及5、6、7、9,都是肯定在我开启的时候已经提交了(事务ID单调递增)。
所以这些版本的的数据,再怎么读都不会变,可以放心的读。
但如果我读到一个数据的 事务ID = 12,说明他在我创建 Read View 之后提交的,我不应该看见这个值,应该去 undo log 里找这个数据的前面的版本,如果找到 事务ID < 4的版本,或者 事务ID = (5、6、7、9)的版本 都是安全的,可以读。
如果我读到一个数据 事务ID在活跃列表的范围内:
如果当前事务就是活跃的事务之一,比如说是8,说明这个数据在我开启事务之后,才被其它活跃事务更改(提交或未提交),那么这个我不能看见,应该去 undo log 中找上一个版本来读,假设说是 7,7也是在这个活跃范围里,但是并不是活跃事务之一,这个版本是在当前事务开启事务之前由事务7提交的,所以这个版本可见。


再举个读已提交的例子:

在这个隔离级别是每次读都采用新的 Read View。
事务10 开启。
读一个数据,事务ID = 9,假设此时Read View 中 活跃事务ID =(4,8,10),按照规则,可见。
过一会再读这个数据,发现此时 事务ID = 11,而此时活跃事务ID =(4,8,10),但是因为开启了新的 Read View( low_limit_id:Read View 生成时刻系统尚未分配的下一个事务ID,也就是等于**目前出现过的最大事务ID + 1**。),当前系统最大事务ID >11(因为我们已经读到11了嘛),根据判断规则,事务11不在活跃ID列表并且 (事务11ID = 11) < (low_limit_id = 12)(RC这次需要新生成read view 所以low_limit_id 变大了,不同于RR),所以可见。这回就读到了这个数据的新版本了。

同一个session 两次读的数据是不一样的

----Read view

Read View 可读视图

组成 [未提交事务数组],最大的事务id

可读视图判断规则

生成一致性视图规则read-view

由执行时所有未提交事务的id数组(数组里最小的trx_id为min_id)和已创建的事务id(max_id)+1 组成(应该分配给下一个事务的id),查询的数据记录与生成 的read-view做对比得到结果

 

版本链比对判断规则

  • 如果落在绿色部分(trx_id<min_id) ,表示这个版本是已提交事务生成的,这个事务是可见的

  • 如果落在黄色部分(min_id<=trx_id<=max_id),分两种情况处理

    a. 若row的trx_id在数组中,表示这个事务是由还没提交的事务生成的,不可见,当前自己的事务是可见的

    b. 若row的trx_id不在数组中,表示这个事务是已经提交的事务生成的,可见(这种情况主要是查询undo时看undo中记录的trx_id吧)

  • 如果落在红色部分(trx_id>max_id), 表示这个版本是由将来启动的事务创建的 ,肯定不可见

对于删除的情况可以认为是update的特殊情况,delete数据的时候会将版本上最新的数据复制一份,然后将trx_id修改成删除操作的trx_id,同时将该条记录的头信息(record header 这个头不在三个隐藏字段列 )里的(deleted_flag)标记位写上true,表示当前记录已经被删除,在查询时按照上面的规则查到对应的记录,如果deleted_flag标记位为true,意味着数据已经被删除,则不会被返回。

 

--------------------------------------- MVCC 都是基于快照的

Read View和快照Snapshot
事务快照是用来存储数据库的事务运行情况。一个事务快照的创建过程可以概括为:

查看当前所有的未提交并活跃的事务,存储在数组中
选取未提交并活跃的事务中最小的XID,记录在快照的xmin中
选取所有已提交事务中最大的XID,加1后记录在xmax中
Read View (主要是用来做可见性判断的):创建一个新事务时,copy一份当前系统中的活跃事务列表。意思是,当前不应该被本事务看到的其他事务id列表。

对于Read View快照的生成时机,也非常关键,正是因为生成时机的不同,造成了RC,RR两种隔离级别的不同可见性;

在innodb中(默认repeatable read级别),事务在begin/start transaction之后的第一条select读操作后,会创建一个快照(Read View),将当前系统中活跃的其他事务记录记录起来
在innodb中(read committed级别),事务中每条select语句都会创建一个快照(Read View)
 

Read View和快照Snapshot
事务快照是用来存储数据库的事务运行情况。一个事务快照的创建过程可以概括为:

查看当前所有的未提交并活跃的事务,存储在数组中
选取未提交并活跃的事务中最小的XID,记录在快照的xmin中
选取所有已提交事务中最大的XID,加1后记录在xmax中
Read View (主要是用来做可见性判断的):创建一个新事务时,copy一份当前系统中的活跃事务列表。意思是,当前不应该被本事务看到的其他事务id列表。

对于Read View快照的生成时机,也非常关键,正是因为生成时机的不同,造成了RC,RR两种隔离级别的不同可见性;

在innodb中(默认repeatable read级别),事务在begin/start transaction之后的第一条select读操作后,会创建一个快照(Read View),将当前系统中活跃的其他事务记录记录起来
在innodb中(read committed级别),事务中每条select语句都会创建一个快照(Read View)
RC是语句级多版本(事务的多条只读语句,创建不同的ReadView,代价更高),RR是事务级多版本(一个ReadView);

read committed 总是读最新一份快照数据,而repeatable read 读事务开始时的行数据版本。
 
read Commited隔离级别判断算法在每次语句执行的过程中,都关闭read_view, 重新创建当前的一份新的read_view。这样导致了low_limit_id变大,原来不可见的现在可以看到了。
 
read view中事务id  T_min~T_max,当前事务T1。
...执行sql,创建一份最新的read_view;
...T1<T_min,说明T1事务比较早,该行对当前事务T1可见。
...T1 > T_max,说明T1比较晚,该行对当前事务不可见,根据DB_ROLL_PTR找到上一个判断再次判断。
...T_min <= T1 <= T_max,如果read_view中有该事务,则不可见,找上一个版本。如果不在则可见(在read commited下)。
 
repeatable read各级离别下判断算法:创建事务trx结构的时候,就生成了当前的global read view。
...trx_id_1< trx_id_min那么表明该行记录所在的事务已经在本次新事务创建之前就提交了,所以该行记录的当前值是可见的。
...trx_id_1>trx_id_max的话,那么表明该行记录所在的事务在本次新事务创建之后才开启,所以该行记录的当前值不可见。通过DB_ROLL_PTR找到上一版数据判断
...trx_id_min<=trx_id_<=trx_id_max, 那么表明该行记录所在事务在本次新事务创建的时候处于活动状态,从trx_id_min到trx_id_max进行遍历,如果trx_id_1等于他们之中的某个事务id的话,那么不可见。通过DB_ROLL_PTR找到上一版数据判断`
实验1:

session A

session B

mysql> set tx_isolation='repeatable-read';

mysql> set tx_isolation='repeatable-read';

mysql> select * from t1;

Empty set (0.01 sec)

mysql> start transaction;

mysql> insert into t1(c1,c2) values(1,1);

mysql> select * from t1;

+----+------+

| c1 | c2   |

+----+------+

|  1 |    1 |

+----+------+

1 row in set (0.00 sec)

实验2:

mysql> set tx_isolation='repeatable-read';

mysql> set tx_isolation='repeatable-read';

mysql> select * from t1;

Empty set (0.01 sec)

mysql> start transaction with consistent snapshot;

mysql> insert into t1(c1,c2) values(1,1);

mysql> select * from t1;

Empty set (0.00 sec)

上面两个实验很好的说明了 start transaction 和 start tansaction with consistent snapshot的区别。第一个实验说明,start transaction执行之后,事务并没有开始,所以insert发生在session A的事务开始之前,所以可以读到session B插入的值。第二个实验说明,start transaction with consistent snapshot已经开始了事务,所以insert语句发生在事务开始之后,所以读不到insert的数据。

所以事务开始时间点,分为两种情况:

START TRANSACTION 时,是第一条语句的执行时间点,就是事务开始的时间点,第一条select语句建立一致性读的snapshot;
START TRANSACTION  WITH consistent snapshot 时,则是立即建立本事务的一致性读snapshot,当然也开始事务了;
参考:MySQL 一致性读 深入研究 - digdeep - 博客园

一致性读肯定是读取在某个时间点已经提交了的数据,有个特例:本事务中修改的数据(同一sesion),即使未提交的数据也可以在本事务的后面部分读取到。

MVCC下的CRUD
SELECT:
  当隔离级别是REPEATABLE READ时select操作,InnoDB必须每行数据来保证它符合两个条件:

InnoDB必须找到一个行的版本,它至少要和事务的版本一样老(也即它的版本号不大于事务的版本号)。这保证了不管是事务开始之前,或者事务创建时,或者修改了这行数据的时候,这行数据是存在的。
这行数据的删除版本必须是未定义的或者比事务版本要大。这可以保证在事务开始之前这行数据没有被删除。
符合这两个条件的行可能会被当作查询结果而返回。

INSERT:InnoDB为这个新行记录当前的系统版本号。
DELETE:InnoDB将当前的系统版本号设置为这一行的删除ID。
UPDATE:InnoDB会写一个这行数据的新拷贝,这个拷贝的版本为当前的系统版本号。它同时也会将这个版本号写到旧行的删除版本里。

这种额外的记录所带来的结果就是对于大多数查询来说根本就不需要获得一个锁。只是简单地以最快的速度来读取数据,确保只选择符合条件的行。这个方案的缺点在于存储引擎必须为每一行存储更多的数据,做更多的检查工作,处理更多的善后操作。(多了三列)

MVCC只工作在REPEATABLE READ和READ COMMITED隔离级别下。READ UNCOMMITED不是MVCC兼容的,因为查询不能找到适合他们事务版本的行版本;它们每次都只能读到最新的版本。SERIABLABLE也不与MVCC兼容,因为读操作会锁定他们返回的每一行数据。

当前读和快照读
MySQL的InnoDB存储引擎默认事务隔离级别是RR(可重复读), 是通过 “行排他锁+MVCC” 一起实现的,不仅可以保证可重复读,还可以部分防止幻读,而非完全防止;

为什么是部分防止幻读,而不是完全防止?

效果: 在如果事务B在事务A执行中,insert了一条数据并提交,事务A再次查询,虽然读取的是undo中的旧版本数据(防止了部分幻读),但是事务A中执行update或者delete都是可以成功的。(参考:MySQL 读提交和重复读隔离级别实验 实验三)

因为在innodb中的操作可以分为当前读(current read)和快照读(snapshot read):

快照读:读取的是快照版本,也就是历史版本
简单的select操作(当然不包括 select … lock in share mode, select … for update)

当前读:读取的是最新版本
UPDATE、DELETE、INSERT、SELECT …  LOCK IN SHARE MODE、SELECT … FOR UPDATE是当前读。

在RR级别下,快照读是通过MVCC(多版本控制)和undo log来实现的,当前读是通过加record lock(记录锁)和gap lock(间隙锁)来实现的。

RR读可能数据不对,但是修改的话是不会丢失别的回话做出的修改的。
 

---------------------------------------------------------------------


整体流程
说了这么多,我们在了解了 隐式字段、undo log、Read View 的概念之后。就可以来看看MVCC 的具体实现流程大致是什么样的了。

我们可以模拟一下:

当 事务2 对某行数据执行了快照读,数据库为该行数据生成一个 Read View (读视图),假设当前事务ID为2,此时还有事务1和事务3在活跃状态中,事务4在事务2快照读前一刻提交了更新,所以 Read View 记录了系统当前活跃事务1,3的ID,维护在一个列表上,假设我们称这个列表为 trx_list:


…    …    …    
ReadView 不仅仅会通过一个列表 trx_list 来维护事务2执行快照读那刻系统中正在活跃的事务ID,还会有两个属性 up_limit_id,low_limit_id;所以在这里的例子中,up_limit_id = 1, low_limit_id = 4+1 = 5,trx_list集合的值是1,3,Read View 如下图。

 在这里插入图片描述

我们的例子中,只有事务4修改过该行记录,并在事务2 执行快照读前,就提交了事务。

所以,当前该行数据的 undo log 就如下图所示。

在这里插入图片描述
我们的事务2,在快照读该行记录的时候,就会拿该行记录的 DB_TRX_ID 去和 up_limit_id,low_limit_id 和 trx_list(活跃事务ID列表)进行比较,判断当前事务2能看到的记录是哪个版本。

在这里插入图片描述

MVCC相关的问题
RR是如何在RC级的基础上解决不可重复读的?
当前读和快照读在 RR级别 下的区别:

 
在表第二部分中,为什么事务B在事务A的提交后,快照读和当前读都是400呢?

这里与第一个表的唯一区别仅仅是表一的事务A修改金额前快照读过一次金额数据,而表二的事务B在事务A提交前并没进行过快照读。

所以我们知道,事务中快照读的结果非常依赖事务首次出现快照读的地方,即某个事务中首次出现快照读的地方十分的关键,它可以决定该事务后续快照读结果的能力。

我们这里测试的是更新,同时删除和更新也是一样的,如果事务B的快照读是在事务A操作之后进行的,事务B的快照读也是能读取到最新的数据的。

-------是不是RR读会导致数据别的事务数据更新丢失呢

如上图,虽然快照读的数据不一样,但是当前读的数据都是别的事务更新后的记录,不存在RR和RC模式下导致数据更新丢失。

RC,RR级别下的InnoDB快照读有什么不同?
正式因为 Read View 的生成时间不同。

在 RR 级别下的某个事务对某条记录的第一次快照读会创建一个快照以及 Read View,记录当前系统中活跃的其它事务,此后在调用快照读的时候,还是用的同一个 Read View,所以只要当前事务在其它事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个 Read View,所以对之后的修改不可见。
即 RR 级别下,快照读生成 Read View 时,Read View 会记录所有当前其它所有活跃事务的快照,这些事务的修改对于当前事务都是不可见的,而早于 Read View 创建的事务所作的修改均可见。
在 RC 级别下,事务中,每次快照都都会生成一个新的 Read View 和最新快照,这就是我们在 RC级别下的事务中可以看到别的事务提交更新的原因。
反正总而言之就是 RC 隔离级别 下,每个快照读都会生成新的 Read View 以及快照,而在 RR隔离级 别下,则是同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View。 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值