MySQL可重复读和读已提交实现原理,MVCC是如何实现的。

2021-04-12 11:12:58 浏览数 (1)

1.隔离级别

MySQL中隔离级别分为4种,提未交读、读已提交、可重复读、串行化。同时MySQL默认隔离级别为可重复读。

查看MySQL隔离级别

SELECT @@tx_isolation

设置当前会话隔离级别

set session transaction isolation level 隔离级别

2.脏读、不可重复读、幻读

建表语句如下

代码语言:javascript复制
CREATE TABLE `account` (
`id` INT ( 11 ) NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` VARCHAR ( 255 ) DEFAULT NULL COMMENT '姓名',
`balance` BIGINT ( 10 ) DEFAULT NULL COMMENT '余额',
PRIMARY KEY ( `id` ) 
) ENGINE = INNODB AUTO_INCREMENT = 3 DEFAULT CHARSET = utf8;

表数据如下

脏读

所谓脏读就是指事务A对数据进行了修改但是还没有提交,此时事务B就能够查询到未提交的事务,同时对数据可以进行操作。

脏读存在于读未提交中,所以需要设置隔离级别为读未提交。如下所示,诸葛亮在事务A中扣款10000元,但是还没有提交,此时事务B就能够查询到扣款后的数据。但是如果此时A发生回滚会导致事务B的数据不是和之前查询的不一致,也就是脏读。

不可重读

所谓不可重复读是指事务A查询到数据后,事务B做了修改后进行提交,此时事务A再此查询数据时发现和前一次的数据不一致。

脏读存在于读未提交中和读已提交,所以需要设置隔离级别为读未提交或读已提交。如下所示,事务A查询余额为10000元,然后事务B在T4时刻将诸葛亮余额扣款10000元,并在T5时刻进行事务提交,此时事务A在T6时刻查询余额为0元,可以看到事务A在T3时刻和T6时刻查询同一数据却得到了不同结果,这种情况称之为不可重复读。

幻读

幻读是指事务A查询范围数据,此时事务B进行数据插入,然后事务A再此查询的时候发现数据多了一条,此时就像是产生了幻觉一样,所以称之为幻读。但是这种情况下的幻读在MySQL的可重复读情况下是不存在的,已经通过MVCC解决了。

我们可以通过以下方式来实现在可重复读情况产生的幻读。事务A先查询id为3的数据,由于没有此时为空,事务B插入一条id为3的数据,然后并提交事务,此时事务A再此插入id为3的数据会出现主键冲突。可以看到和之前看到的数据不一致,这种情况称之为幻读。这种情况产生幻读的原因是当前读(下面会介绍)。

3.MVCC版本控制

如果表中数据如如下所示,同时隔离级别为可重复读那么按照下面的时间进行执行,此时你觉得事务A和事务B查询的结果会是什么呢?

代码语言:javascript复制
CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `k` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;

insert into t(id, k) values(1,1),(2,2);

答案是在事务B中查询的结果为3,而事务A中为1,或许很好理解事务A的值为什么是1,但是却并不好理解事务B为什么是3,这要从MySQL的MVCC开始说起

版本控制链

首先在Innodb中每一个事务都有一个事务ID,只要事务启动就会存在一个事务ID,叫作 transaction id。而且事务ID是按照一定规律进行递增的,当我们对某一行数据进行更新操作时实际上会将当前的事务ID,做一个记录,这个记录存在于MySQL的隐藏列中,也就是row trx_id。同时会产生一个undo log的指针来指向上一次的数据。

例如现在将表中id为1的数据的k修改为2,且当前事务ID为99,同时在版本控制链中,上一次这一行的数据是被事务id为98的进行插入的,那么这一行数据实际上的修改过程如下。

可以看到,当执行更新操作后,实际上会在版本控制链中进行一个记录,可以理解为将原来的数据进行拷贝一份,同时现在用row_trx_id(6tyte)记录当前事务id,同时用DB_ROLL_PTR(7byte回滚指针),来指向上一个数据。

也就是说每一行数据实际上会存在多个版本,同时每个版本都有自己的row_trx_id。

read view

read view实际上就是一个数组,在可重复读隔离级别下,事务启动的时候就会产生一个read view直到事务结束。

read view中存放的是当前活跃事务id,也就是当前还没有提交的事务id,如下图所示,假如在事务之间还存在一个活跃事务id为50,事务A的事务Id为51,事务B为52,事务C为53。那么事务A的read view为[50,51],事务B为[50,51,52],事务C为[50,51,52,53]。

高低水位

read view中最小事务id为低水位,而当前系统已经创建过的事务Id最大值加1,记作高水位。例如在事务A启动时由于read view为[50,51]那么高水位就是52,低水位为50,而事务B启动时由于read view为[50,51,52]那么高水位就是53,低水位为50。

而之所以在可重读级别下能够始终看到的数据都和启动时候看的是一致的,原因就是因为高低水位加上一个当前事务id以及一个比对结果。

高低水位比对规则

1.如果row trx_id小于等于低水位落在绿色部分,表示这个版本是已提交的事务或者是当前事务自己生成的,这个数据是可见的;

2.如果row trx_id大于等于高水位落在红色部分,表示这个版本是由将来启动的事务生成的,是肯定不可见的;

3.如果落在黄色部分,那就包括两种情况

a. 若row trx_id在read view中,表示这个版本是由还没提交的事务生成的,不可见。

b. 若row trx_id不在read view中,表示这个版本是已经提交了的事务生成的,可见。

根据MVCC分析不同事务k的结果

前面说过在如下所示的执行结果中,事务B查询的k为3,事务A查询的结果为1,我们通过MVCC进行分析一下为什么是这样。

通过前面的建表可以知道此时的id为1的k实际数据为1,假设在事务A之前还存在一个事务同时事务id为50,事务A的事务id为51,事务B的事务id为52,事务C的事务id为53。

前面说过在如下所示的执行结果中,事务B查询的k为3,事务A查询的结果为1,我们通过MVCC进行分析一下为什么是这样。

通过前面的建表可以知道此时的id为1的k实际数据为1,假设在事务A之前还存在一个事务同时事务id为50,事务A的事务id为51,事务B的事务id为52,事务C的事务id为53。

如下所示,此时在事务A中需要查询数据,然后在对应的版本控制链中进行查找,首先事务A的read view为[50,51],然后根据上面所说的比对规则,然后进行查询,查找到k为3时,此时row trx_id为52,此时也就是在黄色部分也就是说在将来发生的事务中,然后再次查找下一个,得到row trx_id为53,此时也比高水位大,所以也就是将来发生的事务,然后再找到row trx_id为50的发现是已经提交的事务,因为小于等于低水位。所以事务A查询的结果就是1。

一致性读

所谓的一致性读就是指在可重复读隔离级别下,事务启动时看到的数据无论其他事务怎么修改,自己看到的数据都是和启动时候看到的数据时一致的。

更新逻辑

按照我们上面说的一致性读的话,此时如果按照上图所示,在事务B中查进行了一次更改操作,此时我们再次查询的时候应该是2而不是3,这是为什么呢?虽然事务C进行加1后变成了2,但是实际上事务B此时应该是看不到的,所以在事务B中应该是2,为什么就是3呢?

实际上在更新的时候采用了当前读的机制,也就是读最新的数据,如果不读最新的数据,那么就会导致数据丢失。所以MySQL是在更新的时候先拿到最新的数据也就是(1,2)然后在(1,2)的基础上进行加1操作,同时记录row trx_id为52。

读已提交和可重复读区别

在MySQL中可重复读和读已提交都是通过MVCC进行实现的,却别在于可重读是事务启动的时候就生成read view整个事务结束都一直使用这个read view,而在读已提交中则是每执行一条语句就重新生成最新的read view。

0 人点赞