07-MySQL-事务的隔离级别以及底层原理

1 MySQL InnoDB对隔离级别的支持

|---------------------------|---------|---------|----------------|
| 事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
| 未提交读 (Read Uncommitted) | 可能 | 可能 | 可能 |
| 已提交读(Read Committed) | 不可能 | 可能 | 可能 |
| 可重复读(Repeatable Read) | 不可能 | 不可能 | 对InnoDB不可能 |
| 串行化(Serialiable) | 不可能 | 不可能 | 不可能 |

InnoDB支持的四个隔离级别和SQL92定义的完全一致,隔离级别越高,事务的并发度越低。唯一的区别就在于,InnoDB在PR的级别就解决了幻读的问题。

也就是说,不需要使用串行化的隔离级别去解决所有问题,既保证了数据的一致性,又支持了越高的并发度。这个就是InnnoDB默认使用的RP作为事务隔离级别的原因。

2 两大实现方案

如果要解决读一致性的问题,也就是保证在一个事务中两次读取数据的结果要保持一致,实现数据的隔离;对于这个一种是基于锁来控制数据的修改、另一个基于MVCC快照读的方式保证读一致性

2.1 LBCC

第一种,既然要保证前后两次读取数据一致,那么在读数据的时候,要锁定当前需要操作的数据,不允许其他的事务的修改就行了。这种方案我们叫做基于锁的并发控制Lock Based Concurrency Control (LBCC)。

如果仅仅是基于锁来实现事务隔离级别,一个事务读取的时候不需要其他事务的修改,那就意味着不支持并发的读写操作,而我们的大多数应用都是读多写少的,这样会极大地影响操作数据的效率。

2.2 MVCC

如果一个事务前后两次读取到额数据保持一致,那么我们可以在修改数据之前给它建立一个备份或者叫快照,后面再来读取这个快照就行。这种方案我们叫做多版本并发控制(Multi Version Concurrency Control MVCC)。

MVCC的原则:

一个事务能看到的数据版本:

1、第一次查询之前已经提交的事务的修改

2、本事务的修改

一个事务部能看见的数据版本:

1、在本事务第一次查询之后创建的事务(事务ID比我的事务ID大)

2、活跃的(未提交)事务的修改

MVCC的效果:我们可以查询到这个事务开始之前已经存在的数据,即使它在后面被修改或者删除了,而在我这个事务时候新增的数据,我们是查不到的。

所以我们才把这种叫做快照,不管别的事务做任何增删改查的操作,它只能看到第一次查询时看到的数据版本。

问题: 这个快照是怎么实现的?会不会占用额外存储空间?

InnoDB的事务是有编号的,而且会不断递增,InnoDB为每行记录都实现了两个隐藏字段:
DB_TRX_ID :6字节:事务ID ,表明数据是在哪个事务插入或者修改为新数据的,就记录为当前事务ID。
DB_ROLL_PTR ,7字节:回滚指针 (我们可以把它理解为删除版本号,数据被删除或记录为旧数据的时候,记录当前事务ID,没有修改或者删除的时候是空)

第一次初始化数据:
Transaction1

复制代码
begin;
insert into mvcctest values(NULL, 'bonnie');
insert into mvcctest values(NULL, 'hello');
commit;

此时的数据,创建版本是当前事务ID,假设事务编号是1, 删除版本为空

第二个事务,执行第1次查询,读取到两条原始数据,这个时候事务ID是2:
Transaction 2:

复制代码
begin; 
select * from mvcctest;

第三个事务插入数据:

复制代码
begin;
insert into mvcctest values(NULL, 'world');
commit;

这个时候数据库的数据大致如下:

第二个事务,执行第2次查询:

复制代码
begin; 
select * from mvcctest;

MVCC的查找规则:只能查找到创建时间小于等于当前事务ID的数据,和删除时间大于当前事务ID的行(或未删除)。

也就是不能查到在我的事务开始之后插入的数据,world的创建ID大于2,所以还是只能查到两条数据。

第四个事务,删除数据,删除了id=2 hello这条数据

复制代码
begin; 
delete from mvcctest where id=2;

此时的数据,hello的删除版本被记录为当前事务ID, 4,其他数据不变

在第二个事务中,执行第3次查询:

复制代码
begin; 
select * from mvcctest;

查找规则:只能查找创建时间小于等于当前事务ID的数据,和删除时间大于等于当前事务ID的行(或未删除)

也就是,在事务开始之后删除的数据,所以hello依然可以查出来,所以还是这两条数据。

第5个事务,执行更新操作,这个事务ID是5:

复制代码
begin;
update mvcctest set name="bonnie_1215" where id =1;
commit;

第二个事务,第4次执行:

复制代码
begin; 
select * from mvcctest;

