【数据库】MySQL事务并发、MVCC原理及实现

数据库的事务是指一组sql语句组成的数据库逻辑处理单元,在这组的sql操作中,要么全部执行成功,要么全部执行失败。

#开启手动提交事务
set autocommit=0
#提交|回滚
commit|rollback;  
#回滚到特定位置 savepoint
....
savepoint part1; #设置一个保存点
....
rollback to part1;#回滚到保存点part1

ACID

原子性(Atomicity)一致性(Consistency)隔离性(Isolation)持久性(Durability)

  • 持久性

持久性则是指在一个事务提交后,这个事务的状态会被持久化到数据库中,他对数据库中数据的改变就应该是永久性的,数据会持久化到硬盘。redo日志保存的是执行成功但没写入磁盘的操作。当断电/数据库崩溃等状况发生时把redo日志写到磁盘,保证了事务的持久性

  • 原子性

原子性是指事务的原子性操作,对数据的修改要么全部执行成功,要么全部失败,实现事务的原子性,是基于日志的Redo/Undo机制。

undo日志是保存事务执行一部分尚未提交的操作,当断电/数据库崩溃,保证了事务的原子性

  • 隔离性

数据库使用的方式保证隔离性,一个事务的执行不能被其他事务干扰,每个事务的执行过程是相对独立的。

  • 一致性

由前面三个特性保证,也是最重要的特性。一致性是指执行事务前后的状态要一致,可以理解为数据一致性。原子性、隔离性、持久性都是为了保障一致性而存在的,一致性也是最终的目的。

什么是Redo/Undo机制?

Redo/Undo机制比较简单,它们将所有对数据的更新操作都写到日志中。

  • redo log用来记录某数据块被修改后的值,可以用来恢复未写入 data file 的已成功事务更新的数据;

  • Undo log是用来记录数据更新前的值,保证数据更新失败能够回滚。

  • 假如数据库在执行的过程中,不小心崩了,可以通过该日志的方式,回滚之前已经执行成功的操作,实现事务的一致性。

  • 假如某个时刻数据库崩溃,在崩溃之前有事务A和事务B在执行,事务A已经提交,而事务B还未提交。当数据库重启进行 crash-recovery 时,就会通过Redo log将已经提交事务的更改写到数据文件,而还没有提交的就通过Undo log进行roll back。

隔离性分为四个级别

数据库事务有不同的隔离级别,不同的隔离级别对锁的使用是不同的,锁的应用最终导致不同事务的隔离级别。

  • Read Uncommitted(读未提交):事物能读到不同事物没有提交(未commit)的数据结果,实际应用比较少,会产生脏读,事物已经读到其他事物未提交的数据,但数据被回滚,称为脏读
  • Read Committed(读已提交)RC:事物读取其他事物已经提交的数据,读取到的是最新的数据,所以会出现在同一事物中select读取到的数据前后不一致,会出现不可重复读问题,不可重复读问题就是我们在同一个事务中执行完全相同的select语句时可能看到不一样的结果。
  • Repeatable Read(可重复读)RR:mysql默认事物隔离级别,在同一事物中多次读取同样的数据结果是一样的,解决了不可重复读的问题,此级别会出现幻读的问题,幻读:当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。
  • Serializable(串行化):最高的事物隔离级别,串行化强制事物排序阻塞,避免事物冲突,解决了上述所有的问题,它使用了共享锁,执行效率低下,会导致大量的超时和锁切换竞争现象,实际开发应用很少。

刚刚说了从上到下是的四个级别是因为加锁的力度不同,越往下加锁越严格,隔离性越好,数出错概率越少,但是也会对性能影响严重,造成不能同时访问的问题。所以要选一个适合自己业务的隔离性级别,达到可容忍的数据出错跟并发量的平衡。

并发事务执行过程中可能遇到的一些问题:按照严重性来排序:脏读 > 不可重复读 > 幻读

数据库事务并发可能出现的问题

脏读:在隔离级别读未提交中可能会出现,一个事务读取另外一个事务还没有提交的数据叫脏读,事物A读取了事物B更新的事物,事物B没有commit并且回滚,此时就事物A产生脏读,应用也没保证数据的正确性。

SELECT @@tx_isolation #查看事务隔离等级
#设置事务的隔离级别
SET [SESSION|GLOBAL] TRANSACTION ISOLATION LEVEL [READ UNCOMMITTED|READ COMMITTED|REPEATABLE READ|SERIALIZABLE];

