您现在的位置是:网站首页> 编程资料编程资料
MySQL脏读幻读不可重复读及事务的隔离级别和MVCC、LBCC实现_Mysql_
2023-05-26
478人已围观
简介 MySQL脏读幻读不可重复读及事务的隔离级别和MVCC、LBCC实现_Mysql_
前言
上一篇文章讲解了MySQL的事务的相关概念MySQL的事务特性概念梳理总结
文章末尾提出了事务因并发出现的问题有哪些?
本篇将着重讲述这个问题的前因后果及解决方式。
事务因并发出现的问题有哪些 脏读
概念:一个事务读取到其他事务未提交的数据。
用一个图来讲解,在并发环境下,多个事务操作同一对象带来的问题:

不可重复读
概念:一个事务在一个时间段内 前后读取的数据不一致,或者出现了修改/删除。

幻读
概念:事务A 按照查询条件读取某个范围的记录,其他事务又在该范围内出入了满足条件的新记录,当事务A再次读取数据到时候我们发现多了满足记录的条数(幻行)
建议大家把幻读记作幻行,以免和不可重复读记混淆

不可重复读与幻读的区别
前提:两者都是读取到已经提交的数据
不可重复读:重点是在于修改,在一个事务中,同样的条件,第一次读取的数据与第二次【数据不一样】(因为中间有其他事务对这个数据进行了修改)
幻读:重点在于新增或者删除,在一个事务中,同样的条件(范围),第一次读取和第二读取【记录条数不一样】(因为中间有其他事务在这个范围里插入、删除了的数据)
我们现在已经知道,原来事务并发会出现,脏读,不可重复读,幻读的问题。
那这些问题我们都是需要去解决的,怎么解决呢?
有兴趣可以看看官网是怎么解释的
链接: 官网地址
事务并发的三大问题其实都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决。
事务的四个隔离级别
我们通过事务的隔离级别来解决不同的问题,那么,不同的隔离级别解决了什么问题呢?
其实sql标准92版 官方都有定义出来
另外,sql标准不是数据库厂商定义出来的,大家不要以为sql语言是什么mysql,sqlserver搞出来的,我们会发现每个数据库语句的sql语句都是差不多的。sql是独立于厂商的!!SQL是Structured Query Language的缩写,本来就属于一种查询语言!!
官网支持四种隔离级别:
# 修改当前会话的隔离级别 # 读未提交 SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; # 读已提交 SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; # 可重复读 SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; # 串行化 SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
我们也可以通过SQL去查询当前的隔离级别
SHOW GLOBAL VARIABLES LIKE '%isolation%'; //全局隔离级别 SHOW SESSION VARIABLES LIKE '%isolation%'; set SESSION autocommit=0; //关闭自动提交
InnoDB默认的隔离级别是RR
事务隔离级别越高,多个事务在并发访问数据库时互相产生数据干扰的可能性越低,但是并发访问的性能就越差。(相当于牺牲了一定的性能去保证数据的安全性)
Read UnCommited 读未提交 RU
多个事务同时修改一条记录,A事务对其的改动在A事务还没提交时,在B事务中就可以看到A事务对其的改动。
结论:没有解决任何问题,存在脏读,因为他就是读取最新的数据。
Read Commited 读已提交 RC
多个事务同时修改一条记录,A事务对其的改动在A事务提交之后,在B事务中可以看到A事务对其的改动。
结论:我就读取你已经提交的事务就完事,解决脏读。
Repeatable Read 可重复读 RR
多个事务同时修改一条记录,这条记录在A事务执行期间是不变的(别的事务对这条记录的修改不被A事务感知)。
结论:RR级别解决了脏读、不可重复读、幻读的问题。
Serializable 串行化
多个事务同时访问一条记录(CRUD),读加读锁,写加写锁,完全退化成了串行的访问,自然不会收到任何其他事务的干扰,性能最低。
结论:加锁排队读取,性能最低。
可以看出,RU与串行化都没啥实用意义,主要还是看RC和RR,那么Mysql是怎么实现这两种隔离级别的呢?
我们要先学习Mysql的两种机制,undo 版本链机制以及read view快照读机制,读已提交和可重复读隔离级别的实现都是建立在这两个核心机制之上。
undo 版本链
undo 版本链就是指undo log的存储在逻辑上的表现形式,它被用于事务当中的回滚操作以及实现MVCC,这里介绍一下undo log之所以能实现回滚记录的原理。
对于每一行记录,会有两个隐藏字段:row_trx_id和roll_pointerrow_trx_id表示更新(改动)本条记录的全局事务id (每个事务创建都会分配id,全局递增,因此事务id区别对某条记录的修改是由哪个事务作出的)roll_pointer是回滚指针,指向当前记录的前一个undo log版本,如果是第一个版本则roll_pointer指向null,这样如果有多个事务对同一条记录进行了多次改动,则会在undo log中以链的形式存储改动过程。

