MySQL之MVCC(多版本并发控制)
谈谈数据库隔离级别
谈起MVCC,就不得不说到事务隔离级别,因为MVCC是为了实现数据库的隔离级别,保证事务并发的情况下数据安全的同时还能保持高性能的方式。
在事务并发的场景下会引起脏读,不可重复读,幻读问题,MySQL支持四种隔离级别来解决事务并发中的这些问题。
读未提交:只限制两个数据不能同时修改,但是修改数据的时候,即使事务未提交也可能读到未提交的数据,这种隔离级别仍有脏读,不可重复读,幻读问题
读已提交:当前事务只能读取到其他事务提交的数据,解决了脏读问题,但还是存在不可重复读,幻读问题
可重复读:限制了读取数据的时候,不可以进行修改,所以解决了重复读的问题,但是读取范围数据的时候,是可以插入数据,所以还会存在幻读问题;
序列化:、所有事务都是进行串行化顺序执行的。可以避免脏读、不可重复读与幻读所有并发问题。但是这种事务隔离级别下,事务执行很耗性能。
数据库是如何保证事务的隔离性的
最为简单粗暴的方式就是加锁,不管是读操作还是写操作都加锁当然能保证事务的隔离性,但很显然频繁的加锁,释放锁,在读数据时没办法修改,修改数据时没办法读取大大降低了数据库的性能,所以加锁显然是不可取的
那更为高性能的方式,让我们在读取数据时可以修改,修改数据时可以读取,保证数据库的高性能的方式就是MVCC,数据库隔离级别读已提交、可重复读 都是基于MVCC(多版本并发控制)实现的。
这里的多版本指记录的多版本,还记得我们的undo日志吗?我们每一次对记录的修改都会把旧的版本记录到undo日志中,下面我们说MVCC的具体实现原理
MVCC是如何实现的
MVCC实现组成主要有三个关键部分:
隐藏字段:我们知道innodb存储引擎中每一条行记录都有两个隐藏字段trx_id、roll_pointer,如果表中没有主键和非NULL唯一键时,则还会有第三个隐藏的主键列row_id。
trx_id:每次事务对某条记录进行更新时,都会把该事务的id赋值给trx_id
roll_pointer:每次事务对某条记录进行更新时,都会把旧的版本写入到undo日志中,然后这个隐藏列相当于指针,可以通过它找到之前版本数据信息
row_id:单调递增的行ID,不是必需的(在没有主键且没有唯一非空字段时innodb自动生成的作为主键),占用6个字节。
undo log版本链
多个事务并行操作某一行数据时,不同事务对该行数据的修改会产生多个版本,然后通过回滚指针(roll_pointer),连成一个链表,这个链表就称为版本链 就像下图
这样就保证了我们可以通过回滚指针找到该条记录的旧版本进行回滚或者查询
ReadView
ReadView是解决了什么问题呢
ReadView就是事务A在使用MVCC机制进行快照读操作时产生的读视图。当事务启动时,会生成当前数据库系统快照。innodb为每一个事务构造了一个数组,用来记录并维护系统当前活跃事务ID(启动但未提交的事务)
ReadView的主要属性
createor_trx_id:创建这个ReadView的事务ID
min_limit_id:活跃的事务中最小的事务ID
max_limit_id:系统中应该分配给下一个事务的id值
m_ids:活跃的事务ID列表构成一个list
ReadView的匹配条件
1.如果数据库事务trx_id<min_limit_id,表示生成该版本的事务在生成ReadView前就已经提交所以可以查询到该条记录
2.如果数据库事务trx_id>max_limit_id,表示生成该版本的事务在ReadView生成之后才提交,所以不可以查询到这条记录
3.如果min_limit_id<=trx_id<max_limit_id分为以下三种
1.如果m_ids包含trx_id则代表Read View生成时刻,这个事务还未提交,但是如果数据的
trx_id
等于creator_trx_id
的话,表明数据是自己生成的,因此是可见的。2.如果
m_ids
包含trx_id
,并且trx_id
不等于creator_trx_id
,则Read View生成时,事务未提交,并且不是自己生产的,所以当前事务也是看不见的;3.如果
m_ids
不包含trx_id
,则说明你这个事务在Read View生成之前就已经提交了,修改的结果,当前事务是能看见的。
基于MVCC的查询流程
1.获取当前事务自己id trx_id
2.获取ReadView(不同的隔离级别下获取的时机不同)
3.查询得到的数据和Read View中的事务版本号进行比较
4.如果不符合Read View的可见性规则, 即就需要Undo log中历史快照
5.最后返回符合规则的数据
InnoDB 实现MVCC,是通过
Read View+ Undo Log
实现的,Undo Log 保存了历史快照,Read View可见性规则帮助判断当前版本的数据是否可见。
获取Read View的时机:
在读已提交的隔离级别下:事务中每一次select都会生成一次Read View,会导致不可重复读问题
在可重复的的隔离级别下:只有事务中第一次select会生成Read View,由于每一次select都会复用旧的Read View所以不存在不可重复读问题
如何解决幻读问题
假设表中只有student一条数据
现在事务A B并发事务A的id为20,事务B的id为30
步骤一:事务A开启第一次select语句
select *from student where id>=1
在开始执行select之前,MySQL会为事务A生成一个ReadView,则此时ReadView的内容是m_ids=[20,30],min_liimit_id=[20],max_limit_id=[31],creator_trx_id=[20]
由于此时表中只有一条数据且满足查询条件,因此接下来根据ReadView机制判断数据能否被查询到,此时数据的trx_id<min_liimt_id,则表示该条数据在ReadView之前已经提交所以可以查询到,第一次查询结果为“张三”
步骤二:事务B(trx_id=30)插入两条数据,并提交事务
insert into student(id,name) values(2,'李四');
insert into student(id,name) values(2,'王五');
则此时的undo如下图
步骤三:此时事务开始第二次查询,,根据可重复读隔离级别不会再生成ReadView,内容是m_ids=[20,30],min_liimit_id=[20],max_limit_id=[31],creator_trx_id=[20],此时表中三条数据都满足查询条件,则开始ReadView机制判断数据是否可见
首先id=1,可见
id=2,它的trx_id=30,在min_limit_id和max_limit_id之间,因此判断trx_id在是否在m_ids中,显然在则不能查询到该记录
id=3同理
总结:两次查询结果相同,所以在RR隔离级别下解决了幻读问题
MVCC小结
MVCC解决了什么问题
1.读写之间的阻塞问题:MVCC可以使读写互不阻塞,提高并发能力
2.降低了死锁的概率:这是因为MVCC读取数据时不需要加锁,对于写操作也只是进行行锁
3.解决了快照读问题:解决了快照读问题
MVCC是在RC和RR这两种隔离级别的事务在执行快照读操作访问版本链查询到的对当前事务可见的数据