查找规则: 只能查找创建时间小于等于当前事务ID的数据,和删除时间大于当前事务ID的行(或未删除)。

因为更新后的数据bonnie_1215创建版本大于2,代表是在事务时候增加的,查不出来。

而旧数据bonnie的删除版本大于2,代表是在事务时候删除的,可以查出来。

通过上面的案例,我们能看到通过版本号的控制,无论其他事务是插入、修改、删除第一个事务查询到的数据都没有变化。

这个是MVVV的效果,当然,这里是一个简化的模型。

假设一条数据修改了3次,两次提交了一次未提交。每次修改之后都有开启一个事务去查询,那么事务2、4、6查到的数据会不一样。

|--------|-------------------------------------------------------------|
| trx_id | SQL |
| trx1 | update mvcctest set name='bonnie' where id = 1; commit; |
| trx2 | select * from mvcctest where id=1; |
| trx3 | update mvcctest set name='bonnie1215' where id = 1; commit; |
| trx4 | select * from mvcctest where id=1; |
| trx5 | update mvcctest set name='bonnie1215-151' where id = 1; 未提交 |
| trx6 | select * from mvcctest where id=1; |
| | trx2 4 6各查一次 |

InnoDB 中,一条数据的旧版本,是存放在哪里的呢?undolog。因为修改了多次,这些 undo log 会形成一个链条,叫做 undo log 链,现在 undo log 里面有刘德华、吴彦祖、盆鱼宴。

所以前面我们说的 DB ROLL PTR,它其实就是指向 undo log 链的指针。

第二个问题,事务 2、4、6最后再査一次,它们去 undo log 链找数据的时候,拿到的数据是不一样的。在这个 undo log 链里面,一个事务怎么判断哪个版本的数据是它应该读取的呢?

回想一下 MVCC 规则:
一个事务能看到的数据版本:

1、第一次查询之前已经提交的事务的修改

2、本事务的修改
一个事务不能看见的数据版本:

1、在本事务第一次查询之后创建的事务(事务 ID 比我的事务 ID 大)

2、活跃的(未提交的)事务的修改

所以,我们必须要有一个数据结构,把本事务ID、活跃事务ID、当前系统最大事务ID 存起来,这样才能实现判断。这个数据结构就叫 Read View(可见性视图),每个事务都维护一个自己的Read View.
m_ids: 表示在生成 ReadView 时当前系统中活跃的读写事务的事务 id 列表。
min_trx _id: 表示在生成 ReadView 时当前系统中活跃的读写事务中最小的事务 id也就是 m_ids 中的最小值。
max_trx_id: 表示生成 ReadView 时系统中应该分配给下一个事务的 id 值。
creator_trx_id: 表示生成该 ReadView 的事务的事务 id。有了这个数据结构以后,事务判断可见性的规则是这样的:

0、从数据的最早版本开始判断(undolog)

1、数据版本的 trx_id =creator_trx_id,本事务修改,可以访问

2、数据版本的 trx_id<min_trx_id(未提交事务的最小ID),说明这个版本在生成 ReadView 已经提交,可以访问

3、数据版本的 trx_id >max_trx_id(下一个事务ID),这个版本是生成 ReadView之后才开启的事务建立的,不能访问

4、数据版本的 trx_id 在 min_trx_ id 和 max_trx_id 之间,看看是否在 m_ids 中。

如果在,不可以。如果不在,可以。

5、如果当前版本不可见,就找 undo log 链中的下一个版本。

RR 中 Read View 是事务第一次査询的时候建立的。RC的 Read View 是事务每次查询的时候建立的。

相关推荐
XiaoLeisj15 分钟前
【MyBatis】深入解析 MyBatis XML 开发:增删改查操作和方法命名规范、@Param 重命名参数、XML 返回自增主键方法
xml·java·数据库·spring boot·sql·intellij-idea·mybatis
dleei1 小时前
MySql安装及SQL语句
数据库·后端·mysql
信徒_1 小时前
Mysql 在什么样的情况下会产生死锁?
android·数据库·mysql
嘴对嘴编程2 小时前
oracle数据泵操作
数据库·oracle
苹果酱05673 小时前
Golang标准库——runtime
java·vue.js·spring boot·mysql·课程设计
·薯条大王8 小时前
MySQL联合查询
数据库·mysql
morris13110 小时前
【redis】redis实现分布式锁
数据库·redis·缓存·分布式锁
hycccccch11 小时前
Canal+RabbitMQ实现MySQL数据增量同步
java·数据库·后端·rabbitmq
这个懒人11 小时前
深入解析Translog机制:Elasticsearch的数据守护者
数据库·elasticsearch·nosql·translog
Yan-英杰11 小时前
【百日精通JAVA | SQL篇 | 第二篇】数据库操作
服务器·数据库·sql