关于MySQL的InnoDB的MVCC原理,不少朋友都能说个大概:html
每行记录都含有两个隐藏列,分别是记录的建立时间与删除时间mysql
每次开启事务都会产生一个全局自增IDgit
在RR隔离级别下github
INSERT -> 记录的建立时间 = 当前事务ID,删除时间 = NULLsql
DELETE -> 记录的建立时间不动,删除时间 = 当前事务ID安全
UPDATE -> 将记录复制一次mvc
老记录的建立时间不动,删除时间 = 当前事务ID优化
新记录的建立时间 = 当前事务ID,删除时间 = NULLthis
SELECT -> 返回的记录须要知足两个条件:.net
建立时间 <= 当前事务ID (记录是在当前事务以前或者由当前事务建立的)
删除时间 == NULL || 删除时间 > 当前事务ID (记录是在当前事务以后被删除的)
但实际上,这个描述是很不严格的,问题有如下几点:
它们分别是:
DB_TRX_ID, 6byte, 建立这条记录/最后一次更新这条记录的事务ID
DB_ROLL_PTR, 7byte,回滚指针,指向这条记录的上一个版本(存储于rollback segment里)
DB_ROW_ID, 6byte,隐含的自增ID,若是数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引
另外,每条记录的头信息(record header)里都有一个专门的bit(deleted_flag)来表示当前记录是否已经被删除
UPDATE非主键语句的效果是
老记录被复制到rollback segment中造成undo log,DB_TRX_ID和DB_ROLL_PTR不动
新记录的DB_TRX_ID = 当前事务ID,DB_ROLL_PTR指向老记录造成的undo log
这样就能经过DB_ROLL_PTR找到这条记录的历史版本。若是对同一行记录执行连续的update操做,新记录与undo log会组成一个链表,遍历这个链表能够看到这条记录的变迁)
read_view中维护了系统中活跃事务集合的快照,这些活跃事务ID的最小值为up_limit_id,最大值为low_limit_id(不要搞反了!!!)
附上源码注释以便于理解
trx_id_t low_limit_id; // The read should not see any transaction with trx id >= this value. In other words, this is the "high water mark".
trx_id_t up_limit_id; // The read should see all trx ids which are strictly smaller (<) than this value. In other words, this is the "low water mark".
SELECT操做返回结果的可见性是由如下规则决定的:
DB_TRX_ID < up_limit_id -> 此记录的最后一次修改在read_view建立以前,可见
DB_TRX_ID > low_limit_id -> 此记录的最后一次修改在read_view建立以后,不可见 -> 须要用DB_ROLL_PTR查找undo log(此记录的上一次修改),而后根据undo log的DB_TRX_ID再计算一次可见性
up_limit_id <= DB_TRX_ID <= low_limit_id -> 须要进一步检查read_view中是否含有DB_TRX_ID
DB_TRX_ID ∉ read_view -> 此记录的最后一次修改在read_view建立以前,可见
DB_TRX_ID ∈ read_view -> 此记录的最后一次修改在read_view建立时还没有保存,不可见 -> 须要用DB_ROLL_PTR查找undo log(此记录的上一次修改),而后根据undo log的DB_TRX_ID再从头计算一次可见性
通过上述规则的决议,咱们获得了这条记录相对read_view来讲,可见的结果。
此时,若是这条记录的delete_flag为true,说明这条记录已被删除,不返回。
若是delete_flag为false,说明此记录能够安全返回给客户端
它们的不一样之处在于:
RR:read view是在first touch read时建立的,也就是执行事务中的第一条SELECT语句的瞬间,后续全部的SELECT都是复用这个read view,因此能保证每次读取的一致性(可重复读的语义)
RC:每次读取,都会建立一个新的read view。这样就能读取到其余事务已经COMMIT的内容。
因此对于InnoDB来讲,RR虽然比RC隔离级别高,可是开销反而相对少。
补充:RU的实现就简单多了,不使用read view,也不须要管什么DB_TRX_ID和DB_ROLL_PTR,直接读取最新的record便可。
MySQL的索引分为聚簇索引(clustered index)与二级索引(secondary index)两种。
刚才讲的内容是基于聚簇索引的,只有聚簇索引中含有DB_TRX_ID与DB_ROLL_PTR隐藏列,能够比较容易的实现MVCC
可是二级索引中并不含有这几个隐藏列,只含有1个bit的deleted flag,咋办?
好办,若是UPDATE语句涉及到二级索引的键值,将老的二级索引的deleted flag标记为true,而后建立一条新的二级索引记录便可。
可是若是想根据二级索引来作查询,这可就麻烦了。由于二级索引不维护版本信息,没法判断二级索引中记录的可见性。
因此仍是须要回到聚簇索引中来:
根据二级索引维护的主键值去聚簇索引中查找记录(使用MVCC规则)
若是查出来的结果跟二级索引里维护的结果相同 -> 返回,若是不一样 -> 丢弃
若是对于一条查询语句,二级索引中有不少条知足条件的结果(连续屡次更新,致使二级索引中有不少条记录),那上面这个流程就比较低效了。因此InnoDB的做者搞了个机智的小优化:
在二级索引中,用一个额外的名为MAX_TRX_ID的变量来记录最后一次更新二级索引的事务的ID
那么,若是当前语句关联的read_view的 up_limit_id > MAX_TRX_ID,说明在建立read_view时最后一次更新二级索引的事务已经结束,也就是说二级索引里的全部记录对于当前查询都是可见的,此时能够直接根据二级索引的deleted flag来肯定记录是否应该被返回。
小结一下:二级索引的MVCC可见性判断在MAX_TRX_ID失效的状况下须要依赖聚簇索引才能完成。
从前面的分析能够看出,为了实现InnoDB的MVCC机制,更新或者删除操做都只是设置一下老记录的deleted_bit,并不真正将过期的记录删除。
为了节省磁盘空间,InnoDB有专门的purge线程来清理deleted_bit为true的记录。
为了避免影响MVCC的正常工做,purge线程本身也维护了一个read view(这个read view至关于系统中最老活跃事务的read view)
若是某个记录的deleted_bit为true,而且DB_TRX_ID相对于purge线程的read view可见,那么这条记录必定是能够被安全清除的。
InnoDB多版本(MVCC)实现简要分析(水平很高,分析深刻,必需要看,但可能不太好理解)