在上图中,最下方的undo log中记录了当前行的最新版本,而该条记录之前的版本则以版本链的形式可追溯,这也是事务回滚所做的事。那undo log版本链和事务的隔离性有什么关系呢?那就要引入另一个核心机制:read view。
read view
read view表示读视图,这个快照读会记录四个关键的属性:
- create_trx_id: 当前事务的
- idm_idx: 当前正在活跃的所有事务id(id数组),没有提交的事务的
- idmin_trx_id: 当前系统中活跃的事务的id最小值
- max_trx_id: 当前系统中已经创建过的最新事务(id最大)的id+1的值
当一个事务读取某条记录时会追溯undo log版本链,找到第一个可以访问的版本,而该记录的某一个版本是否能被这个事务读取到遵循如下规则:
(这个规则永远成立,这个需要好好理解,对后面讲解可重复读和读已提交两个级别的实现密切相关)
- 如果当前记录行的row_trx_id小于min_trx_id,表示该版本的记录在当前事务开启之前创建,因此可以访问到
- 如果当前记录行的row_trx_id大于等于max_trx_id,表示该版本的记录创建晚于当前活跃的事务,因此不能访问到
- 如果当前记录行的row_trx_id大于等于min_trx_id且小于max_trx_id,则要分两种情况:
- 当前记录行的row_trx_id在m_idx数组中,则当前事务无法访问到这个版本的记录 (除非这个版本的row_trx_id等于当前事务本身的trx_id,本事务当然能访问自己修改的记录) ,在m_idx数组中又不是当前事务自己创建的undo版本,表示是并发访问的其他事务对这条记录的修改的结果,则不能访问到。
- 当前记录行的row_trx_id不在m_idx数组中,则表示这个版本是当前事务开启之前,其他事务已经提交了的undo版本,当前事务可访问到。
RR中 Read View是事务第一次查询的时候建立的。RC的Read View是事务每次查询的时候建立的。
Oracle、Postgres等等其他数据库都有MVCC的实现。
需要注意,在InnoDB中,MVCC和锁是协同使用的,这两种方案并不是互斥的。
配合使用read view和undo log版本链就能实现事务之间并发访问相同记录时,可以根据事务id不同,获取同一行的不同undo log版本(多版本并发控制)。
MVCC(Multi-Version Concurrent Control )多版本并发控制
多版本并发控制,是什么意思呢?版本控制,我们在进行查询的时候是有版本的,后续在同一个事务里查询的时候,我们都是使用我们当初创建的快照版本。
比如说嘛,快照,你10岁20岁30岁40岁去照相,你只能看到你之前照相的模样,但是不能看到你未来的模样。
MVCC怎么去实现?
每个事务都有一个事务ID,并且是递增,我们后续MVCC的原理都是基于它去完成。
效果:建立一个快照,同一个事务无论查询多少次都是相同的数据。
一个事务能看见的版本:
- 第一次查询之前已经提交的版本
- 本事务的修改
一个事务不能看见的版本:
- 在本事务第一次查询之后创建的事务(事务ID比我大)
- 活跃中的(未提交)的时候的修改。
下面通过模拟并发访问的两个事务操作,介绍MVCC的实现(具体来说就是可重复读和读已提交两个隔离级别的实现)
可重复读实现
下面模拟两个并发访问同一条记录的事务AB的行为,假设这条记录初始时id=1,a=0,该记录两个隐藏字段row_trx_id = 100,roll_pointer = null
注意:在可重复读隔离级别下,当事务sql执行的时候,会生成一个read view快照,且在本事务周期内一直使用这个read view,下面给出了并发访问同一条记录的两个事务AB的具体执行过程,并解释可重复读是如何实现的(解决了脏读和不可重复读)。

事务A的read view:
create_trx_id = 101| m_idx = [101, 102]|min_trx_id = 101|max_trx_id = 103
事务B的read view:
create_trx_id = 102| m_idx = [101, 102]|min_trx_id = 101|max_trx_id = 103
(ps. 这里因为AB事务是并发执行,因此两个事务创建的read view的max_trx_id = 103)

这里要注意的是,每次对一条记录发生修改,就会记录一个undo log的版本,则在A事务中第二次查询id=1的记录的a的值的时候,B事务对该记录的修改已经添加到版本链上了,此时这个undo log的trx_id = 102,在A事务的read view的m_idx数组中且不等于A事务的trx_id = 101,因此无法访问到,需要在向前回溯,这里找到trx_id = 100的记录版本(小于A事务read view的min_trx_id属性,因此可以访问到),故A事务第二次查询依旧得到a = 0,而不是B事务修改的a = 1。
你可能有疑问,在A事务第二次查询的时候,B事务已经完成提交了,那么A事务的read view的m_idx数组应该移除102才对啊,它存的不是当前活跃的事务的id吗?·
注意:在可重复读隔离级别下,当事务sql执行的时候,会生成一个read view快照,且在本事务周期内一直使用这个read view,虽然102确实应该从A事务的read view中移除,但是因为read view在可重复读隔离级别下只会在第一条SQL执行时创建一次,并始终保持不变直到事务结束。
那么也就明白了,在可重复读隔离级别下,因为read view只在第一条SQL执行时创建,因此并发访问的其他事务提交前改动的脏数据、以及并发访问的其他事务提交的改动数据都对当前事务是透明的(尽管确实是记录在了undo log版本链中) ,这就解决了脏读和不可重复读(即使其他事务提交的修改,对A事务来说前后查询结果相同)的问题!
读已提交实现
还是借助上面事务处理的例子,所有的事务处理流程不变,只是将隔离级别调整为读已提交,读已提交依旧遵守read view和undo log版本链机制,它和可重复读级别的区别在于,每次执行sql,都会创建一个read view,获取最新的事务快照。 而因为这个区别,读已提交产生了不可重复读的问题,下面来分析一下原因:

事务A第一次查询创建的read view:
create_trx_id = 101| m_idx = [101,
