MVCC机制的全称为Multi-Version Concurrency Control,即多版本并发控制技术, 主要是为了提升数据库并发性能而设计的,其中采用更好的方式处理了读-写并发冲突,做到即使有读写冲突时,也可以不加锁解决,从而确保了任何时刻的读操作都是非阻塞的。
MVCC机制的组成
MVCC机制主要通过隐藏字段、undo log和ReadView来实现的
隐藏字段
在InnoDB引擎中主要有DB_ROW_ID、DB_TRX_ID、DB_ROLL_PTR这三个隐藏字段
DB_ROW_ID
对于InnoDB引擎的表,表数据是按照聚簇索引的格式存储的, 一般会采用主键作为聚簇索引列并构建索引树,如果表没有定义主键则会选择一个具备非空唯一属性的字段来作为聚簇索引字段。
如果未定义主键也不具备唯一非空属性字段,InnoDB就会隐式定义一个递增的DB_ROW_ID作为主键
DB_TRX_ID
DB_TRX_ID即事务id,MySQL对于每一个事务都会分配一个事务id,并且事务id是循序递增的,即后开启的事务id一定比先开启的事务id要大。
如果是一条select语句,事务id为0,但是如果是手动开启的事务,即便是有一条select语句也会分配递增的事务id。
DB_ROLL_PTR
ROLL_PTR全称为rollback_pointer,即回滚指针,当对一条数据做了改动后,会把旧版本的数据放到undo log中,而DB_ROLL_PTR则指向undo log中的旧版本数据,当事务需要回滚时则通过该字段找回旧数据。
undo log
MySQL的事务回滚时基于undo log实现的,undo log中存储了数据的旧版本,而且是链式的,比如一个事务对一条数据修改了两次,那么undo log中就会有两条旧版本数据,最新的旧版本数据会在链表头部。
ReadView
MVCC的意思是多版本并发控制, InnoDB通过undo log实现数据的多版本,而并发控制则是通过ReadView来实现。
当一个事务在读取一条数据时,MVCC根据当前MySQL的运行状态生成的快照,就是ReadView。
Review的核心内容有四个:
- creator_trx_id: 创建这个ReadView的事务id
- trx_ids: 生成这个ReadView还活跃着的其它事务id列表
- up_limit_id: trx_ids中的最小值
- low_limit_id: 生成该ReadView时,系统要给下一个事务分配的id
MVCC的实现原理
当前有两个事务T1和T2,T1正在修改ID=1的数据,而T2要查询这条数据,怎么判断T2能否读取到最新数据:
当开始查询时,会生成一个ReadView
判断隐藏列DB_TRX_ID == ReadView.creator_trx_id,相同则代表创建ReadView的事务和当前修改事务是同一个,能读取最新数据
判断隐藏列DB_TRX_ID是否小于ReadView.up_limit_id(最小活跃事务ID)
- 小于:修改数据的事务在快照创建前已结束,能读取最新数据
- 不小于:继续往下判断
判断隐藏列DB_TRX_ID是否小于ReadView.low_limit_id(下一个事务ID)
- 大于等于:修改数据的事务在快照生成后开启,不能读取最新数据
- 小于:表明DB_TRX_ID在ReadView.up*_limit_id和之间,继续判断
判断隐藏列DB_TRX*_ID是否在trx_ids中
- 在:修改数据的事务还在执行,不能读取最新数据
- 不在:修改数据的事务已结束,能读取最新数据
上述的345三点,可以把up_limit_id和low_limit_id看成trx_ids的左右区间,DB_TRX_ID在区间左(不包含)可以读取最新数据,区间右(包含)不可以读取最新数据, 在区间内的,则判断DB_TRX_ID是否在trx_ids内。
不同隔离级别下的MVCC
读未提交:数据直接修改读取,无MVCC
串行化:串行化执行,无并行化所以也没有MVCC
使用到MVCC的是读已提交和可重复读这两个隔离界别
- 读已提交:每次select语句生成一个ReadView
- 可重复读:首次select语句生成ReadView
MVCC与幻读问题
现在user表存在如下数据:
id | name |
---|---|
1 | A1 |
当开启事务T1查询
1 | // 步骤1 |
新开一个连接插入一条数据
1 | // 步骤2 |
这个时候继续在T1事务下执行
1 | // 步骤3 |
先更新id=2的数据然后再查询发现又能查询到了,第三次查到的结果集和前两次不一样,这不就是幻读的定义吗。
之所以能查到,是因为第三次去查询的时候id=2的行数据DB_TRX_ID就是T1本身,所以又能查询到了。
还有一种情况,我们把数据恢复到最开始的状态,通用执行步骤1、2,步骤3改成如下
1 | // 步骤3 |
这个时候发现也能查询到数据, 这是因为MVCC是基于读取时的无锁优化,当加了锁以后则不会通过MVCC方式读取,从快照读改为了当前读。同理 select … for update、 insert、update和delete都是当前读
参考
《MySQL技术内部:InnoDB存储引擎》