MVCC
MVCC(Multiversion Concurrency Control) ,多版本并发控制技术。
原理是,通过数据行的多个版本管理来实现数据库的并发控制,简单来说就是保存数据的历史版本。可以通过比较版本号决定数据是否显示出来。读取数据的时候不需要加锁可以保证事务的隔离效果。
一般解决不可重复读和幻读问题,是采用锁机制实现,有没有一种乐观锁的问题去处理,可以采用MVCC机制的设计,可以用来解决这个问题。取代行锁,降低系统开销。
MVCC 可以解决什么问题
数据库在在并发访问时的读写效率
- 读写之间阻塞的问题,通过 MVCC 可以让读写互相不阻塞,读不相互阻塞,写不阻塞读,这样可以提升数据并发处理能力。
- 降低了死锁的概率,这个是因为MVCC采用了乐观锁的方式,读取数据时,不需要加锁,写操作,只需要锁定必要的行。
- 解决了一致性读的问题,当我们朝向某个数据库在时间点的快照是,只能看到这个时间点之前事务提交更新的结果,不能看到时间点之后事务提交的更新结果。
什么是快照读
快照读,读取的是历史版本的记录,不加锁的简单Select都属于快照读.
SELECT * FROM player WHERE ...
什么是当前读
当前读就是读的是最新数据,而不是历史的数据,加锁的SELECT,或者对数据进行增删改都会进行当前读。
SELECT * FROM player LOCK IN SHARE MODE;
SELECT FROM player FOR UPDATE;
INSERT INTO player values ...
DELETE FROM player WHERE ...
UPDATE player SET ...
实现原理
MVCC的实现原理主要依赖:记录中的三个隐藏字段 + undolog + read view来实现的。
排它锁 + undolog + 版本事务链 + 一致性read view + 版本事务链匹配规则。
隐藏字段
每行记录除了我们自定义的字段外,还有数据库隐式定义的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID等字段:
DB_TRX_ID:6字节,创建或最后一次修改该记录的事务id
DB_ROW_ID:6字节,隐藏主键,如果数据库没有主键,那么Innodb会自动生成一个6字节的row_id
DB_ROLL_PTR:7字节,回滚指针,指向这条记录的上一个版本,用于配合undolog日志
undo log
回滚日志,表示进行insert、delete、update操作的时候产生方便回滚的日志。
当进行insert操作时,产生的undolog只在事务回滚的时候需要,并且事务提交后可以被立即丢弃
当进行update、delete操作的时候,产生的undolog不仅仅在事务回滚的时候需要,在快照读的时候也需要,所以不能随便删除,只有在快照读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除(当数据发生更新和删除操作的时候都只是设置一下老记录的deleted_bit,并不是真正的将过时记录删除,因为为了节省磁盘空间,innodb有专门的purge线程来清除deleted_bit为true的记录,如果某个记录的deleted_id为true,并且相对于purge线程的read view可见,那么这条记录一定时可以被清除的)
下面看一下undolog生成的记录链
1.假设有一个事务编号为1的事务向表中插入一条记录,那么此时数据行的状态为:
2.假设有第二个事务编号为2对该记录的name做出修改,改为lisi
在事务2修改改行记录数据时,数据库会对该行加上排它锁
然后把该行数据拷贝到undolog中,作为旧记录,即在undolog中有当前行的拷贝副本
拷贝完毕后,修改改行name为lisi,并且修改隐藏字段的事务id为当前事务2的id,回滚指针指向拷贝到undolog的副本记录中
事务提交后,释放锁
3.假设有第三个事务编号为3
从上述的一系列图中,可以发现,不同事务或者相同事务对同一记录的修改,会导致该记录的undolog生成一条记录版本线性表,即链表,undolog的首链就是最新的旧记录,尾链就是最早的旧记录
Read View
Read View 是事务进行快照读操作的时候生产的读视图,在该事务执行快照读的那一刻,会生成一个数据系统当前的快照,记录并维护系统当前活跃事务的id ,事务的id 值是递增的。
其实 Read View的最大作用是用来做可见性判断的,也就是说当某个事务在执行快照读的时候,对该记录创建一个Read View 的视图,把它当作条件去判断当前事务能够看到哪个版本的数据,有可能读取到的是最新的数据,也有可能读取的是当前行记录的undolog中某个版本的数据
Read View遵循的可见性算法主要是将要被修改的数据的最新记录中的 DB_TRX_ID (当前事务id)取出来,与系统当前其他活跃事务的id去对比,如果DB_TRX_ID 跟 Read View 的属性做了比较,不符合可见性,那么就通过 DB_ROLL_PTR 回滚指针去取出 undolog 中的 DB_TRX_ID 做比较,即遍历链表中的 DB_TRX_ID ,直到找到满足条件的 DB_TRX_ID ,这个 DB_TRX_ID在的旧记录就是当前事务能看到的最新老版本数据。
Read View 的可见性规则如下所示:
首先要知道 Read View 中的三个全局属性:
trx_list:一个数值列表,用来维护 Read View 生成时刻系统正活跃的事务ID (1,2,3)
up_limit_id:记录trx_list列表中事务旧最小的ID (1)
low_limit_id:Read View生成时刻系统尚未分配的下一个事务ID(5)
具体的比较规则如下:
1、首先比较 DB_TRX_ID < up_limit_id,如果小于,则当前事务能看到DB_TRX_ID所在的记录,如果大于等于进入下一个判断
2、接下来判断DB_TRX_ID >=low_limit_id ,如果大于等于则代表DB_TRX_ID所在的记录在ReadView 生成后オ出现的,那么对于当前事务肯定不可见,如果小于,则进入下一步判断
3、判断 DB_TRX_ID 是否在活跃事务中,如果在,则代表在 Read View 生成时刻,这个事务还是活跃状态,还没有 commit ,修改的数据,当前事务也是看不到,如果不在,则说明这个事务在 Read View 生成之前就已经开始 commit ,那么修改的结果是能够看见的。
假设有四个事务同时进行:
事务1 | 事务2 | 事务3 | 事务4 |
---|---|---|---|
事务开始 | 事务开始 | 事务开始 | 事务开始 |
… | … | … | 修改且已提交 |
进行中 | 快照读 | 进行中 | |
… | … | … |
从上述表格中,我们可以看到,当事务2对某行数据执行了快照读,数据库为该行数据生成一个 Read View 视图,可以看到事务1和事务3还在活跃状态,事务4在事务2快照读的前一刻提交了更新,所以,在 Read View 中记录了系统当前活跃事务1,3,维护在一个列表中。同时可以看到up_limit_id 的值为1,而low_limit_id 为5,如下图所示:
上述例子中,只有事务4修改过记录,并且在事务2进行快照读取前,就提交了事务,所以该当前行前数据的undolog如下:
当上述的内容都看明白了的话,那么大家就应该能够搞清楚这几个核心概念之间的关系了,下面我们讲一个不同的隔离级别下的快照读的不同。
RC、RR级别下的 InnoDB 快照读有什么不同
因为 Read View 生成时机的不同,从而造成 RC, RR级别下快照读的结果的不同
1.在 RR 级别下的某个事务的对某条记录的第一次快照读会创建一个快照即 Read View ,将当前系统活跃的其他事务记录起来,此后再 调用快照读的时候,还是使用的是同一个 Read View ,所以只要当前事务在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个 Read View ,所以对之后的修改不可见
2、在 RR 级别下,快照读生成 Read View 时, Read View 会记录此时所有其他活动和事务的快照,这些事务的修改对于当前事务都是不可见的,而早于 Read View 创建的事务所做的修改均是可见
3、在 RC 级别下,事务中,每次快照读都会新生成一个快照和 Read View ,这就是我们在 RC 级别下的事务中可以看到别的事务提交的更新的原因。
总结:在 RC 隔离级别下,是每个快照读都会生成并获取最新的 Read View ,而在 RR 隔离级别下,则是同一个事务中的第一个快照读才会创建 Read View ,之后的快照读获取的都是同一个 Read View
评论区