#设置隔离级别的语句中set global transaction isolation level read uncommitted,这里的global也可以换成session,global表示全局的,而session表示当前会话,也就是当前窗口有效。
#设置隔离等级读未提交
set session transaction isolation level read uncommitted;

#当设置完隔离级别后对于之前打开的会话,是无效的,要重新打开一个窗口设置隔离级别才生效。
#在执行开启事务begin/start transaction命令,它们并不是一个事务的起点,在执行完它们后的第一个sql语句,才表示事务真正的启动

不可重复读:在隔离级别读已提交中可能会出现的问题,事物能读取其他事物对同一数据的更改。事务 A 多次读取同一数据,事务 B在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果不一致。

幻读:在隔离级别可重复读中可能出现的问题,幻读是针对按范围读取多条数据的现象。同一事务A多次查询,若另一事务B只是update,则A事务多次查询结果相同;若B事务insert/delete数据,则A事务多次查询就会发现新增或缺少数据,出现幻读。

隔离级别 脏读 不可重复读 幻读
读未提交
读已提交
可重复读
串行化

读已提交等级下解决脏读办法

  • 把释放锁的位置调整到事务提交之后,此时在事务提交前,其他进程是无法对该行数据进行读取的,包括任何操作。

  • mysql中在该级别开始使用之前提到的MVCC机制(后面),简单来说就是:B事务要修改数据时复制一份旧数据,以事务号区分,在复制的数据上修改直至提交。A事务可以继续从旧数据读取数据,从而实现不相互阻塞。

可重复读解决不可重复读

在这里插入图片描述

那锁又是什么呢?可以参考【数据库】MySQL中的锁

  • 总来的说 锁分为如下

在这里插入图片描述

数据库实现事务隔离的方式有两种

  1. 一种是在读取数据前,对其加锁,阻止其他事务对数据进行修改,简称为LBCC

  2. 另一种是不用加任何锁,通过一定机制生成一个数据请求时间点的一致性数据快照(Snapshot),并用这个快照来提供一定级别(语句级或事务级)的一致性读取,简称MVCC

MySQL 是如何解决幻读的

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

  • 快照读
    像不加锁的 select 操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即 MVCC ,可以认为 MVCC 是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本

说白了 MVCC 就是为了实现读-写冲突不加锁,而这个读指的就是快照读, 而非当前读,当前读实际上是一种加锁的操作,是悲观锁的实现

那什么又是悲观锁呢

乐观锁、悲观锁

  • 锁从使用方式来分可分为乐观锁和悲观锁,乐观锁和悲观锁在很多应用当中都存在的概念,并不是实质存在的锁叫乐观锁悲观锁,在mysql数据中有,在hibernate、java等当中也有。

  • 乐观锁是在遇到事物并发问题时,想法很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,认为这次的操作不会导致冲突,当出现事物并发问题时再处理。乐观锁由于加锁少,所以性能开销比较小,吞吐量大。

  • 悲观锁的特点是先获取锁,再进行业务操作。每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,开始就默认会出现事物并发问题,所以在进行每次操作数据时都要通过获取锁才能进行对相同数据的操作,悲观锁需要耗费较多的时间,处理并发问题也相对严谨,在核心业务的关键之处可使用悲观锁,开销相对来说大。

经典高并发 ,如何避免库存超发问题
平时在购买商品操作库存流程大概如下:

基本购买流程
假如当前库存量为5,第一个购买请求数量为4个,顺利购买,当前剩余1,第二次购买4个,此时库存不足,程序直接返回库存不足,购买失败。

上述情况不是在高并发情况下,在高并发情况下,可能有多个请求同时购买,同时读取库存更新存库,两次请求同时读取到的库存都是5,然后都执行购买操作,减库存,就出现了库存超发现象,库存减至-3。

类似问题可以用乐观锁、悲观锁来解决。

1、悲观锁解决方案

出现超发的问题根本原因就是共享的数据被多个线程同时修改,如果单独的一个线程想要修改数据,在读取数据时将数据锁定,加排他锁,不允许其他线程读取和修改数据,直到存库修改完成释放锁,就不会出现超发现象。

//开始事物
select id,productid,stock from zproduct where id=? for update;#排他锁

update zproduct set stock=stock-? where id=?;
//结束事物

