MVCC(多版本并发控制)
总结
MVCC(多版本并发控制)核心思想是用空间换时间,主要解决了读写冲突,用额外的存储空间(Undo Log 和行版本信息)来换取读写操作的并行执行,从而实现高并发。
MVCC 的实现依赖于 一致性读视图,并通过可见性判断算法(MVCC中的可见性判断规则),判断决定事务能够看到哪个版本的数据。这个过程与 Undo Log 紧密相连,因为历史版本的数据就存储在 Undo Log 中。
尽管 MVCC 有存储和计算上的额外开销,并可能引发长事务问题,但巨大的并发性能优势,使其成为现代主流数据库不可或缺的核心技术。
详情
1. 为什么引入 MVCC?
为了解决这种读写操作之间相互阻塞的问题,提升数据库的并发性能,MVCC(Multi-Version Concurrency Control,多版本并发控制)应运而生。
核心思想:用空间换时间,为数据保留多个历史版本。读取操作可以去读取一个旧的、但一致的版本,而不会被当前的写入操作所阻塞。
2. MVCC 解决了什么问题?
MVCC 主要解决了以下核心问题:
- 读写并发冲突:这是 MVCC 解决的最主要问题。
- 写操作在最新的数据版本上创建新版本
- 而读操作根据其事务的启动时间和隔离级别,去查找一个合适的历史版本进行读取。两者互不干扰,无需加锁等待。
- 降低锁竞争
- MVCC 极大地减少了对数据加锁的需求,尤其是对于读操作,显著提高了并发性能。
- 这种无锁读取通常被称为“快照读”(Snapshot Read)。详见 快照读与当前读。
- 实现特定隔离级别
- MVCC 是实现“读已提交”(Read Committed, RC)和“可重复读”(Repeatable Read, RR)这两个事务隔离级别的标准方式。
- 它通过“一致性读视图”(Consistent Read View)来保证不同事务在不同的时间点能看到符合其隔离级别要求的数据快照。
3. MVCC 有什么优缺点?
优点
- 高并发性能:读操作快,无需加锁。读写操作不互斥,提升了数据库的并发处理能力。
- 减少死锁:读操作不加锁,降低了发生死锁的概率。
- 实现一致性读:在不加锁的情况下,为事务提供一个一致的数据视图,保证事务在执行期间看到的数据状态是一致的。
缺点
- 额外的存储开销:需要为表中的每一行数据存储额外的版本信息(例如,事务 ID 和回滚指针)。Undo Log(回滚日志)会因为需要保留历史版本而变得更大,占用了更多的存储空间。
- 额外的计算开销:当一个事务需要读取数据时,数据库需要遍历版本链,根据可见性算法来判断哪个版本是对当前事务可见的。这带来了一定的 CPU 计算开销。
- 可能导致“长事务”问题:如果一个事务长时间不提交,系统为了保证其可见性,就必须保留该事务所需要的所有历史版本数据,这会导致 Undo Log 无法及时清理,占用大量空间,甚至可能影响到整个系统的性能。
4. MVCC 有什么影响?
MVCC 的引入对数据库系统的设计和使用产生了深远的影响:
- 数据库架构的改变:它成为了现代主流关系型数据库(如 InnoDB, PostgreSQL, Oracle)的并发控制核心机制。数据库内核需要设计复杂的数据结构(如版本链)和可见性判断算法。
- 应用开发模式的改变:开发者可以更加放心地编写高并发的应用,而不必过于担心读写锁冲突导致的性能瓶颈。同时也需要理解 MVCC 的行为,以避免在特定隔离级别下出现意料之外的结果(如幻读)。
- 运维的挑战:DBA 需要关注 Undo Log 的增长情况,监控并处理长事务,以保证数据库的稳定运行。
5. 关联的知识点
理解 MVCC 需要结合以下知识点,它们共同构成了数据库事务和并发控制的完整图景:
- 事务隔离级别(Transaction Isolation Levels):MVCC 是实现 RC、RR 的基石。不同的隔离级别,其生成 一致性读视图(Read View)的时机也不同,从而决定了事务能看到什么样的数据。
- Undo Log(回滚日志):Undo Log 是 MVCC 实现多版本的物理基础。它不仅用于事务回滚,更关键的是,它存储了数据的历史版本。MVCC 正是通过 Undo Log 中的记录来构建出数据的旧版本。
- 快照读与当前读
- 快照读:就是 MVCC 的读,读取的是数据版本的快照,不加锁。普通的
SELECT
就是快照读。 - 当前读:读取的是数据的最新版本,并且会对读取的记录加锁,保证其他并发事务不能修改。
SELECT ... FOR UPDATE
,SELECT ... LOCK IN SHARE MODE
,INSERT
,UPDATE
,DELETE
都是当前读。
- 快照读:就是 MVCC 的读,读取的是数据版本的快照,不加锁。普通的
- 锁(Locking):MVCC 并不能完全取代锁。对于“写 - 写”冲突,仍然需要使用锁来解决。例如,当两个事务同时
UPDATE
同一行数据时,后一个事务必须等待前一个事务提交或回滚,这个等待就是通过行级锁(Row-level Lock)实现的。