【MySQL】根据MVCC和Read View分析事务的四种隔离级别在读写场景分别是如何体现其隔离性的


目录

一、数据库并发的三种场景

二、读写场景的MVCC

1、3个(4个)记录隐藏列字段

[2、undo log(撤销日志)](#2、undo log(撤销日志))

3、模拟MVCC场景

3.1update场景

3.2delete场景

3.3insert

3.4select场景

[4、Read View](#4、Read View)

5、RR和RC的区别

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

例一:root在jly修改前快照读

例二:root在jly修改后快照读

5.2MySQL对四种隔离级别的不同处理方式

三、写写场景


一、数据库并发的三种场景

读-读 :不存在任何问题,也不需要并发控制

读-写 :有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读

写-写 :有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失

二、读写场景的MVCC

多版本并发控制( MVCC )是一种用来解决 读-写冲突的无锁并发控制。

为事务分配单向增长的事务ID,为每个修改保存一个版本,版本与事务ID关联,读操作只读该事务开始前的数据库的快照。 所以 MVCC 可以为数据库解决以下问题:

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

1、3个(4个)记录隐藏列字段

在创建表的时候,MySQL除了创建用户所需的列之外,还会创建3个记录隐藏列字段

**DB_TRX_ID :**6 byte,这一列记录了每一行最后一次修改的事务ID( 修改/插入 )。

**DB_ROLL_PTR :**7 byte,回滚指针,指向这条记录的上一个版本(简单理解成,指向历史版本就行,这些数据一般在 undo log 中)

**DB_ROW_ID :**6 byte,隐含的自增ID(隐藏主键),如果数据表没有主键, InnoDB 会自动以DB_ROW_ID产生一个聚簇索引

**第四个隐藏列字段:**实际还有一个标识该行数据是否删除的隐藏字段flag。

例如创建并插入一条数据,实际的表结构应该是这样的:

|------|-----|---------------|-----------------|---------------|
| name | age | DB_TRX_ID | DB_ROLL_PTR | DB_ROW_ID |
| 张三 | 20 | 创建该事务的ID | null | 1(隐式主键) |

2、undo log(撤销日志)

MySQL是以守护进程的方式,在内存中运行。undo log是MySQL中的一段内存缓冲区,用以保存日志数据。

3、模拟MVCC场景

3.1update场景

现有一个事务ID为10,对上方信息表进行update,将name由张三修改为李四:

1、因为要修改,所以要先给该记录加上行锁。

2、在修改之前,先将改行数据拷贝一份到undo log中(写时拷贝,原始数据在表中,拷贝的数据在undo log中,假设拷贝的数据地址是0XAA)

3、修改原始数据的同时,将隐藏字段DB_TRX_ID修改为10,将DB_ROLL_PTR回滚指针修改为0XAAAAAAAA

4、事务10commit提交,释放行锁

|--------|-----|---------------|-----------------|---------------|
| name | age | DB_TRX_ID | DB_ROLL_PTR | DB_ROW_ID |
| 李四 | 20 | 10 | 0XAAAAAAAA | 1(隐式主键) |

此时又有一个事务11,需要对信息表的记录进行update,将李四那一行的年龄修改为30:

1、因为要修改,所以要先给该记录加上行锁。

2、同样的,将当前的表中的对应行拷贝一份到undo log,假设地址0XBBBBBBBB

3、修改原始数据的同时,将隐藏字段DB_TRX_ID修改为10,将DB_ROLL_PTR回滚指针修改为0XAAAAAAAA

|------|--------|---------------|-----------------|---------------|
| name | age | DB_TRX_ID | DB_ROLL_PTR | DB_ROW_ID |
| 李四 | 30 | 11 | 0XBBBBBBBB | 1(隐式主键) |

undo log中的一个个版本,被称为快照。除了版本链之外,还可以通过记录相反sql的方式,以备数据的回滚(比如delete数据,日志可以保存insert数据)

3.2delete场景

删除数据不是清空,只需将隐藏的flag标志位设置为删除即可。也可以形成版本。

3.3insert

insert是插入,只需插入时在undo log中记录其对应的delete语句即可,回滚时只需执行这些delete语句。如果当前的事务提交了,undo log将会删除这些备份数据。(update和delete可能有别的事务还在访问,commit之后不会立马就删除undo log的回滚数据)

3.4select场景

在MySQL的RR级别下,一个事务的写操作并不会影响另一个事务的读操作。增删改,都是对最新数据进行修改,但是读取,则可能需要读取历史的版本。

当前读:读取最新的记录,就是当前读。增删改,都叫做当前读,select也有可能当前读,比如select lock in share mode(共享锁), select for update

快照读:读取历史版本。快照读不会被加锁。

多个事务同时增删改的时候,是当前读,需要加锁,如果对select也加锁,那么隔离级别就是串行化。如果select是快照读,和增删改的当前读不影响,所以可以不用加锁,并行执行效率高。事务的隔离级别决定了select读取历史数据是当前读还是快照读。(read view是否更新)

那么如何保证不同的事务,看到不同的内容呢;先来的事务,应不应该看到后来的事务所作的修改呢?Read View进行可见性判断。

4、Read View

Read View在事务首次进行快照读的时候由MySQL生成,记录并维护系统当前活跃事务的ID。Read View 在 MySQL 源码中,就是一个类,本质是配合MVCC用来判断哪些快照我能看到,那些快照我看不到。

当某个事务执行select快照读的时候,MySQL会对其new一个对象,用其内部的条件来判断当前事务能够看到哪个版本的数据,可见的数据既能是当前最新的数据,也有可能是该行记录的 undo log 里面的某个版本的数据,这由隔离级别决定。

下面是 ReadView 简化的结构体:

cpp 复制代码
class ReadView {
    // 省略...
    private:
    /** 高水位,大于等于这个ID的事务均不可见*/
    trx_id_t m_low_limit_id
    /** 低水位:小于这个ID的事务均可见 */
    trx_id_t m_up_limit_id;
    /** 创建该 Read View 的事务ID*/
    trx_id_t m_creator_trx_id;
    /** 创建视图时的活跃事务id列表*/
    ids_t m_ids;//ids_t集合类型 
    /** 配合purge,标识该视图不需要小于m_low_limit_no的UNDO LOG,
    * 如果其他视图也不需要,则可以删除小于m_low_limit_no的UNDO LOG*/
    trx_id_t m_low_limit_no;
    /** 标记视图是否被关闭*/
    bool m_closed;
    // 省略...
};
cpp 复制代码
m_ids; //一张列表,用来维护Read View生成时刻,系统正活跃的事务ID
up_limit_id; //记录m_ids列表中事务ID最小的ID
low_limit_id; //ReadView生成时刻系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID的最大值+1
creator_trx_id //创建该ReadView的事务ID

那么哪些数据能被事务读到,那些数据事务看不到呢?见下图:

总结一下:举个例子,我是学弟,我能看到比我早入学的学长的找工作的数据,但是学长看不到后入学的我找工作的数据。同理在形成快照的时候,我能看到:

已经提交的事务:

1、creator_trx_id(创建快照的事务ID)==DB_TRX_ID(undo log中最近一次修改该行的事务ID)

2、DB_TRX_ID(undo log中最近一次修改该行的事务ID)<up_limit_id(形成快照的m_ids列表中事务ID最小的ID)

创建快照时m_ids中的事务(活跃的事务ID):

1、快照中的事务ID不一定连续,快照中的事务ID范围为up_limit_id<=ID<low_limit_id。如果DB_TRX_ID(undo log中最近一次修改该行的事务ID)位于这个范围,但是快照表m_ids列表中并没有这个ID,那就说明该事务已经commit提交了,当前快照可以看到,如果m_ids列表有这个ID,说明当前快照这个ID的事务仍然活跃,不能被看到。

我看不到:

快照建立之后的新事物:

1、DB_TRX_ID(undo log中最近一次修改该行的事务ID)>=low_limit_id(快照生成时系统尚未分配的下一个ID)

如果查到不应该看到当前版本,接下来就是遍历下一个版本,直到符合条件。

|------|-----|---------------|-----------------|---------------|
| name | age | DB_TRX_ID | DB_ROLL_PTR | DB_ROW_ID |
| 张三 | 28 | 创建该事务的ID | null | 1(隐式主键) |

此时的undo log中的版本链:

在事务2对改行进行快照读的时候,按照undo log中快照的先后版本,依次遍历,得出本次我对该行的快照读应该读取到哪一个版本的快照。

5、RR和RC的区别

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

准备工作:

sql 复制代码
--将全局隔离级别设置为可重复读(需重启)
mysql> set global transaction isolation level REPEATABLE READ;
Query OK, 0 rows affected (0.00 sec)
--创建一张表
mysql> create table if not exists account(
    -> id int primary key,
    -> age int not null,
    -> name varchar(20) not null
    -> )ENGINE=InnoDB DEFAULT CHARSET=UTF8;
Query OK, 0 rows affected (0.26 sec)
--插入一条数据
mysql> insert into account values (1,18,'张三');
Query OK, 1 row affected (0.04 sec)

例一:root在jly修改前快照读

使用者:jly

sql 复制代码
--1、启动事务
mysql> begin;
Query OK, 0 rows affected (0.01 sec)
--2、进行快照读
mysql> select* from account;
+----+-----+--------+
| id | age | name   |
+----+-----+--------+
|  1 |  18 | 张三   |
+----+-----+--------+
1 row in set (0.00 sec)
--3、更新数据,修改id为1的字段的年龄为20
mysql> update account set age=20 where id=1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
--4、对事务进行提交
mysql> commit;
Query OK, 0 rows affected (0.04 sec)

使用者:root

sql 复制代码
--当上方用户执行完第一步时,root同时启动事务
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
--当上方用户执行完第二步时,root同时进行快照读,读取的结果一样
mysql> select* from account;
+----+-----+--------+
| id | age | name   |
+----+-----+--------+
|  1 |  18 | 张三   |
+----+-----+--------+
1 row in set (0.01 sec)
--当上方用户执行完第三步时,root进行快照读,发现年龄的修改并没有被读到
mysql> select* from account;
+----+-----+--------+
| id | age | name   |
+----+-----+--------+
|  1 |  18 | 张三   |
+----+-----+--------+
1 row in set (0.00 sec)
--当上方用户执行完第四步提交事务时,root再次进行快照读,发现年龄的修改还是没有被读到
mysql> select* from account;
+----+-----+--------+
| id | age | name   |
+----+-----+--------+
|  1 |  18 | 张三   |
+----+-----+--------+
1 row in set (0.00 sec)
--但是此时root使用当前读,使能够读到年龄的修改的
mysql> select* from account lock in share mode;
+----+-----+--------+
| id | age | name   |
+----+-----+--------+
|  1 |  20 | 张三   |
+----+-----+--------+
1 row in set (0.01 sec)

例二:root在jly修改后快照读

使用者:jly

sql 复制代码
--1、启动事务
mysql> begin;
Query OK, 0 rows affected (0.01 sec)
--2、进行快照读
mysql> select* from account;
+----+-----+--------+
| id | age | name   |
+----+-----+--------+
|  1 |  20 | 张三   |
+----+-----+--------+
1 row in set (0.00 sec)
--3、更新数据,修改id为1的字段的年龄为30
mysql> update account set age=30 where id=1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
--4、提交事务
mysql> commit;
Query OK, 0 rows affected (0.03 sec)

使用者:root

sql 复制代码
--1、同时启动事务
mysql> begin;
Query OK, 0 rows affected (0.01 sec)
--当上方用户执行完第四步提交事务时,root进行快照读,发现读到的数据是被修改过的
mysql> select* from account;
+----+-----+--------+
| id | age | name   |
+----+-----+--------+
|  1 |  30 | 张三   |
+----+-----+--------+
1 row in set (0.00 sec)

通过例一可以发现:root的select在jly提交之前,读到的是修改前的数据;

通过例二可以发现:root的select在jly提交之后,读到的是修改后的数据。

这是因为一个事务在读取时,MySQL会生成一个read view对象,上面介绍read view的章节说了,read view本质就是一个类,用来判断哪些快照我能看到,那些快照我看不到。

read view生成的时机不同,会影响事务的可见性。

5.2MySQL对四种隔离级别的不同处理方式

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

**可重复读:**在RR级别下的某个事务的对某条记录的第一次快照读会创建一个快照及Read View对象, 将当前系统活跃的其他事务记录起来;后续再次调用快照读的时候,还是使用的是同一个Read View,所以只要当前事务在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个Read View,所以对之后的修改不可见;即RR级别下,快照读生成Read View时,Read View会记录此时所有其他活动事务的快照,这些事务的修改对于当前事务都是不可见的。而早于Read View创建的事务所做的修改才能看见。

**读提交:**RC级别的事务中,每次快照读都会新生成一个快照和Read View, 这就是我们在RC级别下可以看到其他事务所更新内容的原因。正是RC每次快照读,都会形成Read View,所以,RC才会有不可重复读问题。
**读未提交:**当前读。没有隔离性。

**串行化:**当前读,对增删改进行加锁的同时,对select也加锁。

三、写写场景

直接理解成当前读。

相关推荐
Rookie也要加油27 分钟前
01_SQLite
数据库·sqlite
liuxin3344556631 分钟前
教育技术革新:SpringBoot在线教育系统开发
数据库·spring boot·后端
看山还是山,看水还是。1 小时前
MySQL 管理
数据库·笔记·mysql·adb
fishmemory7sec1 小时前
Koa2项目实战2(路由管理、项目结构优化)
数据库·mongodb·koa
momo小菜pa2 小时前
【MySQL 09】表的内外连接
数据库·mysql
Jasonakeke2 小时前
【重学 MySQL】四十九、阿里 MySQL 命名规范及 MySQL8 DDL 的原子化
数据库·mysql
程序猿小D2 小时前
第二百六十九节 JPA教程 - JPA查询OrderBy两个属性示例
java·开发语言·数据库·windows·jpa
小宇成长录2 小时前
Mysql:数据库和表增删查改基本语句
数据库·mysql·数据库备份
团儿.3 小时前
解锁MySQL高可用新境界:深入探索MHA架构的无限魅力与实战部署
数据库·mysql·架构·mysql之mha架构
程序猿小D3 小时前
第二百六十七节 JPA教程 - JPA查询AND条件示例
java·开发语言·前端·数据库·windows·python·jpa