使用for update给行数据加排他锁,其他事物就不能对它读和写,避免了数据同时被多个事物修改,此解决方案是实现比较简单,缺点是加排他锁影响系统的并发性能。

2、乐观锁解决方案

悲观锁很容易产生线程阻塞,为了提高性能,出现了乐观锁方案,不使用数据库锁,不阻塞线程并发。

实现方法:给商品添加一个version字段,代码行修改版本号,读取库存是拿到这个version版本号,在更新时再对比version版本号,如果版本号相同,说明库存没有被其他线程修改过,可以正常操作,同时把version数值加1。如果版本号不同,代表被别的线程修改过,则取消修改库存操作,返回购买失败。

update zproduct set stock=stock-?,version=version+1 where id=? and version=?;
MySQL 解决幻读的两种方式
  1. 多版本并发控制MVCC 快照读

多数数据库都实现了多版本并发控制,并且都是靠保存数据快照来实现的。以 InnoDB 为例,每一行中都冗余了两个字段。一个是行的创建版本,一个是行的删除(过期)版本。版本号随着每次事务的开启自增。事务每次取数据的时候都会取创建版本小于当前事务版本的数据,以及过期版本大于当前版本的数据。普通的 select 就是快照读

原理:将历史数据存一份快照,所以其他事务增加与删除数据,对于当前事务来说是不可见的。

  1. next-key 锁 (当前读)

