前提概要
- 什么是mvcc
- 什么是当前读和快照读
- 当前读,快照读和mvcc的关系
MVCC实现原理
- 隐藏字段
- undo log(回滚日志)
- undo log底层实现
- read-view
- 版本链对比规则
1. 前提概要
1.1 什么是mvcc
MVCC(Muti-Version Concurrency Control) 多版本并发控制
官方定义:Innodb通过为每一行记录添加两个额外的隐藏的值来实现mvcc,这两个值一个记录这行数据何时被创建,另一个记录着行数据何时过期(或被删除)。但是Innodb并不存储这些事件发生时的实际时间,相反,它只存储这些事件发生时的系统版本号。这是一个随着事务创建而不断增长的数字。每个事务在事务开始会记录他自己的系统版本号。每个查询必须取用检查每行数据的版本号与事务的版本号是否相同。
自我理解:同一时间,不同事物可以读取到不同版本的数据,从而去解决脏读和不可重复读的问题,是innodb实现事务并发与回滚的重要功能。
1.2什么是当前读和快照读?
- 当前读
像select lock in share mode(共享锁),select for update;update;insert’delete(排他锁)这些操作都是一种当前读。就是它读取的是记录最新的版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁 - 快照读
像不加锁的select操作就是快照读,及不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即mvcc,可以认为mvcc是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读取到的并不一定是数据的最新版本,而有可能是之前的历史版本。
说白了mvcc就是为了实现读写冲突不加锁,这个读就是快照读。不是当前读,当前读实际上是一种加锁的操作,是悲观锁的实现。 - 当前读、快照读和mvcc的关系
- 1.mvcc多版本并发控制是维持一个数据多个版本,是的读写操作没有冲突的概念,只是一个抽象的概念,并非实现
- 2.因为mvcc只是一个抽象的概念,要实现这么一个概念,mysql就需要根据具体的功能去实现它,快照读就是mysql实现mvcc理想模型的其中一个非阻塞读功能。而相对而言,当前读就是悲观锁的具体功能实现。
mvcc带来的好处
mvcc是一种用来解决读写冲突的无锁并发控制,也就是未事务分配单项增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照。所以mvcc可以为数据库解决一下问题
- 在并发读写数据库时,可以做到在读操作的时候不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能。
- 同时该可以解决脏读、幻读、不可重复读等事务隔离问题。但不能解决更新丢失问题。
MVCC实现原理
- mvcc只在REPEATABLE READ(可重复读)和READ COMMITTED(读已提交)这两个隔离级别下适用。
- mvcc原理实现是由三个隐藏字段、undo日志、read-view实现。
1.隐藏字段
每行数据除了记录我们自定义的字段外,还有数据库隐式定义的三个字段
- DB_TRX_ID: 6byte,最近修改事务ID(记录创建这条记录/最后一次修改该记录的事务ID)
- DB_ROLL_PTR: 7byte,回滚指针,指向这条记录的上一个版本
- DB_ROW_ID: 6byte,隐含的自增ID(隐藏主键),如果数据表没有主键,Innodb会自动以DB_ROW_ID产生一个聚簇索引
2.undo log日志
undo log有两个作用,一个是回滚操作实现原子性,另一个作用是实现mvcc的多版本控制。
undo log 分为两种:
-insert undo log
代表事务在insert新纪录时产生的undo log,只在事务回滚时需要,并且在事务提交后立即丢弃
-update undo log
事务在进行update或者delete时产生的undo log;不仅在事务回滚时需要,在快照读也需要;所以不能随便删除,只有在快照读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除
-undo log 在mvcc中的作用
undo log保存的是一个版本链,也就是使用DB_ROLL_PTR这个字段来连接。undo log在mvcc中就是为了根据存储的事务ID和一致性试图做对比,找出当前事务能够看到的版本数据。
3.undo log底层实现
假设一开始的数据如下
然后执行了一条更新sql,update user set name ='niuniu' where id = 1
,那么undo log的记录就会发生变化,也就是说当执行一条更新语句时会把之前的原有数据拷贝到undo log日志中。同时在最新的数据记录中的DB_ROLL_PTR储存指向undo log的指针地址。
下面蓝色那部分时 undo log日志记录,如上图那样。
4.read-view
ReadView可以理解为数据库中某一时刻所有未提交事务的快照。ReadView有几个重要的参数:
- m_ids: 表示生成ReadView时,当前系统正在活跃的读写事务的事物id列表。(也就是还未提交事务)
- min_trx_id: 表示生成ReadView时,当前系统中活跃的读写事物的最小事务id。
- max_trx_id: 表示生成Readview时,当前时间戳Innobd将在下一次分配事务的id。
- creator_trx_id: 当前事务id
用处:配合undo log进行版本链对比,比对出当前事务可见的版本数据。
5.版本链比对规则
- 1.如果落在trx_id < min_id,表示此版本的事务在生成readview前已经提交,所以该版本的数据可以被当前事务访问。
- 2.如果落在trx_id > max_id, 表示此版本的事务在生成readview后才生成,所以该版本不可以被当前事务访问。
- 3.如果被访问版本的trx_id属性值在m_ids列表的最小事务id和最大事务id之间,就需要进一步判断trx_id属性值是不是包含在m_ids中,如果包含的话,说明创建readview时生成的该版本的事务还是活跃的,所以该版本不可以访问;如果不包含,说明创建readview时生成该版本的事务已经被提交,该版本可以访问。
6.示例
假如有一条数据user数据,初始值name=“刘德华”,然后经过下面的更新,时间点如下:
1.基于RC隔离级别的事务在每次查询开始的时候都会生成一个独立的ReadView.
在T4时间点时,版本链如下:
执行update语句,undo log记录一份修改之前的数据,然后更新后的数据后面记录当前事务id100,还有指向undo log的指针地址。如上图所示。因为此时系统活跃的事务由100和200都未提交,所以生成的readview事务列表m_ids=[100,200],然后根据版本比对规则,300>max_trx_id(200),证明此版本数据对当前事务不可见,然后根据回滚指针找到上一个版本接着比对,直到找到数据刘德华。
在T6时间点,版本链如下:
在T6时间点select语句执行时,当前系统正在活跃的事务还有trx_id未200未提交,所以m_ids=[200].因此找到的数据就是古天乐。
2.基于RR隔离级别的事务在第一次读取数据时生成ReadView,之后的查询都不会再生成,所以一个事务的查询结果每次都是一样的。(注意特例:如果在两次快照读之间穿插一个当前读会重新生成ReadView)
第一次查询也就是T4时间点生成ReadView,事务列表m_ids=[100,200].所以当前版本可见数据为刘德华。
第二第三次不会生成新的ReadView,所以结果都是刘德华。
由于在同一个事务中,RR级别的事务在查询中只会生成一个ReadView,所以能解决不可重复读问题。
小结
- 如果当前事务id在绿色部分,是已经提交了事务,说明数据可见。
- 如果当前事务id在蓝色部分,会有两种情况,如果当前事务id在readview的m_ids数组内,是没有提交的事务不可见,如果不在数组内数据可见。
- 如果落在红色部分,则不考虑,对于未来的事情不去想即可。
借鉴:https://zhuanlan.zhihu.com/p/500868047
https://zhuanlan.zhihu.com/p/428066667
https://zhuanlan.zhihu.com/p/367820387