一 使用场景
首先要明确,MVCC机制针对的场景主要是读-写并发。因为读-读并发不会产生问题,写-写并发通过加锁实现,读-写并发场景下会产生诸如脏读、不可重复读、幻读等问题。
二 MVCC依赖的数据结构
1 undolog
作为回滚使用的重要文件,mysql每次更新数据之前,都会先写undolog。也就是说,每条数据的每次操作,都会形成一条undolog数据,这些数据根据时间关系就形成了一条链表,链表中的每个节点即是一个快照。MVCC即是为每个事务寻找对应的快照(快照读)。
2 数据表的三个隐藏字段
mysql表中每行数据都有三个隐藏的重要字段,DB_TRX_ID(最近一次被哪个事务更新)、DB_ROW_ID(隐含的主键id,如果未指定主键,会用该列创建主键索引)、DB_ROLL_PT(回滚指针,指向上个版本的数据,一条数据通过该指针形成链表)。
3 readView
每创建一个事务的时候,就会随事务创建对应的readView视图。readView中包含以下数据结构:
- 当前事务id(Transaction ID)
- 当前所有未提交的事务集合(
m_ids
) - 创建readView时最小的事务id(
min_trx_id
) - 创建readView时最大的事务id(
max_trx_id
)
顺便说一下,在RC隔离级别下,事务中每次select操作都会重新创建一个readView;而在RR隔离级别下,只会在第一次select时创建readView。这也就解释了为什么RC级别下可以解决脏读、RR级别下可以解决不可重复读的问题。
三 快照读
基于上述的数据结构,MVCC为每个事务的select操作找到对应的数据版本。具体的:
先用undolog中最新的数据比较,
DB_TRX_ID < min_trx_id:说明当前版本的数据提交早于readview中所有的事务,因此该版本数据是可见的。
DB_TRX_ID > max_trx_id:说明当前版本的数据提交晚于readview中所有的事务,因此该版本数据是不可见的。
DB_TRX_ID在min_trx_id和max_trx_id之间,并且在集合m_ids中:说明在创建readView时该版本还未提交,在创建后提交了,因此也是不可见的。
DB_TRX_ID在min_trx_id和max_trx_id之间,并且不在集合m_ids中:说明在创建readView时该版本已经提交了,因此是可见的。
经历以上比较,如果数据不可见,那么就顺着undolog的链表,取上一个版本的数据,用上个版本的DB_TRX_ID重新执行上述比较流程,直到找到可见的版本数据。
另外补充一下快照读和当前读的区别,快照读的概念仅存在于RC和RR隔离级别下。其实我们常用的select操作都是快照读,当前读一般是加锁读操作或者是修改操作,比如select ... for update, insert,delete,update...