一致性无锁读
什么是一致性无锁读
一致性无锁读包含两层含义:
- 一致性读
- 无锁
一致性读
一致性读,即快照读。在InnoDB中,事务中的查询会基于某个时间点创建的快照返回结果集,而非查询数据库表空间中的当前数据。一致性读(MySQL官方文档)。
一致性读与当前读相对立:
- 一致性读: 基于快照返回结果集, 普通的select即使用一致性读。
- 当前读:基于当前数据库中已提交的,select ... lock in share mode,select ... for update,insert,update,delete 语句使用当前读
一致性读说白了就是让一个事务中的普通读操作不受其他事务的影响(不同的隔离级别有所差异)
InnoDB中,在RR、RC隔离级别下,默认开启一致性读,但是其创建快照的时机不同。
- 在RR隔离级别下: 事务中的第一次一致性读(时间点)会创建一个快照,然后这个事务中后续所有的一致性读都基于这个快照返回数据。(除非本事务自己修改了相关数据)
- 在RC隔离级别下: 事务每一次一致性读,都会重置快照。
无锁的一致性读
InnoDB,普通的select读操作是不会对记录加锁的,否则就会产生比较大的性能开销。
InnoDB中通过快照(Read-View)、MVCC、undo-log来实现一致性无锁读
- MVCC:通过为表增加隐藏列,用来展示记录被修改的情况,以及提供了回溯历史版本的入口。
- Read-View: 程序中快照以
read_view_t
结构体对象的形式存在,里面记录的与创建快照的timepoint相关联的一些信息(如当时系统中出现过的最大事务ID,活跃的最小事务ID等) - undo-log:撤销日志,用于将记录回退到某个版本的语句
MVCC
什么MVCC
MVCC,即:Multi-Versioning Concurrency Control(多版本并发控制)
MVCC 是InnoDB在RC(Read-Commited)和RR(Read-Repeated)隔离级别下提高并发和支持Rollback的技术,它保存了被修改行的历史版本信息,结合undo-log形成历史版本链。
InnoDB可以使用MVCC实现以下两个目标:
- 提高并发: 通过历史版本链可以重建某个时间点的版本,从而无锁实现一致性读。
- 回滚: MVCC和undo-log可以提供事务回滚所需要的信息。
本文主要介绍如何基于MVCC实现一致性无锁读
MVCC的基本设计
MVCC为每张表增加了下面三个隐藏列:
- DB_TRX_ID: 当前记录被insert或update的最后一个事务的事务ID。(delete在内部也被视为update)
- DB_ROLL_PTR: 指向用于将当前记录回滚到上一个版本的undo-log的指针。可以用于重建历史版本。
- DB_ROW_ID: 自增列。
快照 Read-View
一致性读中的快照,在程序中医Read-View对象的形式存在。源码链接
代码语言:c复制struct read_view_t{
ulint type;
undo_no_t undo_no;
trx_id_t low_limit_no;
trx_id_t low_limit_id;
trx_id_t up_limit_id;
ulint n_trx_ids;
trx_id_t* trx_ids;
trx_id_t creator_trx_id;
UT_LIST_NODE_T(read_view_t) view_list;
};
这里对几个关键的字段进行下解释说明:
- low_limit_id: 创建Read-View时出现过的最大的事务ID 1。所以事务ID比low_limit_id大的事务对记录所做的更新,都不应该被当前Read-View可见。
- up_limit_id: 活跃事务列表trx_ids中最小的事务ID,所有事务ID小于up_limit_id的事务所做的更新,对当前Read-View可见。
- trx_ids: 创建当前Read-View时,还未提交的活跃事务的事务ID列表。
- creator_trx_id: 创建当前Read-View的事务ID。
undo-log
undo-log是InnoDB中与单个读写事务关联的撤消日志记录的集合。
undo-log中包含了如何撤销事务对聚簇索引记录所做的修改的信息。
当InnoDB需要读取某行记录的旧版本时,可以顺着undo-log找到对应的历史版本。
undo-log可以分为两大类:
- insert undo log: insert语句产生的undo log, 仅在事务回滚时需要,insert事务提交后即可删除对应日志。
- update undo log: update和delete语句产生的undo log,用于事务回滚和一致性读。因此只有与之相关的所有一致性读的事务都提交了访客删除(purge线程)
题外话:insert undo log在事务提交后即可删除,可以推出InnoDB无法基于undo-log和MVCC解决幻读问题。 幻读问题在RR隔离级别下仍然可能出现。
基于MVCC、Read-View、Undo-log实现一致性无锁读
有了前面的基础知识,我们来看下如何基于MVCC、Read-View、undo-log实现一致性无锁读。
首先,undo-log中提供了rebuild记录历史版本的信息(SQL语句),而MVCC则提供了重建历史版本的入口(指向undo-log的指针)。
那么现在的问题就是,如何确定一致性读对应的历史版本。
因为MVCC和undo-log中的版本信息是以事务ID来表示的,所以问题其实就是转化为:确定哪些事务对记录行的修改对当前可见。
快照创建前已经提交的事务所做的修改对当前快照可见,快照创建后才提交的事务所做的修改,对当前快照不可见。
因此一致性读的重点就是将Read-View中几个与事务ID相关的重点字段与数据库当前表数据和undo-log中DB_TRX_ID
列进行对比。
如果能够直接知道创建Read-View时哪些事务提交,哪些事务未提交,那么一切就会变得简单。然后并没有这样的直接渠道。 数据库中,很可能事务ID大的先提交,事务ID小的后提交。
在Read-View中我们维护了low_limit_id
、up_limit_id
、trx_ids
, 将他们与MVCC、undo-log中的DB_TRX_ID
列对比,即可得到哪些事务在创建Read-View已提交,哪些不是。
- 如果
DB_TRX_ID
<up_limit_id
, 则修改改记录的事务在创建Read-View之前就提交了。该版本对这个一致性读可见。 - 如果
DB_TRX_ID
>low_limit_id
, 则修改改记录的事务在创建Read-View时事务还未开启(更不用谈提交了),因此改版本对这个一致性读不可见。 - 如果
up_limit_id
<DB_TRX_ID
<low_limit_id
, 只能说明事务在创建Read-View前已经开启,但是无法判断其是否提交。需要与活跃列表进一步对比。 3.1DB_TRX_ID
在活跃列表trx_ids
中,即事务还在活跃(还未提交),因此其修改不可见。 3.2DB_TRX_ID
不在活跃列表trx_ids
中,即事务已经开启且不活跃,那必然就是已经提交。因此其修可见。
参考文献
MySQL 官方文档——consistent read
MySQL 5.7 Reference Manual: Consistent Nonlocking Reads
MySQL 5.7 Reference Manual: InnoDB Multi-Versioning
MySQL中MVCC的正确打开方式(源码佐证)