在这篇文章中介绍了一致性非锁定读和快照读的概念。快照数据其实就是当前行数据之前的历史版本,每行记录可能有多个版本。一个行记录可能有不止一个快照数据,一般称这种技术为行多版本技术。由此带来的并发控制,称之为多版本并发控制(Multi Version Concurrency Control, MVCC)
MVCC
MVCC只在读已提交READ-COMMITTED(读取已提交)和REPEATABLE-READ(可重复读)这两种事务隔离级别下才有效,是数据库引擎(InnoDB)层面实现的,用来处理读写冲突的手段(不用加锁),提高访问性能。
MVCC是怎么实现的呢,靠的是undo log和read view实现的
隐式字段
每行记录除了我们自定义的字段外,还有数据库隐式定义的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID字段
DB_TRX_ID
6个字节,最近修改/插入这条数据的事务ID:记录创建这条记录/最后一次修改该记录的事务ID。删除也被视为更新,会有一位特殊的标记位来标记这条数据为已被删除
DB_ROLL_PTR
7个字节,回滚指针:指向这条记录的上一个版本(存储于rollback segment里)
DB_ROW_ID
6个字节,隐含的自增ID(隐藏主键):如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引
undo log
undo log主要分为两种:
insert undo log
事务在insert新记录时产生的undo log, 只在事务回滚时需要,并且在事务提交后可以被立即丢弃
update undo log
事务在进行update或delete时产生的undo log; 不仅在事务回滚时需要,在快照读时也需要;所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除
举个例子:
比如一个有个事务插入persion表插入了一条新记录,记录如下,name为Jerry, age为24岁,隐式主键是1,事务ID和回滚指针,我们假设为NULL
现在来了一个事务1对该记录的name做出了修改,改为Tom
- 在事务1修改该行(记录)数据时,数据库会先对该行加排他锁
- 然后把该行数据拷贝到undo log中,作为旧记录,既在undo log中有当前行的拷贝副本
- 拷贝完毕后,修改该行name为Tom,并且修改隐藏字段的事务ID为当前事务1的ID, 我们默认从1开始,之后递增,回滚指针指向拷贝到undo log的副本记录,既表示我的上一个版本就是它
- 事务提交,释放锁
又来了个事务2修改person表的同一个记录,将age修改为30岁
- 在事务2修改该行数据时,数据库也先为该行加锁
- 然后把该行数据拷贝到undo log中,作为旧记录,发现该行记录已经有undo log了,那么最新的旧数据作为链表的表头,插在该行记录的undo log最前面
- 修改该行age为30岁,并且修改隐藏字段的事务ID为当前事务2的ID, 那就是2,回滚指针指向刚刚拷贝到undo log的副本记录
- 事务提交,释放锁
不同事务或者相同事务的对同一记录的修改,会导致该记录的undo log成为一条记录版本线性表,既链表,undo log的链首就是最新的旧记录,链尾就是最早的旧记录
Read View
有了上面undo log形成的版本链还不够,我们还需要判断版本链中的哪个版本是是当前事务可见的,因此有了一致性视图的概念。其中有四个属性比较重要
- m_ids: 在生成ReadView时,当前活跃的读写事务的事务id列表
- min_trx_id: m_ids的最小值
- max_trx_id: m_ids的最大值+1
- creator_trx_id: 生成该事务的事务id,单纯开启事务是没有事务id的,默认为0
版本链中的当前版本是否可以被当前事务可见的要根据这四个属性按照以下几种情况来判断
- 当 trx_id = creator_trx_id 时:当前事务可以看见自己所修改的数据, 可见,
- 当 trx_id < min_trx_id 时 : 生成此数据的事务已经在生成readView前提交了, 可见
- 当 trx_id >= max_trx_id 时 :表明生成该数据的事务是在生成ReadView后才开启的, 不可见
- 当 min_trx_id <= trx_id < max_trx_id 时
- trx_id 在 m_ids 列表里面 :生成ReadView时,活跃事务还未提交,不可见
- trx_id 不在 m_ids 列表里面 :事务在生成readView前已经提交了,可见
MySQL源码:
m_up_limit_id = min_trx_id
m_low_limit_id = max_trx_id
如果当前版本不可见的话,那么需要从版本链中继续寻找符合要求的版本,然后读出数据。具体例子可参见这里。