多版本并发控制
- 多版本并发控制(MVCC)是一种用来解决读写冲突的无锁并发控制,主要依赖记录中的3个隐藏字段、undo log和Read view来实现
- 为事务分配单向增长的事务ID,为每个修改保存一个版本,将版本和事务ID关联,读操作只读该事务开始前的数据库快照(快照读)
- MVCC保证读写并发时,读操作不会阻塞写操作,写操作也不会阻塞读操作,提高了数据库并发读写的性能,同时也能解决脏读、不可重复读和幻读等事务隔离性问题
记录中的3个隐藏字段
数据库表中的每条记录都会有如下3个隐藏字段:
- DB_TRX_ID:6字节,创建或最近一次修改该记录的事务ID
- DB_ROW_ID:6字节,隐含的自增ID(隐藏主键)
- DB_ROLL_PTR:7字节,回滚指针,指向这条记录的上一个版本
说明一下:
- 采用InnoDB存储引擎建立的每张表都会有一个逐渐,如果用户没有设置,InnoDB会自动以DB_ROW_ID产生一个聚簇索引
- 此外,数据库中的每条记录还有一个隐藏字段------flag,用于表示这个记录是否被删除,便于数据回滚

当你向表中插入一条数据的时候,该记录不仅包含了name和age字段,还有三个隐藏的字段。

- 假设当前插入的元素的事务ID为9,那么DB_TRX_ID的值就是9
- 因为这是插入的第一个记录,所以隐式主键就是1
- 由于这条记录是新插入的,没有历史版本,所以回滚指针的值设置为null
- MVCC重点需要的就是这三个隐藏字段,实际还有其他隐藏字段,只不过暂时不需要关心
undo日志
MySQL的三大日志:
- redo log:重做日志,用于MySQL崩溃后的数据恢复,保证数据的持久性
- bin log:逻辑日志,用于主从数据备份进行数据同步,保证数据的一致性
- undo log:回滚日志,用于对已经执行的操作进行回滚,保证事务的原子性
MVCC机制的实现主要是依靠ubdo log,记录的历史版本就是在undo log对应的缓冲区中
快照
现在有一个事务ID为10的事务,要将刚才插入学生表中的学生姓名改为"李四"
- 因为是要进行写操作,所以需要先给该记录加行锁。
- 修改前,先将该行记录拷贝到undo log中,此时undo log中就有了一行副本数据。
- 然后再将原始记录中的学生姓名改为"李四",并将该记录的DB_TRX_ID改为10,回滚指针DB_ROLL_PTR设置成undo log中副本数据的地址,从而指向该记录的上一个版本。
- 最后当事务10提交后释放锁,这时最新的记录就是学生姓名为"李四"的那条记录。

现在又有一个事务ID为11的事务,要求将学生表中的那条记录的学生年龄改为"20"
- 因为是要进行写操作,所以需要先给最新的记录加行锁
- 修改前,先将该行拷贝到undolog中,此时undo log中又多了一行副本数据
- 然后将元素记录中的学生年龄改为 20 ,并将该记录的DB_TRX_ID改为11,DB_POLL_PTR指针指向该记录的上一个版本
- 最后当事务11提交后释放锁,这时最新的记录就是学生年龄为20的记录

