深入浅出: MySQL中的一致性无锁读与MVCC魔法

2024-03-11 22:14:47 浏览数 (2)

一、引言

在现代数据库管理系统中,高效的并发控制机制是维护数据一致性、确保数据库性能的关键。一致性无锁读在这一背景下显得尤为重要。它通过避免在读取操作中使用锁机制,来提高数据库的并发访问性能,减少等待和死锁的可能性。

在MySQL(innoDB存储引擎)中一致性无锁读的通过多版本并发控制(MVCC)、undo-log、READ-VIEW等技术实现。

本文将介绍MVCC、undo-log、READ-VIEW等技术的概念和实现原理,从而帮助读者更好的理解和适用MySQL的并发控制。

二、一致性无锁读基本概念

一致性无锁读是数据库管理系统中的一种读取数据的机制,它允许用户在不加锁的情况下读取数据,从而避免了读写操作之间的锁竞争。这种机制确保了读取操作可以并发进行,而不会被其他的读写操作所阻塞,同时保证了读取到的数据是一致的,即反映了事务开始时或查询时刻的数据库状态。

一致性无锁读包含两层含义:

  • 一致性读
  • 无锁

一致性读

一致性读,即快照读。在InnoDB中,事务中的查询会基于某个时间点创建的快照返回结果集,而非查询数据库表空间中的当前数据。一致性读(MySQL官方文档)。

一致性读与当前读相对立:

  • 一致性读: 基于快照返回结果集, 普通的select即使用一致性读。
  • 当前读: 基于当前数据库中已提交的,select ... lock in share modeselect ... for updateinsertupdatedelete 语句使用当前读

一致性读说白了就是让一个事务中的普通读操作不受其他事务的影响(不同的隔离级别有所差异)

InnoDB中,在RRRC隔离级别下,默认开启一致性读,但是其创建快照的时机不同。

  • 在RR隔离级别下:
    • 事务中的第一次一致性读(时间点)会创建一个快照,然后这个事务中后续所有的一致性读都基于这个快照返回数据。(除非本事务自己修改了相关数据)
  • 在RC隔离级别下:
    • 事务每一次一致性读,都会重置快照。

无锁的一致性读

InnoDB,普通的select读操作是不会对记录加锁的,否则就会产生比较大的性能开销。

InnoDB中通过快照(Read-View)、MVCCundo-log来实现一致性无锁读

  • MVCC:通过为表增加隐藏列,用来展示记录被修改的情况,以及提供了回溯历史版本的入口。
  • Read-View: 程序中快照以read_view_t结构体对象的形式存在,里面记录的与创建快照的timepoint相关联的一些信息(如当时系统中出现过的最大事务ID,活跃的最小事务ID等)
  • undo-log:撤销日志,用于将记录回退到某个版本的语句

三、MVCC深度剖析

MVCC的定义与工作原理

多版本并发控制(MVCC,Multi-Version Concurrency Control)是一种广泛应用于数据库管理系统中的技术,用于提高数据库并发性能,同时保持事务的隔离性。

MVCC通过为每个数据对象维护多个版本来实现,每个版本对应于数据对象在不同时间点的状态。当事务请求数据时,MVCC能够提供一个该事务时间点之前的一致性数据版本,从而允许多个读写事务并发执行,而不会相互干扰。

MVCC 是InnoDB在RCRead-Commited)和RRRead-Repeated)隔离级别下提高并发和支持Rollback的技术,它保存了被修改行的历史版本信息,结合undo-log形成历史版本链。

InnoDB可以使用MVCC实现以下两个目标:

  • 提高并发: 通过历史版本链可以重建某个时间点的版本,从而无锁实现一致性读。
  • 回滚: MVCC和undo-log可以提供事务回滚所需要的信息。

如何通过MVCC实现一致性无锁读

MVCC实现一致性无锁读的关键在于,它允许读事务访问数据的旧版本,而写事务则创建新的数据版本。这意味着读事务不需要等待写事务完成,也不会阻塞写事务的执行,因为它们操作的是不同版本的数据。

这种机制减少了锁的需求,尤其是在读多写少的场景中,能够显著提高数据库的并发性能。同时,由于读事务访问的是事务开始时刻的数据快照,它能够保证读取到的数据是一致的,即使在读取过程中数据被其他事务修改。

