MySQL-InnoDB-MVCC多版本并发控制
1.基本概念:
MVCC :应对高并发事务, MVCC比单纯的加锁
更高效;
-
多版本控制,是一种提高并发的技术,最早的数据库系统,只有读和读之间可以并发,读和写,写和读,写和写之间都要阻塞,引入多版本之后,只有写写之间相互阻塞,其他三种操作都可以并行。
- MVCC 用于解决并发事务访问数据库的时候,可能出现的脏读,不可重复读和幻读
-
MVCC主要是为了提升数据库的并发性能 而设计的,其中采用了更好的方式处理了读写的并发冲突,做到即使有读写冲突的时候,可以实现并发执行,从而提升并发能力,确保了任何时刻的读操作都是非堵塞的
-
MySql的大多数的事务性存储引擎实现的都不是简单的行级锁,基于提升并发性能的考虑,它们一般都同时实现了多版本并发控制(MVCC),也就是说每个数据库实现MVCC的方法不一样;
-
同样我们可以认为MVCC是行级锁的一个变种,但是在很多情况下避免了加锁的操作,因此开销也会很低
-
MVCC的实现方式有多种: 乐观锁, 悲观锁
-
MVCC只有在
READ COMMITTED
(读已提交)和REPETABLE READ
(可重复读) 两个隔离级别下工作,其他的隔离级别够,但是和MVCC不兼容,因为READ UNCOMMITTED
(读未提交) 总是读取最新的数据行,而不是符合当前事务版本的数据行,而SERIALIZABLE
(可串行化)则会对所有读取的行加锁 :-
读未提交级别,不适用MVCC:
既然可以读取未提交的数据,也就是最低的隔离级别,那就是允许脏读的出现,那么,我完全可以去读取最新的数据,所以也就无需使用 mvcc
-
可串行化 , 不适用MVCC:
这个隔离级别 是最高的隔离级别,也就是说,没有什么并发的事务进行处理,他要求,事务一个一个排好队,一个一个执行,不存在所谓的多线程并发问题。
-
并发事务去访问数据库的时候可能出现的一些问题:
脏写、脏读 :
脏写: 两个事务,事务A和事务B,事务A先把它更新为A值,事务B紧接着把他跟新为B值
但是此时A进行了回滚操作,B去读取的时候,欸??? 我更新的值不见了
也就是说,我刚才明明写了一个数据值,结果过了一会却没了。而它的本质就是事务 B 去修改了事务 A 修改过的值,但是此时事务 A 还没提交,所以事务 A 随时会回滚,导致事务 B 修改的值也没了,这就是脏写的定义。
脏读:
事务 A 更新了一行数据的值为 A 值,此时事务 B 去查询了一下这行数据的值,看到的值是 A 值,
然后B拿着A的值做了一些列的操作,然后A突然给回滚了,然后事务B去查询这个数据的时候,发现是null值
不可重复读 :
事务A开启了,他会对一条数据进行查询操作,然后呢,有另外两个事务B和事务C进行更新操作,我们必须规定一下,事务A只有在BC提交之后才能读取到那个数据,避免脏读
假设缓存页里一条数据原来的值是 A 值,此时事务 A 开启之后,第一次查询这条数据,读取到的就是 A 值。接着事务 B 更新了那行数据的值为 B 值,同时事务 B 立马提交了然后事务 A 此时还没提交。它在事务执行期间第二次查询数据,此时查到的是事务 B 修改过的值,B 值,因为事务 B 已经提交了,所以事务 A 是可以读到的,紧接着事务 C 再次更新数据为 C 值,并且提交事务了,此时事务 A 在还没提交的情况下,第三次查询数据,查到的值为 C 值。
明显 A 值是不可重复读的。因为事务 B 和事务 C 一旦更新值并且提交了,事务 A 会读到别的值,所以此时这行数据的值是不可重复读的。
幻读 :
你一个事务 A,先发送一条 SQL 语句,里面有一个条件,要查询一批数据出来,如
SELECT * FROM table WHERE id > 10
。然后呢,它一开始查询出来了 10 条数据。接着这个时候,别的事务 B往表里插了几条数据,而且事务 B 还提交了,此时多了几行数据接着事务 A 此时第二次查询,再次按照之前的一模一样的条件执行
SELECT * FROM table WHERE id > 10
这条 SQL 语句,由于其他事务插入了几条数据,导致这次它查询出来了 12 条数据。
2. MVCC的根本目标:提升并发能力
在MVCC机制中,数据库中的每个数据行都可以存在多个版本,并且每个事务看到的数据版本可能不同。具体来说,MVCC机制通过以下方式实现并发控制:
-
版本控制 :每当对数据库中的数据进行更新操作 的时候,不是直接覆盖原始数据,而是创建一个新的数据版本,并将新版本的数据与事务的时间戳关联起来;
-
快照读取:在MVCC中,读取操作不会阻塞写操作,也不会阻塞其他读取操作,事务可以读取数据库的快照,即某个时间点之前的数据版本,而不会收到其他事务的影响
-
可见性判断:在进行读取操作的时候,事务只能看到被提交之后的数据版本,而看不到其他事务正在修改的数据,这样可以避免脏读和不可重复读的问题
-
回滚操作: 当事务回滚的时候,不会对数据库的数据进行删除和修改,而是先把我们的所标记的一个数据版本设置为无效,使得其他事务无法看到该版本
3 事务并发处理的四大场景
1. 读读
读-读场景即是指多个事务/线程在并发读取一个相同的数据,比如事务T1
正在读取ID=16
的行记录,事务T2
也在读取这条记录,两个事务之间是并发执行的。
MySQL
执行查询语句,绝对不会对引起数据的任何变化,因此对于这种情况而言,不需要做任何操作,因为不改变数据就不会引起任何并发问题。
2. 写写
也就是指多个事务之间一起对同一数据进行写操作
事务T1
对ID=16
的行记录做修改操作,事务T2
则对这条数据做删除操作,事务T1
提交事务后想查询看一下,结果连这条数据都不见了,这也是所谓的脏写问题,也被称为更新覆盖问题,
3.读写 写读
读-写、写-读实际上从宏观角度来看,可以理解成同一种类型的操作,但从微观角度而言则是两种不同的情况,
-
读-写是指一个事务先开始读,然后另一个事务则过来执行写操作,
-
写-读则相反,主要是读、写发生的前后顺序的区别。
4.解决方案
对于写-写、读-写、写-读这三类存在线程安全问题的场景,最为简单粗暴的方式,通过 加锁 的方案确保线程安全。
但是:加锁会导致部分的串行化、整体串行化,因此效率会下降
MVCC
机制,在读-写并存(读-写、写-读)的场景,使用局部无锁架构,提升性能。
MVCC 机制 在线程安全问题和加锁串行化之间做了一定取舍,让两者之间达到了很好的平衡,即防止了脏读、不可重复读及幻读问题的出现,
又无需对并发读-写事务加锁处理。
4 局部无锁架构COW思想:COW思想 专门用于优化读的次数远大于写次数的场景
Copy-On-Write :
当多个线程需要对共享数据进行修改的时候,不直接在原始数据上修改,而是 把原始数据先复制一份,然后再副本上进行write
通过操作 写操作副本,引入了局部无锁架构,解决并且处理了数据冲突,提高了并发性能
具体实现步骤:
-
读取数据 : 多个线程去读取共享数据的时候,他们可以访问原始数据,不需要去复制
-
写入数据: 多个线程去修改共享数据的时候,首先会将原始数据进行一个复制,然后再副本上继续进行修改,其他读线程可以继续读取原始数据,不受写入线程的影响
-
更新引用: 当写入结束的时候,会更新共享数的引用,使得其他线程后续访问的时候可以获取到更新之后的数据
优缺点:
-
线程安全: 通过复制数据副本再副本上进行操作,避免了多线程并发修改原始数据时的数据冲突问题,从而提高了安全性
-
节省内存: 我们发现只有再写入的时候才会对共享数据副本进行复制,而进行读操作的时候,我们可以直接去读取共享数据
-
减少锁竞争:同样 由于我们不给读取共享数据的时候枷锁,所以会减少锁竞争吗,提高并发性能
-
因为写操作的时候,更新引用的时候,都需要创建副本,所以对内存的消耗性能会有影响
-
并且每次写操作都会有一次copy,所以只适合读大于写的情况 例如java中:
CopyOnWriteArrayList写时复制容器
jdk1.5以后并发包中提供的一种并发容器,写操作 通过创建底层数组的新副本 来实现,是一种读写分离的并发策略,我们也成为"写时复制容器"。
java
public boolean add(E e) {
//加锁,对写操作保证线程安全
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
//拷贝原容器,长度为原容器+1
Object[] newElements = Arrays.copyOf(elements, len + 1);
//在新副本执行添加操作
newElements[len] = e;
//底层数组指向新的数组
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
MVCC如何使用Copy-On-Write思想呢?借鸡生蛋
顾名思义,undo log是一种用于撤销回退 的日志,在事务没提交之前,MySQL会先记录更新前的数据到 undo log日志文件里面,当事务回滚时或者数据库崩溃时,可以利用 undo log来进行回退。
事务要实现ACID,其中的原子性,一致性主要靠undo-log数据副本实现,undo-log就是重做日志,一个事务一个undo-log日志副本,多个事务的 undo-log 日志副本 (数据快照),组成了一个 副本链,如下下图:
5 MVCC 与 锁的关系:
1.MVCC :
-
MVCC 通过 维护 **数据的多个版本 来实现并发控制,允许事务并发访问数据库而不会发生阻塞
-
在MVCC中,读取操作不会阻塞写入操作,也不会阻塞其他读取操作,每个事务可以看到一个一致性的数据快照,而不受其他事务影响。
-
MVCC 主要 用于读取操作的并发控制,可以有效的避免脏读,不可重复读,和幻读等并发问题
2.锁:
-
锁是一种悲观并发控制机制,通过事务访问数据时,对数据进行加锁,以防止其他事务对该数据进行修改和读取。
-
在使锁进行并发控制的时候,可能会出现阻塞或者死锁的问题,特别是在高并发的情况下,锁的颗粒度过大都会或者锁的竞争激烈都会对性能造成影响。
3 两者关系:
-
MVCC是一个乐观的并发控制机制,通过维护多个副本的数据来实现并发控制,也不需要给数据加特定的行锁
-
锁是一个悲观的并发控制机制,通过事务访问数据,对数据进行加锁,以防止其他事务对该数据进行修改删除和读取
-
很多时候,MVCC和锁可以结合使用,实现更加细粒度的并发控制,提高系统性能和并发能力
6 事务的ACID 特性
1 什么是事务:
是数据库管理系统执行的一个逻辑单位, 它有一个有限的数据库操作序列构成
这些操作要不都执行,要么都不执行 是一个不可分隔的工作单位
事务的目的就是 确保数据的一致性和完整性,它通过一系列的操作,将数据库从一个一致性状态转换到另一个一致性的状态上,
2 ACID :
A: 原子性: 事务作为一个整体执行,包含在其中的对数据库的操作要么全部执行,要么全部不执行
C: 一致性: 事务必须使数据库从一个一致状态变换到另一个一致性状态,也就是说,一个事务的执行不能破环数据库的完整性和一致性
I: 隔离性: 事务的执行不受其他事务的干扰,事务执行中间的结果对其他事务是不可见的
D: 持久性 : 事务一旦提交,其结果就是永久性的,即使系统崩溃也不会丢失
3事务的隔离级别
-
读已提交 : oracle数据库中是默认的
-
只允许读取并发事务已经提交的数据
-
这个级别可以防止脏读,可能会导致幻读,不可重复读
-
在这个状态,每个事务只能看到他开始时候的数据状态,以及他提交时其他事务所作的提交
-
-
读没提交 :这是最低的隔离级别
-
允许读取尚未提交的数据变更。
-
这是最低的隔离级别,它可以导致脏读,不可重复读,和幻读
-
在这个级别,一个事务可以读取到另一个事务尚未提交的修改, 这可能导致数据的不一致性
-
-
可重复读 :这是Mysql默认的隔离级别
-
他确保在同一个事务中多次读取同一个数据时,看到的是相同的数据版本,即使其他事务在此期间修改了这些数据。
-
尽管可以避免脏读和 不可重复读,但在这个级别下可能出现幻读,(即在一个事务中,两次相同的查询可能会返回不同的结果集,可能其他事务在此期间插入了新的记录
-
-
可串行化 :这是最高的隔离级别。
-
它通过强制事务的串行执行来避免,脏读,不可重复读,幻读
-
在这级别下,每个事务进行串行执行的时候,都会完全锁定表的数据,来保证事务的一致性,但是会造成性能下降
-
7 隔离级别、并发性、数据一致性的三角之间关系
-
并发性:
-
并发性是指数据库系统同时处理多个事务的能力,隔离级别越低,允许的并发操作越多,系统的并发性能也就越高
-
但是,过高的并发操作,可能会导致事务之间的相互干扰,产生一些并发问题,如脏读,不可重读,幻读等问题
-
数据的一致性也就越低。
-
-
数据一致性:
-
是指,事务执行后,数据库的数据是一致的,隔离级别越高,数据一致性也就越强,但对并发操作的限制也是最严格的
-
高隔离级别可以防止一些并发问题的产生,如脏读,不可重读,幻读,但会降低系统的并发性能
-
7 MVCC 的三个核心组件
MVCC
机制主要通过三个组件实现:
1 隐藏字段:
在Innodb存储引擎中,每一行记录中都有隐藏字段:
-
在有聚簇索引的情况下每一行隐藏三个字段
-
DB_TRX_ID:记录创建这条数据上次修改它的事务 ID,
-
DB_ROLL_PTR:回滚指针,指向这条记录的上一个版本
-
deleted_bit字段,即记录被更新或删除,这里的删除并不代表真的删除,而是将这条记录的delete flag改为true
-
-
在没有聚簇索引的情况下每一行隐藏四个字段。
2 Undo-log日志:用于实现事务的原子性、隔离性、一致性
-
事务的回滚操作 :
当一个事务执行过程中发生错误或者被用户显式回滚时,数据库系统需要能够撤销该事务已经执行的操作 将数据库恢复到事务开始之前的状态。这就是回滚操作。
undo log记录了事务执行过程中所做的所有修改操作的逆操作,通过undo log可以快速回滚事务所做的修改,从而保证事务的原子性。
-
恢复和崩溃恢复 :
当数据库系统发生崩溃或者异常关闭时,可能会导致部分事务未提交的修改操作丢失或者部分已提交的修改操作未持久化到磁盘。
通过undo log,数据库系统可以在恢复过程中, 将未提交的修改操作回滚 ,并将已提交但未持久化的修改操作重新应用到数据库中,从而保证数据库的一致性和完整性。
3 ReadView:
当一个事务在尝试读取一条数据时,MVCC
基于当前MySQL
的运行状态生成的快照,也被称之为读视图,即ReadView
在MVCC中,每个事务 可以有一个特定的时间戳或者版本号 ,而通过对比事务的时间点所能看到的数据版本的集合。
熟悉一下时间戳和版本号的对比规则:
已提交数据,事务只能看到已经提交的数据版本,即如果某个数据版本的提交时间早于当前事务的开始时间,则 该数据版本对当前事务是可见的
未提交数据: 事务不应该看到其他事务尚未提交的数据版本,即 如果某个数据版本的提交时间晚于事务的开始时间则该数据版本对事务是不可见的
事务开始时间: 事务开始时间是确定事务Read-view的关键因素,事务只能看到在他开始时间之前已经提交的数据版本
数据快照: 事务读取快照的时候,read-view应该是一个一致的数据快照,即事务开始时刻的数据库状态的一个一致性快照。这样可以确保事务读取的数据是在一个一致的时间点获取的。
给大家弄一个例子,展示一个下面的场景:
上图中,事务2,事务3,事务5的快照版本,事务4的是不可以看到的。对于事务4来说,可以看到的数据版本,是事务1的已经提交的数据
上面是通过时间比对来的,但是 mysql 的MVCC不是通过对比时间戳来实现的。
MVCC 使用 一个新的组件,read-view + 一组对比规则,来计算可见版本
read-view 有一些列的对比规则,这些规则用于确定一个事务在读取数据时,如何在数据据库中与其他事务id进行比较,确定它能看到的数据版本 【通常而言,一个事务与一个ReadView
属于一对一的关系】
当执行一个select 语句的时候,MVCC会产生一致性试图(read-view)那么找个read view 没有记录事务的开始时间和截止时间,而是换成另一种方式去记录开始时间和截至时间,:
-
read view 记录当前活跃事务id,组成活跃事务id数组,这个属性的作用,就是这样生成的时候,这些个事务id正在活跃,也就是还没有提交,是我们不可见的
-
read view 记录当前最小活跃事务id,也就是这些个事务是已经提交了的,我们是可见的
-
read view 记录当前的下一个事务id,这个属性的作用,是用于判断哪些事务是未来事务,也是不可见的
ReadView 一般包含四个属性:
属性 | 描述 |
---|---|
creator_trx_id | 代表的是创建当前的这个ReadView 的事务 ID |
trx_ids | 表示在生成当前的ReadView时,系统内活跃的事务id列表,也就是未提交的事务id,他的数据结构是一个List |
up_limit_id | 活跃事务列表中,trx_ids中,最小的事务id,如果trx_ids为空,则up_limit_id 为 low_limit_id |
low_limit_id | 表示在生成当前ReadView时,系统要给下一个事务分配id值, |
例如: 我们假设目前数据库中共有T1~T6
这6个事务,T1、T2、T4、T6
还在执行,T3
已经回滚,T5
已经提交,
此时当有一条查询语句执行时,就会利用MVCC
机制生成一个ReadView
,由于在MySQL中单纯由一条select
语句组成的事务并不会分配事务ID
,因此默认为0
,所以目前这个ReadView的信息如下:
ReadView 的读取规则:
-
如果被访问的版本的
事务ID = creator_trx_id
,表示当前事务访问的是自己修改过的记录,那么该版本对当前事务可见, -
如果被访问的版本的
事务ID < up_limit_id
,表示当前事务也已经被提交了,所以该版本可以被当前事务所提交 -
如果被访问版本的
事务ID > low_limit_id
值,那么表示生成该版本的事务在当前事务生成 ReadView 后才开启,所以该版本不可以被当前事务访问。 -
如果被访问版本的
事务ID在 up_limit_id和m_low_limit_id
之间,那就需要判断一下版本的事务ID是不是在 trx_ids 列表中,-
如果在,说明创建 ReadView 时生成该版本的事务还是活跃的,该版本不可以被访问;
-
如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。
-
ReadView的生成规则
在mysql的mvcc中只有在 可重复读 和 读已提交 两个事务的隔离级别下才有效:
-
在 可重复读中,ReadView会在事务中第一次执行select 语句执行的时候生成,只有在本事务中对数据进行更改才会更新快照
-
在 读已提交 中,每次Select 中都会重新生成一个 ReadView 总是读取最新版本数据
-
两者的区别在于:
-
在 读已提交 隔离级别下,是每个select 都会创建最新的ReadView;
-
在 可重复读 的级别下,则是当事务中的第一个select 请求才创建 ReadView
-
8 MVCC实现 ------总结
原理:
-
当一个事务尝试改动 某条数据的时候,会将原本表中的旧数据 放到 Undo-log日志中
-
当一个事务尝试查询某条数据的时候,MVCC 会生成一个ReadView快照。
-
Undo-log 主要实现 数据的多版本, 而ReadView 实现多版本的并发控制
通过例子 来看看具体的流程:
sql
-- 事务T1:trx_id=1
UPDATE user_info SET name = "小夏" WHERE id = 1;
UPDATE user_info SET sex = "女" WHERE id = 1;
sql
-- 事务T2:trx_id=2
SELECT * FROM user_info WHERE id = 1;
如上述两个事务T1,T2, T1是要修改id =1 的这条数据,而T2 则准备查询这条数据,那么T2在执行时具体的过程如下:
-
- 当事务中出现select 语句时,会根据mysql 的当前情况生成一个ReadView;
-
- 判断行数据中隐藏列的 trx_id 与ReadView.creat_trx_id 是否相同?
-
相同: 说明是同一个 事务,可以读取最新的数据
-
不同:代表要查询的数据 ,是别的事务进行修改的,继续执行:
-
- 判断 隐藏列 的 trx_id 与 ReadView.up_limit_id 最小活跃id
-
小于: 说明该事务在其他事务活跃的时候,就已经提交了,可以访问到最新数据
-
不小于: 说明 目前的数据正在被修改把,需要进行判断:
-
- 判断 隐藏列 trx_id 和ReadView.low_limit_id 进行比较:
-
大于或等于: 代表改动行数据的事务,生成快照之后才开启的,不能访问最新版的数据
-
小于: 说明他会介于 ReadView.up_limit_id 和 ReadView.low_limit_id 之间,需要进一步判断
-
- 在上述小于的情况下我们要进一步判断:
-
在之间,说明还是属于活跃事务,现在正在被别的 事务所修改,不能访问新的数据
-
不在,表示改动行数据的事务已经结束, 可以查询到最新的数据
-
- 经过上述一系列的操作,可以得知 目前查询数据的事务到底能不能访问最新的数据:
-
如果能访问到最新数据,那么之间拿到表中的数据返回就行了
-
如果不能访问最新数据,在Undo-log回滚日志中,获取旧版本的数据返回。
总结:
MVCC
多版本并发控制,其中的多版本主要依赖Undo-log
日志来实现,而并发控制则通过表的隐藏字段
+ReadView
快照来实现,通过Undo-log
日志、隐藏字段
、ReadView
快照这3点,就实现了MVCC
机制。