什么是多版本并发控制?其实这个主要目的,就是为了解决在数据库高并发下的读写冲突。在此之前有两个概念一个为快照读,一个为当前读。
当前读:例如update、delete、insert这些DML语句,为了读取到最新的数据,会对读取的记录进行加锁读取,保证其他事务不能对其进行修改。
快照读:像select这样的读取,即快照读,不加锁不阻塞,但是前提是,非串行化隔离级别下,MVCC多版本并发控制,由多个版本进行控制,不同的事务读取的可能是多个版本,不一定是读取的最新数据,但是相互之间能尽量不受到影响。
1 MVCC如何实现的?
MVCC实现方式主要由三大部分组成:
(1)数据行的三个隐藏字段
(2) undo log
(3) Read view(读视图)
2 数据行的三个隐藏字段
数据行在我们原始列的基础上,还有三个隐藏的字段:
(1)DB_TRX_ID
(2)DB_RALL_PTR
(3)DB_ROW_ID
2.1 DB_TRX_ID
DB_TRX_ID,即事务ID,每个事务对数据行进行修改后,都会记录此事务的ID,例如我们开启事务后,分配的事务id为1,对t1表进行插入,字段为name=a,age=10。在插入后字段为name 列为a,age为10,而隐藏字段DB_TRX_ID,就会等于1。如果此时事务2进来,对此行记录进行修改,那么此时在修改的过程中,隐藏字段DB_TRX_ID就会变成了2,随后提交。
2.2 DB_RALL_PTR
DB_RALL_PTR,这个隐藏字段主要存储的为undo log中 rollback segment 旧版本记录的指针,还是上方的例子。分配的事务id为1,对t1表进行插入,字段为name=a,age=10。在插入后字段为name 列为a,age为10,而隐藏字段DB_TRX_ID,就会等于1。而DB_RALL_PTR因为是新纪录插入,假设回滚记录为null,或者是回滚后为删除此条记录,如果此时事务2进来,对这行记录进行update,将age修改为了11,那么此时name列为a,age列为11,DB_TRX_ID列就等于2了,而DB_RALL_PTR记录的则是insert插入的那条数据,undo log中的记录。但是其中有一点概念啊,insert与其他dml语句不同,在插入后commit后,很大概率就会从undo log中删除了。这里只是为了举例DB_RALL_PTR的记录。
2.3 DB_ROW_ID
什么是DB_ROW_ID,他的作用其实是在我们表没有主键的情况下,生成的隐藏主键列,然后用这个列去建立的聚集索引。他在这里,主要的作用其实是与DB_RALL_PTR的配合,DB_RALL_PTR存储的指针,其实就是undo log中 rollback segment中的历史版本记录的的DB_ROW_ID。所以我们可以把上方的图进行一个完善。就是如下的样子:
小知识:其实还有一个flag的隐藏字段,也提一嘴,他的作用是,我们delete某条数据的时候,并不是真的把数据删除了,而是在flag字段上打上了一个被删除的标签。
3 MVCC中undo log的作用
undo log我们都知道,是回滚日志,为什么在MVCC中也被用上了是怎么用上的呢?前面页提到了,MVCC快照读取记录的时候,不一定是最新的记录。这个不一定最新的记录指的就是去undo log中读取的记录。undo log又分为insert undo和update undo。两种也是存在一定的区别的。
insert undo:顾名思义,就是insert事务插入时产生的undo日志,这部分日志在insert语句提交后,基本上也就从undo log中被丢弃了。
update undo:这种类型的undo,主要是update语句和delete语句产生的。delete或者update语句commit之后,undo日志不一定会被立即丢弃。有可能会等着给MVCC进行使用。但是为了节省磁盘空间,还会有一个专门的purge线程进行负责清理这部分数据。
小知识:undo log 的purge线程是如何清理数据的呢?其实当我们对一个记录进行delete后,不一定会被数据库立即删除,就像刚才提到的flag隐藏字段,只是将delete的这条记录打上了一个deleted_bit 等于true的标签,当打上标签的记录,被purge盯上后,purge会根据与read view进行比较后,再决定是否丢弃这部分数据。
4 Read view
什么是read view,从名称可以知道是读视图,简单说,他其实就是当事务进行快照读时,所产生的读视图。当一个事务开启快照读后,就会对当前数据库系统生成一个快照,这个快照会记录当前系统,所有活跃的事务ID,并进行维护,这就是读视图,事务ID为自增的,也就是说最大的事务ID就能代表最新的事务。
而MVCC的实现方式,就是通过读视图维护的事务ID与记录中隐藏字段的事务ID进行比较,来判断当前事务对该记录的可见性。从而实现的多版本并发控制。
4.1 Read view由几个关键的部分组成:
m_ids:在生产读视图时,当前系统活跃的事务id列表,即没有提交的事务id。
Min_trx_id:表示当前列表中,活跃的最小的事务id
Max_trx_id:表示当前列表中(最大的事务id+1),也就是下一次系统分配的事务id。
Creator_trx_id:表示生成该read view的事务id。当前事务id
4.2 Read view比对可见性的方法
上面我们理解了read view还有隐藏字段,以及undo log,那么他们配合后是如何进行比对,可见性,实现多版本并发控制呢?分为四种情况:
第一种:当TRX-id与当前开启快照读的事务id一致,可以断定,是自己修改自己读,可以见。
第二种:当TRX-id小于当前read view种最小的活跃事务id,表示这个记录早就被修改提交了,所以也可以直接获取最新记录
第三种:如果说,当TRX-id大于当前read view种最大的事务id+1,那则表示,这行记录是在你开启快照都之后被修改了。所以不可见,需要根据行记录种的RALL-PTR去undo log中找合适你这个事务id的数据
第四种:即不小于,也不大于等于,也不是当前事务id,那就要判断,这个trx-id的事务,是不是还在活跃列表中,如果在,那就是还没提交不可见,需要去找undo,如果不在,表示他已经提交了。可以获取数据。
4.3 举例
下方有五个事务,事务3和事务4开启了快照读,而事务5修改了这个记录并提交退出了了。那么快照都应该比较呢?还是这张表:
那么此时表会变成这样:
事务3:当事务开启了快照读,生成了读视图,当前事务3的trx_id为3,而现在read view列表中,活跃的id有,id1,id2,id3,id4。最小的id为1,最大的id为5+1,也就是6,首先与最小的进行比较,3是否小于1,不小于,下一个判断,是否大于等于6,也不大于,在进行判断事务id5,是否存在与列表中,如果在表示数据还没提交,不能读取,需要按照当前事务id3去undo log找历史版本的记录,如果不在,表示事务已经提交了,那么可以直接进行读取最新记录,也就是age=1。从上方图可以看出,事务5已经提交了,所以我们可以直接读取最新记录。
事务4:与事务3原理一样
另一种情况:如果此时来了一个事务7修改了这条记录还没提交:
然后开启快照都后,read view活跃事务列表是这样的:
随后事务8,拿着trx_id 7进行与活跃列表比较,不小于4,下一个判断条件,是否大于等于9,不等于,判断是否存在7活跃列表,发现存在,那么不能获取当前记录,因为猜测事务7还没提交,所以只能获取上一个版本的信息。即age=1的数据。
4.4 MVCC原理图
5 MVCC与隔离级别的关系
在RC与RR不同的隔离级别下,进行快照读取的情况可能存在不同。RC为读已提交,只能读取到提交的数据,但是可能存在不可重复读和幻读,RR,可重复读级别,可能造成幻读。
MVCC快照读取在RC隔离级别下,每次快照读,都会生成一个新的read view,这样的话就可能导致,两次读取的数据不一致。但是在RR隔离级别下,相同的事务,只有第一次快照读的时候会产生read view,一直沿用,所以不会出现不可重复度的情况。