MVCC简介
MVCC:Multi-Version Concurrency Control多版本并发控制,不仅用于MySQL,分布式事务也可以使用;是一种乐观锁,用于RR(可重复读)、RC(读已提交)隔离级别。使用了行级锁。
当执行查询sql时会生成一致性视图read-view,它由执行查询时所有未提交事务id数组(数组里最小id为min_id)和已创建的最大事务id(max_id)组成,查询的数据结果需要跟read-view做比对从而得到快照结果。
MVCC通过保存数据在某个时间点的快照来实现的,基本特征如下:
- 每行数据都存在一个版本,每次数据更新时都更新该版本。
- 修改时Copy出当前版本随意修改,各个事务之间互不干扰。
- 保存时比较版本号,如果成功commit则覆盖原记录;失败则放弃copy。
2、InnoDB引擎的MVCC策略
每行数据额外保存两个隐藏列(当前行创建时的版本号和删除时的版本号,另外还有一列称为回滚指针,用于事务回滚);
InnoDB内部为每一行添加了两个隐藏列:DB_TRX_ID版本号和DB_ROLL_PTR回滚指针(MySQL另外还有一个隐藏列DB_ROW_ID,这是在InnoDB表没有主键的时候会用来作为主键)。
- DB_TRX_ID:长度为6字节,存储了插入或更新语句的最后一个事务的事务ID。
- DB_ROLL_PTR:长度为7字节,称之为:回滚指针。回滚指针指向写入回滚段的undo log记录,读取记录的时候会根据指针去读取undo log中的记录。
- DB_ROW_ID: 行标识(隐藏单调自增 ID ),大小为 6 字节,如果表没有主键, InnoDB 会自动生成一个隐藏主键,因此会出现这个列。另外,每条记录的头信息( record header )里都有一个专门的 bit ( deleted_flag )来表示当前记录是否已经被删除。
快照读:在RR隔离级别下,在不加锁的情况下MySQL会根据回滚指针选择从undo log记录中获取快照数据,而不总是获取最新的数据,这也就是为什么另一个事务提交了数据,在当前事务中看到的依然是另一个事务提交之前的数据。RR隔离级别快照并不是在BEGIN就开始产生了,而是要等到事务当中的第一次查询之后才会产生快照,之后的查询就只读取这个快照数据。
3、版本链比对规则
规则描述1
事务规则:
从最新记录开始查找:
- 如果,当前记录的事务id<未提交事务的最小id;说明事务都是已提交的,可读。
- 如果,未提交事务的最小id<=当前记录的事务id<=未提交事务的最大id;事务id是否在未提交事务id数组中,若在则不可读(但可以读自己本事务的)。
- 如果,当前记录的事务id>事务的最大id;事务还未开始,不可读。
RR(可重复读):返回的readview是第一条记录的,在事务中不会重复生成。
RD(读已提交):每次查询都生成最新的readview。
规则描述2
1、如果落在绿色部分(trx_id<min_id),表示这个版本是已经提交的事务生成的,这个数据是可见的;
2、如果落在红色部分(trx_id>max_id),表示这个版本是由将来启动的事务生成的,是肯定不可见的;
3、如果落在黄色部分(min_id<=trx_id<=max_id),那就包括两种情况:
- 若row的trx_id在数组中,表示这个版本是由还没有提交的事务生成的,不可见,当前自己的事务是可见的。
- 若row的trx_id不再数组中,表示这个版本是已经提交了的事务生成的,可见。
删除的实现
对于删除的情况可以认为是update的特殊情况,会将版本链上最新的数据复制一份,然后将trx_id修改成删除操作的trx_id,同时在该条记录的头信息(record header)里的(delete_flag)标记位写上true,来表示当前记录已经被删除,在查询时按照上面的规则查找到对应的记录,如果delete_flag标记位为true,意味着记录已被删除,则不返回数据。
版本链生成是全局的不是单一表的,这些版本链记录在undo日志中。rc隔离级别下是多个select是中途更新read-view快照的。而RR隔离级别是不更新read-view的,因此可重复读。
4、案例分析
事务过程:
事务4的分析过程:
1、select name from table where id=1; readview:[1,3] 3
从undo日志的首行开始:
trx_id=1,属于未提交事务的最小id<=当前记录的事务id<=未提交事务的最大id,1在其中,所以不可读。继续向下。
trx_id=3,属于未提交事务的最小id<=当前记录的事务id<=未提交事务的最大id,3在其中,所以不可读。继续向下。
trx_id=2,属于未提交事务的最小id<=当前记录的事务id<=未提交事务的最大id,2不在其中,所以可读。完成,返回name=B这条记录。
2、select name from table where id=1; readview:[1] 3
RR:沿用上个readview:[1,3] 3;查询结果仍旧不变(这就是为什么叫做可重复读)。
RD:
trx_id=1,属于未提交事务的最小id<=当前记录的事务id<=未提交事务的最大id,1在其中,所以不可读。继续向下。
trx_id=3,属于未提交事务的最小id<=当前记录的事务id<=未提交事务的最大id,3不在其中,所以可读。返回结果name=C这条记录。(这就是为什么叫做读已提交)