MVCC在MySQL中的实现细节

在MySQL数据库中,MVCC是通过InnoDB存储引擎实现的。InnoDB利用了几个关键的数据结构来支持MVCC:

  • 事务ID(Transaction ID):每个事务在开始时都会被分配一个唯一的事务ID,用于标识事务的版本。
  • 隐藏的版本字段:InnoDB在每行数据中存储两个隐藏的字段(自增字段DB_ROW_ID与MVCC无关),分别记录了行的创建事务ID和删除事务ID。这些字段用于判断某个事务是否能够看到该行数据。
    • DB_TRX_ID: 当前记录被insertupdate的最后一个事务的事务ID。(delete在内部也被视为update
    • DB_ROLL_PTR: 指向用于将当前记录回滚到上一个版本的undo-log的指针。可以用于重建历史版本。
  • Undo日志:当事务修改数据时,InnoDB会在Undo日志中记录数据的旧版本。如果需要访问旧版本数据,InnoDB可以在Undo日志中找到。
  • Read View:InnoDB为每个读事务创建一个Read View,其中包含了活跃事务的ID列表。这个列表用于判断事务在访问数据时应该看到哪个版本。

通过这些机制,InnoDB能够为每个事务提供一个一致性的数据视图,支持高效的一致性无锁读。然而,MVCC也带来了额外的开销,例如维护多个数据版本和Undo日志需要额外的存储空间,且过时的数据版本需要定期清理以释放空间。因此,虽然MVCC提高了并发性能,但也需要适当的配置和优化以平衡性能和资源消耗。

四、Undo Log的奥秘

Undo Log的定义与作用

undo-log是InnoDB中与单个读写事务关联的撤消日志记录的集合。

undo-log中包含了如何撤销事务对聚簇索引记录所做的修改的信息。

当InnoDB需要读取某行记录的旧版本时,可以顺着undo-log找到对应的历史版本。

undo-log可以分为两大类:

  • insert undo log insert语句产生的undo log, 仅在事务回滚时需要,insert事务提交后即可删除对应日志。
  • update undo log updatedelete语句产生的undo log,用于事务回滚和一致性读。因此只有与之相关的所有一致性读的事务都提交了访客删除(purge线程)

★ 题外话:insert undo log在事务提交后即可删除,可以推出InnoDB无法基于undo-logMVCC解决幻读问题。 幻读问题在RR隔离级别下仍然可能出现。 ”

Undo Log与一致性无锁读

Undo Log是MVCC实现一致性无锁读的基石。通过维护数据的历史版本,Undo Log使得读事务可以无需等待写事务的完成,就能访问到数据的一致性版本。这种机制显著减少了读写冲突,提高了数据库的并发性能。同时,由于读操作不需要加锁,也就降低了死锁的风险,进一步提升了系统的稳定性和可靠性。

然而,Undo Log的管理(如Undo Log的创建、维护和清理)也对数据库性能有一定的影响,因此需要适当的策略来优化Undo Log的处理,以平衡并发性能和系统资源的使用。

五、Read-View的构建与应用

  • Read-View的定义与构建过程
  • Read-View在一致性无锁读中的作用
  • Read-View与事务隔离级别的关系

Read-View的定义与构建过程

Read-View是MVCC(多版本并发控制)机制中的一个关键概念,它是一个逻辑上的数据视图,使得事务能够读取到执行开始时刻的一致性数据快照。

在innoDB源码中,Read-View 结构体定义如下:

代码语言:javascript复制
struct read_view_t{
 ulint  type;
 undo_no_t undo_no;
 trx_id_t low_limit_no;
 trx_id_t low_limit_id;    // 创建Read-View时出现过的最大的事务ID 1。
 trx_id_t up_limit_id;     // 活跃事务列表trx_ids中最小的事务ID
 ulint  n_trx_ids;
 trx_id_t* trx_ids;         // 创建当前Read-View时,还未提交的活跃事务的事务ID列表
 trx_id_t creator_trx_id;  // 创建当前Read-View的事务ID
 UT_LIST_NODE_T(read_view_t) view_list;
};

Read-View的构建过程主要涉及确定哪些数据版本对当前事务是可见的,从而保证读取操作的一致性。

  • 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可见。

构建Read-View的过程通常包括以下几个步骤:

  1. 记录当前活跃事务:当事务开始时,系统会记录下当前所有活跃的事务ID。这些活跃事务包括已经开始但尚未提交的事务。
  2. 确定可见版本:对于每一行数据,通过比较数据的版本信息(如创建事务ID和删除事务ID)与活跃事务列表,来确定当前事务能够看到的数据版本。具体来说,只有当数据的创建事务ID小于当前事务ID且不在活跃事务列表中,以及数据的删除事务ID要么未定义,要么大于当前事务ID时,该数据版本对当前事务可见。
  3. 生成Read-View:基于上述步骤,系统为当前事务生成一个Read-View,确保事务在其执行期间看到的是一致性的数据快照。

Read-View与 一致性无锁读

Read-View通过为每个读事务提供一个数据的一致性快照,使得读事务可以访问到执行开始时刻的数据状态,而无需等待其他并发写事务的完成。

这样,读事务和写事务可以并发执行,而不会相互阻塞,显著提高了数据库的并发性能。同时,由于读操作不需要加锁,也就降低了死锁的风险。

Read-View与事务隔离级别

Read-View的应用使得数据库能够支持不同的事务隔离级别。在MySQL/InnoDB中,最常见的隔离级别包括:

  • READ UNCOMMITTED(读未提交)
    • 在这个级别下,事务可以读取到其他事务未提交的数据,不使用Read-View
  • READ COMMITTED(读已提交)
    • 事务只能读取到其他事务已提交的数据。每次查询都会生成一个新的Read-View
  • REPEATABLE READ(可重复读)
    • 事务在开始时创建一个Read-View,并在整个事务期间使用这个Read-View,确保可以多次读取到相同的数据快照。
  • SERIALIZABLE(可串行化)
    • 最高的隔离级别,通过加锁来强制事务串行执行,通常不依赖Read-View

不同的隔离级别通过使用Read-View的方式不同,来平衡一致性、并发性和性能之间的关系。在实际应用中,选择合适的隔离级别是非常重要的,它直接影响到数据库系统的整体表现。

六、基于MVCCRead-ViewUndo-log实现一致性无锁读

有了前面的基础知识,我们来看下如何基于MVCCRead-Viewundo-log实现一致性无锁读。

首先,undo-log中提供了rebuild记录历史版本的信息(SQL语句),而MVCC则提供了重建历史版本的入口(指向undo-log的指针DB_ROLL_PTR)。

那么现在的问题就是,如何确定一致性读对应的历史版本。

因为MVCCundo-log中的版本信息是以事务ID来表示的,所以问题其实就是转化为:确定哪些事务对记录行的修改对当前可见。

★ 快照创建前已经提交的事务所做的修改对当前快照可见,快照创建后才提交的事务所做的修改,对当前快照不可见。 ”

因此一致性读的重点就是将Read-View中几个与事务ID相关的重点字段与数据库当前表数据和undo-log中DB_TRX_ID列进行对比。

★ 如果能够直接知道创建Read-View时哪些事务提交,哪些事务未提交,那么一切就会变得简单。然后并没有这样的直接渠道。 数据库中,很可能事务ID大的先提交,事务ID小的后提交。 ”

在Read-View中我们维护了low_limit_idup_limit_idtrx_ids, 将他们与MVCC、undo-log中的DB_TRX_ID列对比,即可得到哪些事务在创建Read-View已提交,哪些不是。

  1. 如果DB_TRX_ID < up_limit_id, 则修改改记录的事务在创建Read-View之前就提交了。该版本对这个一致性读可见。
  2. 如果DB_TRX_ID > low_limit_id, 则修改改记录的事务在创建Read-View时事务还未开启(更不用谈提交了),因此改版本对这个一致性读不可见。
  3. 如果up_limit_id < DB_TRX_ID < low_limit_id, 只能说明事务在创建Read-View前已经开启,但是无法判断其是否提交。需要与活跃列表进一步对比。 3.1 DB_TRX_ID在活跃列表trx_ids中,即事务还在活跃(还未提交),因此其修改不可见。 3.2 DB_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的正确打开方式(源码佐证)

附:MVCC与一致性无锁读脑图

0 人点赞