1 MVCC 基础概念
1.1 什么是 MVCC?
多版本并发控制(Multi-Version Concurrency Control,MVCC) 是一种用于数据库管理系统的并发控制机制,旨在提高系统的并发性能,特别是在读多写少的场景下。MVCC 的核心思想是通过维护数据的多个版本来实现并发操作,而不是通过传统的锁机制来限制对数据的访问。
与 锁机制(Lock-Based) 的并发控制相比,MVCC 的主要区别在于它允许多个事务同时读取数据,而不会相互阻塞。在锁机制中,读写操作通常需要互斥,即读操作会阻塞写操作,写操作也会阻塞读操作。而 MVCC 通过为每个事务提供数据的一个快照(Snapshot),使得读操作不会阻塞写操作,写操作也不会阻塞读操作。
MVCC 的核心目标是 实现高并发读写,避免读写冲突,从而提升系统的整体性能。
1.2 MVCC 解决了什么问题?
MVCC 主要解决了以下几个问题:
- 读写操作之间的阻塞问题:在传统的锁机制中,读写操作通常会相互阻塞。MVCC 通过为每个事务提供数据的一个快照,使得读操作不会阻塞写操作,写操作也不会阻塞读操作,从而提高了系统的并发性能。
- 事务隔离性:MVCC 能够实现不同的事务隔离级别,如 MySQL 中的 REPEATABLE READ。通过 MVCC,事务可以看到一个一致的数据视图,即使在事务执行期间其他事务对数据进行了修改。
- 数据版本历史追溯:MVCC 通过维护数据的多个版本,使得系统能够追溯数据的历史状态。这在某些场景下非常有用,例如 etcd 的 Watch 机制,可以监控键值对的变化历史。
[!NOTE] REPEATABLE READ REPEATABLE READ 是 MySQL 默认的事务隔离级别,它通过 MVCC(多版本并发控制)机制确保在同一个事务中多次读取同一数据时,结果是一致的,即使其他事务在此期间修改了这些数据。它避免了脏读和不可重复读,并通过 Next-Key Locking(间隙锁)部分避免了幻读。然而,这种隔离级别可能会带来锁争用和存储开销,尤其是在长事务场景下。
1.3 核心组成要素
MVCC 的实现依赖于以下几个核心组成要素:
- 版本号(Version/Timestamp):每个数据版本都有一个唯一的版本号或时间戳,用于标识该版本的创建时间或事务 ID。版本号通常由系统自动生成,并随着事务的提交而递增。
- 数据快照(Snapshot):MVCC 为每个事务提供一个数据快照,该快照包含了事务开始时数据库中所有数据的可见版本。事务在读取数据时,只能看到快照中的数据,而不会受到其他事务修改的影响。
- 可见性规则(Visibility Rules):MVCC 通过可见性规则来确定哪些数据版本对当前事务是可见的。通常,事务只能看到在它开始之前已经提交的数据版本,而看不到未提交的或在其开始之后提交的数据版本。
- 垃圾回收机制(旧版本数据清理):由于 MVCC 会保留数据的多个版本,随着时间的推移,旧版本的数据可能会占用大量存储空间。因此,MVCC 需要一种垃圾回收机制来定期清理不再需要的旧版本数据,以防止存储空间的无限膨胀。
2 MVCC 在 MySQL 中的实现
MySQL 通过 MVCC(多版本并发控制) 机制来实现高并发的事务处理,特别是在 REPEATABLE READ 隔离级别下,MVCC 能够有效避免读写冲突,提供一致性读取。以下是 MySQL 中 MVCC 的核心实现机制。
2.1 关键数据结构
每行数据包含两个隐藏字段:
DB_TRX_ID
:记录最后修改该行数据的事务 ID。DB_ROLL_PTR
:指向该行数据在 Undo Log 中的历史版本。
Undo Log 用于存储数据的历史版本,支持事务回滚和 MVCC。它的主要结构包括:
- Insert Undo Log:记录插入操作的历史版本,用于事务回滚。
- Update Undo Log:记录更新和删除操作的历史版本,用于事务回滚和 MVCC。
2.2 核心机制
MySQL 使用 Undo Log 来存储数据的历史版本。每当一行数据被修改时,MySQL 会将该行的旧版本数据写入 Undo Log,同时更新当前行的 DB_TRX_ID
(事务 ID)和 DB_ROLL_PTR
(回滚指针)。通过 Undo Log,MySQL 能够构建数据的版本链,支持事务回滚和历史版本查询。
每个事务在第一次读取数据时会创建一个 Read View(可重复读隔离级别,如果是读提交,那么每次读取数据都会创建),用于确定哪些数据版本对当前事务是可见的。Read View 包含以下信息:
- 当前活跃事务的列表(未提交的事务)。
- 当前事务开始时已提交的最小事务 ID。
- 当前事务的事务 ID。
事务在读取数据时,会根据 Read View 和 DB_TRX_ID
判断数据版本是否可见:
- 如果
DB_TRX_ID
小于 Read View 的最小事务 ID,说明该版本已提交,对当前事务可见。 - 如果
DB_TRX_ID
在活跃事务列表中,说明该版本未提交,对当前事务不可见。 - 如果
DB_TRX_ID
大于当前事务的事务 ID,说明该版本在当前事务开始之后提交,对当前事务不可见。 - 通过 Undo Log 递归查找,直到找到满足第一点的版本,读取这个版本即可。
每一行数据都有一个隐藏字段 DB_ROLL_PTR
,指向该行数据在 Undo Log 中的历史版本。通过 DB_ROLL_PTR
,MySQL 可以构建一条版本链,从最新版本回溯到旧版本。事务在读取数据时,会遍历版本链,找到对当前事务可见的最新版本。
2.3 与事务隔离级别的关系
在 REPEATABLE READ 隔离级别下,事务在第一次读取数据时会创建一个 Read View,并在整个事务生命周期中复用该 Read View。因此,事务在多次读取同一数据时,结果是一致的,即使其他事务在此期间修改了数据。
虽然 MVCC 通过 Read View 和版本链避免了脏读和不可重复读,但它无法完全解决幻读问题。为了防止幻读,MySQL 在 REPEATABLE READ 隔离级别下使用了 Next-Key Locking(间隙锁)。Next-Key Locking 不仅锁定当前查询到的行,还锁定相邻的间隙,防止其他事务在范围内插入新数据。
3 MVCC 在 Etcd 中的实现
3.1 设计目标与场景
- 高可用与强一致性:作为分布式系统的核心协调组件(如 Kubernetes 的元数据存储),etcd 需要保证数据的强一致性和容错能力(基于 Raft 共识算法)。
- 历史版本追溯:支持分布式系统的 Watch 机制(监听键值变化)和版本回滚需求(如配置错误恢复)。
- 高性能并发读写:避免锁竞争,支持高吞吐量的读操作(如 Kubernetes 频繁查询 Pod 状态)。
3.2 核心机制
- 全局单调递增的版本号(Revision):每个键值对的修改(Put/Delete)都会生成一个全局唯一的
Revision
(单调递增的整数)。所有键的修改共享同一 Revision 序列,保证操作的全局有序性。 - 键值存储的版本化设计:etcd 使用 B+ 树(或 BoltDB 的优化结构) 存储数据,每个键的历史版本按 Revision 排序。每个键的多个版本按
Revision
顺序存储,形成版本链。 - Watch 机制的实现:客户端通过
Watch
接口监听键的变化,指定起始 Revision 即可获取后续所有变更事件。
4 MySQL Vs Etcd 的 MVCC 对比
特性 | MySQL | etcd |
---|---|---|
目标场景 | 关系型事务处理(OLTP) | 分布式系统协调、配置管理 |
版本存储 | Undo Log + 行版本链 | 全局版本号 + 键值历史版本存储 |
隔离性实现 | 通过 Read View 控制事务可见性 | 基于版本号的线性一致性(Linearizable) |
垃圾回收 | 后台 Purge 线程清理 Undo Log | Compact 操作手动/自动触发 |
版本查询能力 | 有限(依赖 Undo Log 保留时间) | 直接通过版本号访问历史数据 |
5 MVCC 的优缺点
- 优势
- 提升并发性能(减少锁竞争)
- 支持非阻塞读取
- 天然支持数据历史版本(如审计、回滚)
- 局限性
- 存储空间开销(需保留多版本数据)
- 垃圾回收机制的设计复杂度
- 对长事务的敏感性(如 MySQL 的 Undo Log 膨胀)
6 应用场景
- 适合使用 MVCC 的场景
- 高并发读多写少的系统(如电商库存)
- 需要事务隔离性的数据库
- 分布式系统状态同步(如 Kubernetes 使用 etcd)
- 不适用场景
- 写密集型系统(版本管理开销过大)
- 对实时一致性要求极高的场景