next-key 锁包含两部分

  • 记录锁(行锁

  • 间隙锁

记录锁是加在索引上的锁,间隙锁是加在索引之间的。(思考:如果列上没有索引会发生什么?行锁变表锁)

select * from T where number = 1 for update;		#排他锁
select * from T where number = 1 lock in share mode; #共享锁
  • Mysql官方给出的幻读解释是:只要在一个事务中,第二次select多出了row就算幻读。
    T1事务先select,T2事务insert确实会加一个gap锁,但是如果T2事务commit,这个gap锁就会释放(释放后T1事务可以随意dml操作),T1事务再select出来的结果在MVCC下还和第一次select一样,接着T1事务不加条件地update,这个update会作用在所有行上(包括T2事务新加的),T1事务再次select就会出现T2事务中的新行,并且这个新行已经被update修改了,实测在RR级别下确实如此。
    如果这样理解的话,Mysql的RR级别确实防不住幻读

模拟演示

  • student

在这里插入图片描述

  • T2对student新增,并提交

在这里插入图片描述

  • T1再对student查询发现还是没有,这也是保证了不可重复读,但是T1执行update所有行,在查询的时候发现T2新增的也被修改了

在这里插入图片描述

【但是我们进行update相当于进入到了当前读,结果是最新的所以会修改到T2的插入】

  • 在快照读读情况下,mysql通过mvcc来避免幻读。
  • 在当前读读情况下,mysql通过next-key来避免幻读。
select * from t where a=1;属于快照读
select * from t where a=1 lock in share mode;属于当前读

不能把快照读和当前读得到的结果不一样这种情况认为是幻读,这是两种不同的使用。所以mysql的RR级别是解决了幻读的

如引用一问题所说,T1 select 之后 update,会将 T2 中 insert 的数据一起更新,那么认为多出来一行,所以防不住幻读。但是其实是错误的,InnoDB 中设置了 快照读 和 当前读 两种模式,如果只有快照读,那么自然没有幻读问题,但是如果将语句提升到当前读,那么 T1 在 select 的时候需要用如下语法: select * from t for update (lock in share mode) 进入当前读,那么自然没有 T2 可以插入数据这一回事儿了。

【注意】
next-key 固然很好的解决了幻读问题,但是还是遵循一般的定律,隔离级别越高,并发越低。

为什么要解决幻读

在高并发数据库系统中,需要保证事务与事务之间的隔离性,还有事务本身的一致性。

总结

在mysql中,提供了两种事务隔离技术,第一个是mvcc,第二个是next-key技术。这个在使用不同的语句的时候可以动态选择。不加lock inshare mode之类的快照读就使用mvcc。否则 当前读使用next-key。mvcc的优势是不加锁,并发性高。缺点是不是实时数据。next-key的优势是获取实时数据,但是需要加锁。

SQL 标准中规定的 RR 并不能消除幻读,但是 MySQL 的 RR 可以,靠的就是 Gap 锁。在 RR 级别下,Gap 锁是默认开启的,而在 RC 级别下,Gap 锁是关闭的。

MVCC

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

  • 同一行数据平时发生读写请求时,会上锁阻塞住,但mvcc用更好的方式去处理读–写请求,做到在发生读–写请求冲突时,不加锁
  • 这个读时指的快照读,而不是当前读,当前读是一种加锁操作,是悲观锁

什么是当前读和快照读?这里再重述一遍

  • 当前读
    像 select lock in share mode (共享锁), select for update; update; insert; delete (排他锁)这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁

  • 快照读
    像不加锁的 select 操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即 MVCC ,可以认为 MVCC 是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本

  • 说白了 MVCC 就是为了实现读-写冲突不加锁,而这个读指的就是快照读, 而非当前读,当前读实际上是一种加锁的操作,是悲观锁的实现

数据库并发场景有三种,分别为:

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

MVCC 解决并发哪些问题?

多版本并发控制(MVCC)是一种用来解决读-写冲突无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照。 所以 MVCC 可以为数据库解决以下问题

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

因为有了下面提高并发性能的组合 :

  • MVCC + 悲观锁
    MVCC解决读写冲突,悲观锁解决写写冲突
  • MVCC + 乐观锁
    MVCC 解决读写冲突,乐观锁解决写写冲突

MVCC 的实现原理

它的实现原理主要是依赖记录中的 版本链(3个隐式字段)undo日志Read View 来实现的

版本链

我们数据库中的每行数据,除了我们肉眼可见的数据,还有三个隐藏字段:db_trex_iddb_roll_pointerdb_row_id

  • db_trex_id
    6 byte,最近修改(修改/插入)事务 ID:记录创建这条记录/最后一次修改该记录的事务 ID
  • db_roll_pointer(版本链关键)
    7 byte,回滚指针,指向这条记录上一个版本(存储于 rollback segment 里)
  • db_row_id
    6 byte,隐含的自增 ID(隐藏主键),如果数据表没有主键,InnoDB 会自动以DB_ROW_ID产生一个聚簇索引
  • 实际还有一个删除 flag隐藏字段, 既记录被更新删除并不代表真的删除,而是删除 flag变了

在这里插入图片描述

如上图,db_row_id 是数据库默认为该行记录生成的唯一隐式主键db_trex_id 是当前操作该记录的事务 ID,而 db_roll_pointer 是一个回滚指针,用于配合 undo日志,指向上一个旧版本

每次对数据库记录进行改动,都会记录一条undo日志,每条umdo日志也都会有一个db_roll_pointer属性(insert操作对应的undo日志没有该属性,因为该记录并没有更早的版本),可以将这些undo日志都连起来串成一个链表,所以现在的情况就像下图一样

在这里插入图片描述

在这里插入图片描述

对该记录每次更新后,都会将旧值放到一条undo日志中,就算是该记录的一个旧版本,随着更新的次数增多,所有的版本都会被roll_pointer属性连成一个链表,我们把这个链表称之为版本链,版本链的头结点就是当前最新的值。另外,每个版本中还包含生成该版本是对应的事务id,这个信息很重要,在根据ReadView判断版本可见性的时候会用到

undo日志

Undo log 主要用于记录数据被修改之前的日志,在表信息修改之前先会把数据拷贝到undo log里。

事务进行回滚时可以通过undo log 里的日志进行数据还原

Undo log 的用途

  • 保证事务进行rollback时的原子性和一致性,当事务进行回滚的时候可以用undo log的数据进行恢复
  • 用于MVCC快照读的数据,在MVCC多版本控制中,通过读取undo log历史版本数据可以实现不同事务版本号都拥有自己独立的快照数据版本

undo log主要分为两种:

  • insert undo log

    代表事务在insert新记录时产生的undo log , 只在事务回滚时需要,并且在事务提交后可以被立即丢弃

  • update undo log(主要)

    事务在进行update或delete时产生的undo log ; 不仅在事务回滚时需要,在快照读时也需要;

    所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除

Read View(读视图)

什么是Read View

  • 事务进行快照读操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照

  • 记录并维护系统当前活跃事务的ID(没有commit,当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以越新的事务,ID值越大),是系统中当前不应该被本事务看到的其他事务id列表

  • Read View主要是用来做可见性判断的, 即当我们某个事务执行快照读的时候,对该记录创建一个Read View读视图,把它比作条件用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的undo log里面的某个版本的数据。

Read View几个属性

  • trx_ids: 当前系统活跃(未提交)事务版本号集合。
  • low_limit_id: 创建当前read view 时“当前系统最大事务版本号+1”。
  • up_limit_id: 创建当前read view 时“系统正处于活跃事务最小版本号
  • creator_trx_id: 创建当前read view的事务版本号;

Read View可见性判断条件

在这里插入图片描述

  • 该方法展示了我们拿 db_trx_id去跟 Read View 某些属性进行怎么样的比较

  • db_trx_id < up_limit_id || db_trx_id == creator_trx_id(显示)

    如果数据事务ID小于read view中的最小活跃事务ID,则可以肯定该数据是在当前事务启之前就已经存在了的,所以可以显示

    或者数据的事务ID等于creator_trx_id ,那么说明这个数据就是当前事务自己生成的,自己生成的数据自己当然能看见,所以这种情况下此数据也是可以显示的。

  • db_trx_id >= low_limit_id(不显示)

    如果数据事务ID大于read view 中的当前系统的最大事务ID,则说明该数据是在当前read view 创建之后才产生的,所以数据不显示。如果小于则进入下一个判断

  • db_trx_id是否在活跃事务(trx_ids)中

    • 不存在:则说明read view产生的时候事务已经commit了,这种情况数据则可以显示
    • 已存在:则代表我Read View生成时刻,你这个事务还在活跃,还没有Commit,你修改的数据,我当前事务也是看不见的。

了解了 隐式字段undo log, 以及 Read View 的概念之后,就可以来看看 MVCC 实现的整体流程是怎么样了

整体流程

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

在这里插入图片描述

  • Read View 不仅仅会通过一个列表 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,Read View 如下图
    在这里插入图片描述

  • 我们的例子中,只有事务 4修改过该行记录,并在事务 2 执行快照读前,就提交了事务,所以当前该行当前数据的 undo log 如下图所示;我们的事务 2 在快照读该行记录的时候,就会拿该行记录的 DB_TRX_ID 去跟 up_limit_id , low_limit_id 和活跃事务 ID 列表( trx_list )进行比较,判断当前事务 2能看到该记录的版本是哪个。

在这里插入图片描述

  • 所以先拿该DB_TRX_ID 字段记录的事务 ID 4去跟 Read View 的 up_limit_id比较,看 4 是否小于 up_limit_id( 1 ),所以不符合条件,继续判断 4 是否大于等于 ow_limit_id( 5 ),也不符合条件,最后判断 4 是否处于trx_list 中的活跃事务, 最后发现事务 ID 为 4 的事务不在当前活跃事务列表中, 符合可见性条件,所以事务 4修改后提交的最新结果对事务 2 快照读时是可见的,所以事务 2 能读到的最新数据记录是事务4所提交的版本,而事务4提交的版本也是全局角度上最新的版本

  • 也正是 Read View 生成时机的不同,从而造成 RC , RR 级别下快照读的结果的不同

MVCC和事务隔离级别

上面所讲的Read View用于支持RC(Read Committed,读提交)和RR(Repeatable Read,可重复读)隔离级别实现

RR 是如何在 RC 级的基础上解决不可重复读的?

当前读和快照读在 RR 级别下的区别:

在这里插入图片描述

  • 事务 B 的在事务 A 提交修改后的快照读是旧版本数据,而当前读是实时新数据 400

在这里插入图片描述

  • 事务 B 在事务 A 提交后的快照读和当前读都是实时的新数据 400,这是为什么呢?

  • 这里与上表的唯一区别仅仅是表 1的事务 B 在事务 A 修改金额前快照读过一次金额数据,而表 2的事务B在事务A修改金额前没有进行过快照读。

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

RC , RR 级别下的 InnoDB 快照读有什么不同?

正是 Read View 生成时机的不同,从而造成 RC , RR 级别下快照读的结果的不同

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

参考文章
【MySQL笔记】正确的理解MySQL的MVCC及实现原理
我以为我对Mysql事务很熟,直到我遇到了阿里面试官
MySql是如何解决幻读的?
【MySQL】当前读、快照读、MVCC
MySQL的锁机制 - 记录锁、间隙锁、临键锁
B站IT老哥

Guess you like

Origin blog.csdn.net/qq_51998352/article/details/121128313