Mysql的MVCC原理

Mysql的MVCC原理
DreamCollector一、概述
MVCC
(多版本并发控制)是一种常用于数据库管理系统中的并发控制方法,旨在通过为每个事务提供数据的多个版本,从而避免并发事务间的数据冲突。MVCC的核心优势在于读操作无需加锁,从而实现读不阻塞写,同时写操作不会阻塞其他事务的读操作。这种方式特别适用于读多写少的应用场景,如 OLTP 系统。在传统的基于锁的并发控制(Lock-based Concurrency Control)中,多个事务在访问共享数据时需要加锁,这就会导致在高并发环境下的性能瓶颈。而MVCC通过在事务间维护数据的多个版本,能够显著提高并发性能。
二、MVCC的实现原理
MVCC的实现依赖于三个关键要素:
- 隐式字段:每条记录都会附带一些额外的隐式字段,用于标识记录的版本信息。
- 版本链:每次更新数据时,都会生成新的版本并通过回滚指针链接到旧版本,形成版本链。
- Read View:每个事务会在开始时生成一个快照(Read View),该快照决定了该事务在其生命周期内能够访问到哪些数据版本。
1. 隐式字段
每行记录除了我们自定义的字段外,还有数据库隐式定义的 DB_TRX_ID
, DB_ROLL_PTR
, DB_ROW_ID
等字段。
DB_TRX_ID:6 byte,最近修改(修改/插入)事务 ID:记录创建这条记录/最后一次修改该记录的事务 ID
DB_ROLL_PTR:7 byte,回滚指针,指向这条记录的上一个版本(存储于 rollback segment 里)
DB_ROW_ID:6 byte,隐含的自增 ID(隐藏主键),如果数据表没有主键,InnoDB 会自动以DB_ROW_ID产生一个聚簇索引
实际还有一个删除DELETED_FLAG隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除 flag 变了
2. 版本链
每次对记录进行修改时,数据库不会覆盖原有数据,而是通过创建一个新的数据版本来保存变更。这些新的版本会通过回滚指针链接成一个版本链。
- 每一条记录都有一个版本链,版本链的头结点是当前记录的最新版本。
- 回滚指针指向上一个版本,通过回滚指针,数据库能够维护记录的历史版本,确保支持事务回滚和数据快照。
当一个事务修改了数据时,系统会将修改后的数据版本放到版本链的头部,之前的版本则会继续保留。
3. Undo日志
Undo日志是数据库中的一种日志机制,用于记录事务执行前的数据状态,以支持事务的回滚操作。每当事务对数据进行修改时,数据库会生成对应的Undo日志,并保存修改前的记录快照。如果事务出现错误或需要回滚,数据库就可以通过Undo日志将数据恢复到修改前的状态。
Undo日志的作用:
- 事务回滚:在事务执行过程中,如果出现错误或事务被显式回滚,数据库通过Undo日志来撤销所有已执行的修改,将数据恢复到事务开始之前的状态。
- 支持快照:Undo日志不仅记录了事务回滚的信息,也为MVCC提供了数据的历史版本。每次修改数据时,数据库会生成一个新的版本,同时保存Undo日志,以便在回滚时恢复。
Undo日志的工作原理:
- 当事务执行修改操作时,Undo日志会记录修改前的数据。
- 如果事务回滚或发生故障,数据库将利用Undo日志恢复被修改的数据。
- Undo日志通常是顺序写入的,与版本链不同,版本链是通过回滚指针在记录间进行链接的。
4. Read View(读取视图)
为了保证事务的一致性和隔离性,MVCC依赖于Read View来确定当前事务可以读取的数据版本。每个事务在开始时都会生成一个独立的Read View
,该视图定义了当前事务能够访问的版本范围。
- 生成Read View:当事务开始时,系统会为事务生成一个快照,该快照记录了事务开始时的所有版本信息。
- 隔离级别的影响:在
READ COMMITTED
和REPEATABLE READ
两个隔离级别下,Read View会影响事务能看到的数据。具体来说:- 读已提交:每次select时生成一个read-view,并且可以看到其他事务已经提交的数据(即使这些数据是本事务之外的其他事务修改的)。这种级别提供较高的并发性,但可能会导致不可重复读问题。
- 可重复读:在事务开始时生成
read-view
,并且在整个事务内,这个read-view
不会改变。即使其他事务提交了数据,本事务也只能读取到其事务开始时的版本,这就避免了不可重复读问题。
三、MVCC的工作原理
1. 事务的快照和隔离
每个事务在启动时生成一个独立的快照,即Read View
。该快照包含了当前事务能看到的数据版本范围。在执行读操作时,数据库会根据事务的快照来选择合适的版本:
- 对于
SELECT
语句,数据库会检查当前数据的DB_TRX_ID
,并根据事务的快照判断该版本是否对事务可见。 - 事务提交后,如果没有冲突,数据库会将该事务的修改提交至数据库,并成为新的版本。
- 事务回滚后,该事务所修改的数据会被回滚至原始版本,版本链中的其他事务版本依然可见。
2. 版本链的管理
数据库会为每条记录维护一个版本链。每次记录被修改时,系统会生成一个新的数据版本,并通过回滚指针将其链接到旧版本。例如:
- 初始状态:记录版本V0。
- 第一次更新:生成新的版本V1,V0成为V1的上一个版本(通过回滚指针)。
- 第二次更新:生成新的版本V2,V1成为V2的上一个版本。
在查询时,数据库会根据事务的Read View
判断哪些版本对当前事务可见,并返回符合条件的版本。
3. 事务间的冲突
在高并发环境中,可能会存在多个事务同时修改同一数据的情况。MVCC能够通过版本链和回滚指针有效地解决此类问题:
- 读操作:每个事务读取的数据版本是根据事务开始时的快照来确定的,这样避免了其他事务的更新操作对当前事务的影响。
- 写操作:当事务修改数据时,它会创建新的版本,并且不会直接修改现有版本,从而避免了锁争用问题。
4. 隔离级别与MVCC
MVCC的行为依赖于数据库的事务隔离级别。常见的隔离级别包括:
- 读未提交—READ UNCOMMITTED:事务可以读取其他事务未提交的数据,MVCC不适用。
- 读已提交—READ COMMITTED:每次读取时生成新的
Read View
,保证读取的数据已提交,但可能会出现不可重复读的问题。 - 可重复读—REPEATABLE READ:事务在开始时生成一个固定的
Read View
,保证在整个事务过程中,读取的数据始终保持一致。 - 序列化—SERIALIZABLE:最严格的隔离级别,所有事务都被串行化执行,通常会涉及加锁,MVCC在此级别下表现较差。
四、总结
优势
- 高并发性:读操作不加锁,能够有效提高并发性能。
- 避免死锁:由于不需要加锁,避免了传统锁机制下可能出现的死锁问题。
- 读取一致性:通过事务快照和版本链,保证事务内的一致性。
不足
- 存储开销:由于需要维护版本链和历史数据,存储空间消耗较大。
- 版本管理复杂:频繁更新的表可能会导致版本链非常长,版本管理变得复杂。
- 回滚性能问题:如果版本链很长,回滚操作可能会变得较慢。
graph LR MVCC --> 版本链 MVCC --> ReadView MVCC --> 核心问题 MVCC --> 实现 MVCC --> 关于undo %% 版本链部分 版本链 --> 三个隐藏列 三个隐藏列 --> row_id["DB_ROW_ID:自增ID(隐藏主键)"] 三个隐藏列 --> trx_id["DB_TRX_ID:最近修改该记录的事务ID"] 三个隐藏列 --> roll_pointer["DB_ROLL_PTR:回滚指针,指向旧版本"] roll_pointer --> 旧版本["旧版本串成单链表,版本链头结点为最新值"] %% ReadView 部分 ReadView --> ReadView概念["ReadView是事务可见性判断的关键"] ReadView --> ReadView生成["READ COMMITTED 每次查询生成新 ReadView"] ReadView --> ReadView持久性["REPEATABLE READ 事务期间 ReadView 不变"] %% 核心问题部分 核心问题 --> 版本可见性判断["事务只能访问 ReadView 允许的版本"] 核心问题 --> 版本链访问逻辑["遍历版本链,找到符合 ReadView 规则的版本"] %% undo日志部分 关于undo --> undo日志概念["Undo日志存储历史版本,支持MVCC与回滚"] 关于undo --> undo日志存储["Undo日志存储于回滚段(Rollback Segment)"] 关于undo --> undo日志作用 undo日志作用 --> 事务回滚["事务失败时,Undo日志可撤销操作"] undo日志作用 --> 版本管理["支持MVCC,提供旧版本供事务访问"] %% 实现部分 实现 --> 事务提交["提交后新版本成为最新版本"] 实现 --> 事务回滚["回滚时通过Undo日志恢复旧版本"] 实现 --> 版本清理["历史版本可能被垃圾回收"]