MVCC

我们一般默认工作在事务的中间两级隔离级别:

2、TRANSACTION_READ_COMMITTED。已提交读。说明读取未提交的数据是不允许的。这个级别仍然允许不可重复读和虚读产生。

3、TRANSACTION_REPEATABLE_READ。可重复读。说明事务保证能够再次读取相同的数据而不会失败,部分解决了虚读,但虚读是仍然会出现的。

mysql 的undo日志在哪_数据库


备注:

事务隔离级别越高,为避免冲突所花费的性能也就越多。

在“可重复读”级别,实际上可以解决部分的虚读问题,但是不能防止update更新产生的虚读问题,要禁止虚读产生,还是需要设置串行化隔离级别。

InnoDB的事务隔离级别已提交读和可重复读的底层实现原理:MVCC(多版本并发控制)–》并发的读取方式:快照读
InnoDB提供了2种读取操作:锁定读和非锁定读
锁定读就是读取的时候加锁了(S锁或者X锁)
非锁定读就是读取的时候没有加锁,指的就是MVCC提供的快照读–》依赖底层的undo log回滚日志。

事务日志:undo log回滚日志 和 redo log重做日志
ACD(依靠 事务日志 保证的) I(依靠 锁+MVCC 保证的)

undo log回滚日志的主要作用:
1、事务发生错误时回滚rollback ,回滚日志(数据在更新的时候,把改之前的数据存下来在回滚日志中,目的是为了事务万一出错回滚了或者我们手动回滚的时候,能够把最初的数据在回滚日志中找到)
2、提供了MVCC的非锁定读(快照读)—》undo log的原理

undo log回滚日志

undo log回滚日志,保存了事务发生之前的数据的一个版本,用于事务执行时的回滚操作,同时也是实现多版本并发控制(MVCC)下读操作的关键技术。

DB_TRX_ID:事务ID

DB_ROLL_PTR:回滚指针

mysql 的undo日志在哪_数据库_02


在MVCC下,针对表的所有记录,除了我们能看到的book_id,book_name等存放书籍信息的一张表,如下:

mysql 的undo日志在哪_mysql_03

它还会给这张表添加额外的字段如下:

mysql 的undo日志在哪_数据_04


我们看到额外添加了3个字段DB_ROW_ID和MVCC的关系不大,这个是你在创建表的时候,如果没有加主键列,那么InnoDB就给你增加的主键列id,为了区分每一行记录,通过这个id作为主键创建索引树,在B+树的叶子节点上存放一行一行的数据,因为InnoDB的数据和索引是存放在一起的,如果你没有设置主键,它会帮你设置主键。

DB_TRX_ID(事务ID) (1002, 1001, 1000),我们在之前经常登录两个窗口session,然后各输入begin;这个意思就是在两个窗口上各开启一个事务,事务开启的请求最终会发到MySQL server上,MySQL server为每1个事务都会分配一个全局的,不冲突的事务ID。(InnoDB存储引擎分配的,因为它才支持事务)
那也就是说,当我这个事务1修改数据的时候,DB_TRX_ID放的就是事务1的ID,同一个事务对这个数据不断改来改去,这个事务ID是不会改变的。

DB_ROLL_PTR(回滚指针),我们看到,存放的是地址,这个地址表示的是一个数据的内存的位置,看起来是一个链表,上图中的这个橙色表是最后的成品,那么这个表是怎么变来的呢?

最初的时候表是这样的:

mysql 的undo日志在哪_mysql_05


假设现在有一个事务来更改这条数据了。

这个事务把book_name给更改了。

改成了这个:

mysql 的undo日志在哪_mysql 的undo日志在哪_06


此时数据涉及到修改了,修改的数据存放在当前这个位置,那修改之前的数据存放在哪里?存放在undo log中!假设最初是 最初给这条数据赋值的事务的ID是1000,回滚指针是空

mysql 的undo日志在哪_mysql_07


现在另一个事务要对数据进行修改:

mysql 的undo日志在哪_回滚_08


对这条数据修改的事务的ID是1020

它的DB_ROLL_PTR指针就指向修改之前的数据(undo log,是写在缓存中的,提高效率)

mysql 的undo日志在哪_mysql 的undo日志在哪_09

假设现在1000这个事务又把book_name给更改了

此时之前的这2条数据就是老数据了,都到undo log里面了