此时我们就有了一个基于链表记录的历史版本链,而undo log中的一个个的历史版本就称为一个个的快照。
所谓回滚就是用undo log中的历史数据覆盖当前数据,创建保存点(savepoint)就是给某些版本做了标记,让我们可以直接用这些版本数据来覆盖当前数据
insert和delete的记录如何维护版本链?
- 新插入的数据是历史版本的,但是一般为了方面回滚,新插入的记录也需要向undo log中拷贝一个记录,同时将这个拷贝的记录的flag字段设置为1表示被删除,此后如果回滚的话,这个数据就自动被删除了
- 删除记录不是真正的把数据给删除了,而是将该记录拷贝一份放在undo log中,然后将该记录的flag字段设置为1,这样回滚之后,flag字段就从1变成0,相当于删除的数据又恢复了。
当前读和快照读
- 当前读:读取最新的记录,就叫做当前读。
- 快照读:读取历史版本,就叫做快照读
事务在进行增删查改的时候,并不是一定需要加锁保护
- 事务对数据进行增删改的时候,操作的都是最新记录(当前读),这个时候需要加锁
- 事务进行select的时候,可以是当前读,也可以是快照读。如果是当前读,需要进行加锁保护,如果是快照读,就不需要加锁保护------历史版本不会被修改,因此不需要加锁,也能就能并发的执行,提高了效率
而select查询时应该进行当前读还是快照读,则是由隔离级别决定的,在读未提交和串行化隔离级别下,进行的都是当前读,而在读提交和可重复读隔离级别下,既可能进行当前读也可能进行快照读。
undo log中的版本链何时才会被清除?
- 在undo log中形成的版本链不仅仅是为了进行回滚操作,其他事务在执行过程中也可能读取版本链中的某个版本,也就是快照读。
- 因此,只有当某条记录的最新版本已经修改并提交,并且此时没有其他事务与该记录的历史版本有关了,这时该记录在undo log中的版本链才可以被清除。
Read View
-
事务在进行快照读操作时会生成读视图Read View,在该事务执行快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃的事务ID。
-
Read View在MySQL源码中就是一个类,本质是用来进行可见性判断的,当事务对某个记录执行快照读的时候,对该记录创建一个Read View,根据这个Read View来判断,当前事务能够看到该记录的哪个版本的数据。
class ReadView {
// 省略...
private:
/** 高水位:大于等于这个ID的事务均不可见*/
trx_id_t m_low_limit_id;/** 低水位:小于这个ID的事务均可见 */ trx_id_t m_up_limit_id; /** 创建该 Read View 的事务ID*/ trx_id_t m_creator_trx_id; /** 创建视图时的活跃事务id列表*/ ids_t m_ids; /** 配合purge,标识该视图不需要小于m_low_limit_no的UNDO LOG, * 如果其他视图也不需要,则可以删除小于m_low_limit_no的UNDO LOG*/ trx_id_t m_low_limit_no; /** 标记视图是否被关闭*/ bool m_closed; // 省略...};
-
m_ids:一张列表,记录Read View生成时刻,系统中活跃的事务ID
-
m_up_limit_id:记录m_ids列表中的最小ID
-
m_low_limit_id:记录Read View生成时刻,系统尚未分配的下一个事务ID。
-
m_creator_trx_id:记录创建该Read View事务的事务ID
根据Read View中的m_up_limit_id和m_low_limit_id可以将事务ID分成三部分

- 事务ID小于m_up_limit_id的事务,一定是生成Read View已经提交的事务,因为m_up_limit_id是会生成Read View时刻系统中活跃事务ID的最小ID,因此事务ID比它小的事务在生成Read View的时候已经提交了
- 事务ID大于等于m_low_limit_id的事务,一定是生成Read View时还没有启动的事务。
- 事务ID大于等于m_up_limit_id,小于m_low_limit_id的事务,在生成Read View可能处于活跃状态,也可能已经提交了,这是需要通过事务ID是否存在于m_ids中来判断事务是否已经提交
一个事务在进行读操作,只应该看到自己或者已经提交了的事务所做的修改,因此我们可以根据Read View来判断当前事务是否可以看到另一个事务所做的修改
版本链中每个版本都有自己的DB_TRX_ID,即创建或最近修改改记录的事务ID,因此可以一次遍历版本链中的各个版本,通过Read View来判断当前事务是否可以看到这个版本,如果不可以,就继续遍历。
RR和RC的本质区别
先把隔离级别设置为repeatable read(可重复读)


第二个

- 上面两次实验的唯一区别在于,右终端中的事务在左终端中的事务提交修改数据之前是否进行过快照读。
- 由于RR级别下要求事务内每次读取到的结果必须是相同的,因此事务首次进行快照读的地方,决定了该事务后续快照读结果的能力。
RR和RC的本质区别
因为Read View产生时机不同,从而造成了RC和RR的快照读的结果的不同
在RR级别下,事务第一次快照读的是会创建一个Read View,此后一直使用这个Read View进行判断可见性
在RC界别下,事务每次进行快照读都会创建一个Read View,然后根据这个Read View进行可见性判断,因此每次快照读都可以读取到最新的数据
RR界别下只会创建一个Read View,它的可见历史版本不会发生变化,所以是重复读的,而RC每次快照读Read View都会变化,所有RC是不可重复读的。