MVCC是什么?MVCC即多版本控制协议,InnoDB实现了MVCC作版本控制,防止不该被当前事务看到的数据看到。
举个例子,下面就是在T4时刻,事务A和事务C看到的数据不一致,也就是说有多个版本。
事务时刻 | 事务A | 事务B | 事务C |
---|---|---|---|
T1 | begin; | begin; | begin; |
T2 | select * from transation_test; |
结果:id salary
1 1 | | |
| T3 | | 插入一行数据(2,2)
commit; | |
| T4 | select * from transation_test;
结果:id salary
1 1 ** ** | | select * from transation_test;
结果:id salary
1 1
2 2 |
而引入MVCC的原因就是在读写事务时读的情况不加锁,提高并发性能。
本质理解
在InnoDB中,主要是通过使用readview的技术来判断数据是否能当前事务读到。如果可以,则输出,否则就利用undolog来构建历史版本,再进行判断,直到记录构建到最老的版本或者可见性条件满足。
上面的工作需要两或三个隐藏字段、undo log、一个数组、ReadView完成。
InnoDB表数据组织方式是聚簇索引。在聚簇索引上还有一些额外信息会存储,就是两或三个隐藏字段。
DB_TRX_ID:事务ID,表示最近一次插入或者更新该记录的事务ID。
DB_ROLL_PTR:回滚指针,指向该记录的之前的undo log记录
DB_ROW_ID:当表上没有用户主键的时候,InnoDB会自动创建(这也是为什么两或三个隐藏字段)
假设一个表中有两个字段(ID,Name),一个事务id为1插入一条记录(1,事务1)。该条记录还没有上一版本,回滚指针为null。那么现在这张表就变成了下面的样子。
前面我们也介绍过undo log在innodb中有两个作用,MVCC、事务回滚。undo log主要存放一条sql语句执行前的记录及与sql语句相反的操作。
现在来一个事务要修改ID为1这一行记录,那么它的过程如下。
首先向InnoDB申请一个事务ID,注意事务ID是严格递增的,假如申请的ID为3,简称事务3。
事务3要对ID为1的记录做update修改操作,数据库为这行记录加上行锁。
将该行数据拷贝进undo log中
拷贝完成后,事务3将ID为1记录修改为(1,事务3),事务ID也会变成3,回滚指针指向undo log中该条记录事务1版本
事务3提交,释放锁。
现在又来一个事务,修改ID为1这行记录
该事务会重新走一遍事务3的流程,假如该事务ID为5,则现在的图变为
可以看出此时事务对同一条记录的修改,会使这条记录的undo log变为一个链表的形式。链首是最新的旧记录,链尾是最早的旧记录。
注意点
undo log分为insert undo和update undo,insert undo即执行insert 操作留下的历史记录,insert undo会在事务提交/回滚后直接删除,而update undo会保留下来做历史版本链表。上面为了能够讲述明白所以没有删除事务1的insert undo
在InnoDB内部维护着一个数组,该数组(trx_sys->descriptors数组)会记录当前还未提交的事务id,id会从小到大排序。也就是说事务执行时会向InnoDB申请一个事务id,该数组会记录此id,如果该事务提交了,则从该数组中删除。
这个数组有什么用呢?为下面的ReadView做铺垫,创建ReadView时会复制一份该数组到ReadView中,ReadView会依据数组中未提交的id值进行判断事务是否可见一条记录。
什么是ReadView?
ReadView,读视图。ReadView从代码层面其实是一个结构体(C语言名词),名叫read_view_t。事务其实也是一个结构体,trx_t。每个数据库连接持有一个trx_t(事务),每个trx_t(事务)持有一个read_view_t(读视图);事务进行快照读操作产生的一个ReadView。
ReadView有什么用?
前面说到ReadView主要做事务可见性判断,即某个事务执行快照读时,对该记录创建一个ReadView读视图,根据ReadView去判断当前事务能够看到哪个版本数据,有可能是最新的数据,也有可能是该行记录undo log里面某个版本的数据。
ReadView如何做可见性判断?
回答这个问题要从ReadView结构体中的属性入手了。
read_view_t
这几个属性有什么用呢?利用上述属性做事务可见性判断
判断的核心思想是事务启动以前及以后所有还没提交的事务,它都不可见。源码如下
//id:一条记录的事务id bool changes_visible(trx_id_t id, const table_name_t &name) const MY_ATTRIBUTE((warn_unused_result)) { ut_ad(id > 0); //如果这条记录的事务id<数组中最小值 或者 等于当前事务id,返回true那么当前事务可见这条记录 if (id < m_up_limit_id || id == m_creator_trx_id) { return (true); } check_trx_id_sanity(id, name); //如果这条记录的事务id大于等于最大事务id,返回false那么当前事务不可见这条记录 if (id >= m_low_limit_id) { return (false); } else if (m_ids.empty()) { return (true); } const ids_t::value_type *p = m_ids.data(); return (!std::binary_search(p, p + m_ids.size(), id)); }
为了方便理解,下面对上面的代码做进一步的说明,一共分四种情况
如果最新记录上事务id<up_limit_id(min_trx_id),证明当前事务构建readview时这个事务已经提交了,所以可以看见这条记录
如果最新记录上事务id>=low_limit_id(max_trx_id),证明当前事务构建readview时这个事务还没有对记录进行修改操作,所以看不见这条记录
如果最新记录上事务id在up_limit_id和low_limit_id之间,且在readview数组中,证明当前事务构建readview时这个事务正在修改该条记录,所以看不见这条记录
如果最新记录上事务id在up_limit_id和low_limit_id之间,且不在readview数组中,证明当前事务构建readview时这个事务已经提交,所以可以看见这条记录
是不是字太多,记这么多东西简直是难为人。
总结下最核心的,InnoDB的事务快照读的情况下只能看见已经提交事务的数据,已经提交分为两种情况
如果满足其中一种情况,事务则可以看见该记录
事务隔离级别为可重复读。当前系统中有5个事务,5个事务都对id为1这行记录进行操作。
其中事务1和事务5已经提交,事务8进行快照读。
时刻 | 事务1 | 事务3 | 事务5 | 事务7 | 事务8 |
---|---|---|---|---|---|
T1 | begin; | begin; | begin; | begin; | begin; |
T2 | 插入(1,事务1)记录 | ||||
commit; | |||||
T3 | 修改id为1的名字为事务3 | 修改id为1的名字为事务5 | |||
commit; | 修改id为1的名字为事务7; | | |||
查询id为1的记录(快照读) |
事务8在T4时刻快照读创建ReadView,在T4时刻可以读取到事务几的数据呢?
首先看T4时刻ReadView中各个属性的值为多少
根据前面所说隐藏字段及undo log版本链,可以做成如下图
最后得出结论,事务8可以读取到事务5版本的数据
需要注意的是:
MVCC可以通过ReadView的方式实现读已提交和可重复读的隔离级别,但是两种隔离级别创建的ReadView的时间点不同。
因此可重复读的隔离级别解决了不可重复读的问题,并一定程度上避免了幻读问题,但是没有真正结解决,请看下一篇
事务实现源码级:http://mysql.taobao.org/monthly/2018/11/04/
事务概括:http://mysql.taobao.org/monthly/2017/12/01/
通俗易懂级MySQL MVCC
深入理解MVCC:https://www.cnblogs.com/kismetv/p/10331633.html