mysql 的undo日志在哪_回滚_10


MVCC会给我们创建的表增加2个列,一个是事务ID,一个是指向修改前的数据的指针,修改之前的数据都是放在undo log回滚日志当中。当前数据中是哪个事务ID改的就写哪个事务ID号,DB_ROLL_PTR指针把当前数据和旧数据串成1个链表。从当前行的DB_ROLL_PTR可以访问到旧数据,进行回滚是理所当然的。

mysql 的undo日志在哪_数据库_11


绿色数据表示当前行的最新修改的数据,是事务1002修改的,从指针DB_ROLL_PTR可以找到undo log找到上次修改数据的是事务1001,从事务1001的数据的DB_ROLL_PTR还可以找到上上次的数据是事务1000修改的。以此类推。

示例讲解

这是原始的user表:

mysql 的undo日志在哪_数据_12


我们现在对id=7这条记录进行更改。

在MVCC机制下,对这张表增加2列。修改当前数据的事务ID和指向undo log的指针。

我们把id=7的age改为16。如下图所示:

mysql 的undo日志在哪_数据库_13


假如我们增加了1行新数据。

因为新的数据就没有老数据,如下图所示:

mysql 的undo日志在哪_mysql_14


回滚的时候发现是空,就知道是insert增加的数据。回滚的时候执行delete就把这条数据删除了。如果我们对这条新数据进行修改了,那么这些数据就都跑到undo log中了!如下图所示:

mysql 的undo日志在哪_数据_15


在已提交读和可重复读这2种隔离级别下,当我们去读数据的时候,是通过MVCC的快照读实现的。

MVCC多版本并发控制

MVCC是多版本并发控制(Multi-Version Concurrency Control,简称MVCC),是MySQL中基于乐观锁理论实现隔离级别的方式,用于实现已提交读和可重复读隔离级别的实现,也经常称为多版本数据库。MVCC机制会生成一个数据请求时间点的一致性数据快照 (Snapshot), 并用这个快照来提供一定级别 (语句级或事务级) 的一致性读取。从用户的角度来看,好象是数据库可以提供同一数据的多个版本(系统版本号和事务版本号)。

MVCC多版本并发控制中,读操作可以分为两类:

分为

快照读(非锁定读):读的是记录的可见版本,不用加锁。如select读的是拍照片的时候的现场的数据。

当前读:读取的是记录的最新版本,并且当前读返回的记录。如insert,delete,update,select…lock in share

mode/for update这些操作,都是读的是最新的数据

mysql 的undo日志在哪_回滚_16

MVCC:每一行记录实际上有多个版本,每个版本的记录除了数据本身之外,增加了其它字段
DB_TRX_ID:记录当前事务ID
DB_ROLL_PTR:指向undo log日志上数据的指针

已提交读:每次执行语句的时候都重新生成一次快照(Read View),每次select查询时。
可重复读:同一个事务开始的时候生成一个当前事务全局性的快照(Read View),第一次select查询时

快照内容读取原则

1、版本未提交,无法读取生成快照
2、版本已提交,但是在快照创建后提交的,无法读取
3、版本已提交,但是在快照创建前提交的,可以读取
4、当前事务内自己的更新,可以读到(通过匹配当前事务的id号就可以看到当前数据的最新更新是不是当前事务做的,如果是当前事务做的,当前事务就可以看到最近的更新)

已提交读 这个隔离机制的测试

已提交读:解决了脏读问题。但是不可重复读和幻读的问题还是存在。

我们先修改隔离级别:

mysql 的undo日志在哪_mysql_17


mysql 的undo日志在哪_数据_18


脏读就是事务2读取了事务1还未提交的数据。

事务2 select

mysql 的undo日志在哪_数据_19


然后事务1 update

mysql 的undo日志在哪_数据_20


没有提交,所以事务2不能看到

mysql 的undo日志在哪_回滚_21


这就是已经解决了脏读了。

不管是已提交读还是可重复读,当我们只要去select的时候,就会产生一个数据快照,相当于给当前的数据拍个照片,以后去查询,都是查询快照上的数据。已提交读是采用非锁定读。通过MVCC提供的快照读解决的。
非锁定读是在快照上的操作。

已提交读:每一次select都会产生一次新的数据快照。

当事务1进行更改的时候,事务2又去select,重新产生数据快照了但是产生新的数据快照的前提是新的数据已经被事务正确commit过的

数据有2种状态:prepare和commit

事务2第二次select的时候,由于事务1修改数据并没有commit,当又一次产生数据快照时,产生的数据快照还是undo log回滚日志的链表指向的旧数据。

mysql 的undo日志在哪_数据_22

接下来测试不可重复读

事务1把修改的数据进行commit了

mysql 的undo日志在哪_数据库_23


事务2现在select出来的数据就是age=16了

mysql 的undo日志在哪_数据_24


这就是不可重复读的现象。

mysql 的undo日志在哪_数据库_25


为什么无法解决不可重复读?

因为每一次select都会重新产生1次数据快照。其他事务更新后而且已commit的数据可以实时反馈到当前事务的select结果当中。同样的为什么会出现幻读?

mysql 的undo日志在哪_mysql 的undo日志在哪_26


事务2 select

mysql 的undo日志在哪_mysql 的undo日志在哪_27


然后事务1insert,然后commit提交了

mysql 的undo日志在哪_回滚_28


然后现在事务2再查

mysql 的undo日志在哪_mysql_29


出现了幻读。因为每一次select都重新产生1次快照。

每一次select就是相当于给下图的绿色框拍照。然后找age=16的数据,自然就产生幻读了。

mysql 的undo日志在哪_数据库_25


总结:

mysql 的undo日志在哪_回滚_31

可重复读测试 这个隔离机制的测试

解决了脏读问题(因为生成了数据快照,在数据快照上查询),而且支持可重复读!!!

为什么解决了不可重复读问题?
因为 一个事务的第一次select 产生数据快照,而且只产生1次数据快照。

我们修改好隔离级别:

mysql 的undo日志在哪_回滚_32


事务2 第一次 select ,产生了数据快照

mysql 的undo日志在哪_数据库_33


这张照就已经写死了。事务2后面的select就不生成数据快照了。

事务1 update,然后commit

mysql 的undo日志在哪_数据库_34


照片第一次已经拍了,事务1现在去修改,

mysql 的undo日志在哪_数据库_35


事务2 现在再去select。查的还是第一次select生成的快照。

mysql 的undo日志在哪_回滚_36


再举一个例子:

mysql 的undo日志在哪_mysql 的undo日志在哪_37


刚才是事务2先第一次select了,所以产生了一次数据快照,然后事务1更改数据,事务2再查就是原来的数据快照。

现在我们让事务1先修改数据,commit

mysql 的undo日志在哪_mysql_38


现在事务2才第一次select,之前没有数据快照。

所以可以访问到事务1更改后的数据

mysql 的undo日志在哪_数据_39


当前事务第一次select产生数据快照,后面其他事务虽然修改了最新的数据,但是当前事务再select的时候,依然查看的是最初的快照数据。为什么部分解决了幻读???

mysql 的undo日志在哪_回滚_40


事务2第一次select,产生了数据快照

mysql 的undo日志在哪_数据库_41


就是下面这个数据快照:

mysql 的undo日志在哪_数据库_42


然后事务1现在insert into,然后commit

mysql 的undo日志在哪_数据_43


事务2再select,查看的是最初的数据快照,在原来的数据快照中进行数据查找。

mysql 的undo日志在哪_数据库_44


已经解决了幻读。

但是我们说,它只是部分解决了幻读

那是因为

我们现在在事务2 update

但是update,insert,delete,做的是当前读,读的是最新的数据!
更新的是最新的数据

假设事务1的ID是1000,事务2的ID是2000

事务1第一次insert的数据的事务ID就是1000

现在事务2执行update,修改事务1insert的数据,当前最新数据的事务ID是2000

如下图所示 :

mysql 的undo日志在哪_数据_45


当前事务是可以看见自己事务修改、更新的数据的!!!

mysql 的undo日志在哪_mysql_46

事务2update修改,是当前读。

mysql 的undo日志在哪_mysql 的undo日志在哪_47


如果事务2再去select的时候,它不仅可以看到数据快照,还可以看到自己修改的数据

mysql 的undo日志在哪_数据_48


mysql 的undo日志在哪_mysql 的undo日志在哪_49

当前事务是可以看见自己事务修改、更新的数据的!!!
所以,并没有完全解决幻读问题