MVCC(多版本并发控制)的实现
ps:如果这篇帖子对于还在找工作和找实习的你有所帮助,可以关注我,给本贴点赞、评论、收藏并订阅专栏;同时不要吝啬您的花花
MVCC(多版本并发控制)的实现
MVCC(Multi-Version Concurrency Control,多版本并发控制)是数据库为解决并发读写冲突、提升并发性能而设计的核心机制,其核心思想是:为每一条数据维护多个版本,不同事务根据自身的隔离级别,读取对应版本的数据,从而避免读写互斥,实现“读不加锁、写不阻塞读”的效果。主流关系型数据库(如MySQL InnoDB、PostgreSQL)均采用MVCC机制,其中InnoDB的实现最具代表性,以下围绕InnoDB的MVCC实现展开详细解析。
一、MVCC实现的核心前提
MVCC的实现依赖两个关键基础:事务的隔离级别(主要支撑可重复读、读已提交隔离级别)和数据的版本管理。其中,InnoDB通过“事务ID”和“版本链”来标记数据的不同版本,确保不同事务能精准获取自身可访问的数据版本,同时不干扰其他事务的读写操作。
二、MVCC实现的关键组件
InnoDB的MVCC实现,主要依赖以下4个核心组件,四者协同工作,完成版本的生成、管理和读取。
1. 事务ID(Transaction ID, trx_id)
数据库会为每一个启动的事务分配一个唯一的自增事务ID,事务ID的分配遵循“递增原则”,即后启动的事务,其trx_id大于先启动的事务。事务ID的作用是标记“数据版本由哪个事务生成”,以及判断事务之间的先后顺序,为版本可见性判断提供依据。
注意:只有执行了“写操作”(insert、update、delete)的事务,才会分配trx_id;只读事务(仅执行select)不会分配trx_id,会复用当前已有的事务ID或使用一个临时ID,以此减少资源消耗。
2. 隐藏字段
InnoDB会为表中的每一行数据,自动添加3个隐藏字段(用户不可见,由数据库维护),用于存储版本相关信息,这是MVCC实现的核心载体:
- db_trx_id:记录生成当前数据版本的事务ID(即“谁修改了这条数据”)。初始插入时,该字段为插入事务的trx_id;后续每一次update,都会生成新的版本,并用当前更新事务的trx_id覆盖该字段。
- db_roll_ptr:回滚指针,指向当前数据版本对应的“undo log记录”,通过该指针可以串联起当前数据的所有历史版本,形成“版本链”。
- db_row_id:行标识,用于唯一标识一行数据(若表没有主键或唯一索引,InnoDB会自动用该字段作为主键),与MVCC核心逻辑关联不大,主要用于行定位。
3. Undo Log(回滚日志)
Undo Log是InnoDB的事务日志之一,核心作用有两个:一是事务回滚时,通过undo log恢复数据(撤销当前事务的写操作);二是为MVCC提供历史版本数据,即每一次写操作(insert/update/delete)都会生成对应的undo log记录,这些记录会被db_roll_ptr串联起来,形成版本链。
不同操作的undo log类型不同,对应不同的版本维护逻辑:
- Insert操作:生成insert undo log,记录插入的数据内容;事务提交后,该undo log可被清理(因为插入的行之前不存在,无历史版本关联)。
- Update/Delete操作:生成update undo log,记录修改前的数据内容(即历史版本);即使事务提交,该undo log也不能立即清理,需等待所有依赖该历史版本的事务结束后,才会被purge线程清理(避免读取不到历史版本)。
4. Read View(读视图)
Read View(读视图)是事务执行select操作时,生成的一个“快照”,用于判断当前事务能看到哪些数据版本。简单来说,Read View相当于一个“版本过滤器”,通过判断数据版本的trx_id与Read View中的参数,决定该版本是否对当前事务可见。
Read View包含4个核心参数,这些参数由当前事务启动时的数据库状态决定,一旦生成,在事务期间(可重复读隔离级别)不会变化:
- m_ids:当前正在执行的所有事务的ID集合(即“活跃事务ID列表”)。
- min_trx_id:m_ids中的最小事务ID(当前活跃事务中,最早启动的事务ID)。
- max_trx_id:当前数据库中,下一个即将分配的事务ID(比所有已分配的trx_id都大)。
- creator_trx_id:当前生成Read View的事务ID(即当前事务自身的trx_id)。
三、MVCC的核心工作流程(以InnoDB可重复读为例)
MVCC的工作流程主要分为“版本生成”和“版本读取”两个阶段,结合上述组件,完整流程如下:
1. 版本生成(写操作触发)
当事务执行insert、update、delete操作时,InnoDB会为数据生成新的版本,同时记录undo log,更新隐藏字段,具体步骤:
- 事务启动,分配唯一trx_id(只读事务除外)。
- 执行写操作时,先读取当前数据的最新版本,将其复制一份作为历史版本,存入undo log。
- 修改当前数据的内容,更新数据的db_trx_id为当前事务的trx_id,更新db_roll_ptr指向刚刚生成的undo log记录(串联历史版本)。
- 若后续还有事务对该数据执行写操作,会重复上述步骤,形成“版本链”(最新版本在数据表中,历史版本在undo log中,通过db_roll_ptr串联)。
示例:事务A(trx_id=100)更新了一行数据,会生成该数据的历史版本(存于undo log),当前数据的db_trx_id=100,db_roll_ptr指向undo log中的历史版本;后续事务B(trx_id=200)再次更新该数据,会生成新的历史版本(指向事务A更新后的版本),当前数据的db_trx_id=200,形成版本链:当前版本(trx_id=200)→ 历史版本1(trx_id=100)→ 初始版本(insert时的trx_id)。
2. 版本读取(读操作触发)
当事务执行select操作时,会生成Read View,然后遍历数据的版本链,根据Read View的参数判断每个版本是否可见,最终返回符合条件的最新可见版本,具体判断规则如下:
- 若当前版本的db_trx_id = creator_trx_id:该版本是当前事务自身修改的,可见。
- 若当前版本的db_trx_id < min_trx_id:该版本是在所有活跃事务启动前生成的,可见。
- 若当前版本的db_trx_id >= max_trx_id:该版本是在当前事务启动后生成的,不可见,需通过db_roll_ptr读取上一个历史版本,重新判断。
- 若min_trx_id ≤ db_trx_id < max_trx_id:判断该db_trx_id是否在m_ids(活跃事务列表)中。若不在,说明生成该版本的事务已提交,可见;若在,说明该事务仍在活跃,不可见,需读取上一个历史版本,重新判断。
关键说明:可重复读隔离级别下,Read View仅在事务第一次执行select时生成,后续所有select操作复用该Read View,因此能保证“同一事务内,多次读取同一数据,结果一致”;而读已提交隔离级别下,每一次select都会重新生成Read View,因此能看到其他事务已提交的最新版本。
具体实例(涵盖所有版本可见性判断情况,基于可重复读隔离级别)
假设场景:数据库中存在一张user表,其中有一条初始数据:id=1,name="张三",age=20,该初始版本由事务T0(trx_id=50,已提交)插入。当前数据库中活跃事务及相关信息如下:
- 事务T1:trx_id=100,未提交,执行操作:update user set age=22 where id=1;(生成版本2)
- 事务T2:trx_id=200,未提交,执行操作:update user set age=24 where id=1;(生成版本3,基于版本2)
- 事务T3:trx_id=300,已提交,执行操作:update user set age=21 where id=1;(生成版本1,基于初始版本,已提交)
- 事务T4:trx_id=400,当前执行select操作(首次执行),生成Read View,参数为:m_ids={100,200}(活跃事务)、min_trx_id=100、max_trx_id=500(下一个待分配ID)、creator_trx_id=400
此时id=1的数据版本链(从新到旧):版本3(db_trx_id=200,age=24)→ 版本2(db_trx_id=100,age=22)→ 版本1(db_trx_id=300,age=21)→ 初始版本(db_trx_id=50,age=20)。事务T4读取该数据时,会按以下规则判断每个版本的可见性,对应所有4种情况:
- 情况1:db_trx_id = creator_trx_id(对应规则1)假设事务T4(trx_id=400)先执行update user set age=25 where id=1;(生成版本4,db_trx_id=400),再执行select操作。此时版本4的db_trx_id=400,与creator_trx_id(400)相等,说明该版本是T4自身修改的,因此T4能看到版本4(age=25)。
- 情况2:db_trx_id < min_trx_id(对应规则2)初始版本的db_trx_id=50,min_trx_id=100,50<100,说明该版本是在所有活跃事务(T1、T2)启动前生成的(T0已提交),因此T4能看到该版本(age=20)。
- 情况3:db_trx_id >= max_trx_id(对应规则3)假设此时有新事务T5启动(trx_id=500,未提交),并更新该数据生成版本5(db_trx_id=500,age=26)。T4的max_trx_id=500,版本5的db_trx_id=500,满足“db_trx_id >= max_trx_id”,说明该版本是T4启动后生成的,不可见。T4会通过db_roll_ptr读取上一个版本(版本3),重新判断。
- 情况4:min_trx_id ≤ db_trx_id < max_trx_id(对应规则4)- 版本1(db_trx_id=300):100≤300<500,且300不在m_ids({100,200})中,说明生成该版本的T3已提交,因此T4能看到版本1(age=21);- 版本2(db_trx_id=100):100≤100<500,且100在m_ids中,说明T1仍活跃,该版本不可见,T4读取上一个版本(版本1);- 版本3(db_trx_id=200):100≤200<500,且200在m_ids中,说明T2仍活跃,该版本不可见,T4读取上一个版本(版本2),再判断版本2不可见后,最终读取到可见的版本1(age=21)。
补充:事务T4最终读取到的是版本1(age=21),因为它是版本链中最新的可见版本。若T4再次执行select操作(可重复读隔离级别),会复用之前的Read View,即使此时T1提交(m_ids变化),T4仍会读取到age=21,保证“可重复读”;若为读已提交隔离级别,T4再次select会重新生成Read View(m_ids={200}),此时版本2(T1已提交)会变为可见,读取到age=22。
3. 快照读与当前读的区别
在InnoDB中,并发读取数据存在两种方式——快照读和当前读,二者基于MVCC机制实现,核心区别在于“是否读取数据的最新版本”以及“是否加锁”,具体对比如下:
- 快照读(Snapshot Read):即普通select操作(无for update、lock in share mode),是MVCC的核心读方式。读取的是数据的“历史版本”,通过Read View过滤版本链获取可见快照,无需加锁,实现“读不加锁、写不阻塞读”。例如:select * from table where id = 1;,该操作会生成Read View,遍历版本链返回符合条件的可见版本,不会阻塞其他事务的写操作,也不会被其他写操作阻塞。
- 当前读(Current Read):读取的是数据的“最新版本”,会对读取的行加锁,阻塞其他事务的写操作,避免并发冲突。触发当前读的场景包括:带有for update(加X锁)、lock in share mode(加S锁)的select操作,以及insert、update、delete操作(写操作执行前需先读取最新版本,本质也是当前读)。例如:select * from table where id = 1 for update;,该操作会读取当前数据的最新版本,并对该行加X锁,其他事务无法对该行执行写操作,直至当前事务释放锁。
补充说明:快照读依赖MVCC的版本链和Read View实现,仅适用于可重复读、读已提交隔离级别;当前读依赖锁机制,适用于所有隔离级别,核心是保证数据读取的“实时性”,避免脏读、不可重复读等问题,与MVCC协同工作,兼顾并发性能与数据一致性。
四、MVCC的优势与局限
1. 优势
- 读写不阻塞:读操作无需加锁,写操作仅阻塞其他写操作,大幅提升数据库并发性能(尤其适合读多写少的场景)。
- 避免幻读:在可重复读隔离级别下,通过Read View和版本链,实现了“快照读”,避免了幻读(InnoDB通过Next-Key Lock解决了间隙幻读,与MVCC协同工作)。
- 事务隔离:通过版本可见性判断,精准实现不同隔离级别的需求,兼顾并发与数据一致性。
2. 局限
- 资源消耗:需维护undo log和版本链,占用额外的磁盘和内存资源;purge线程需定期清理过期的undo log,增加了数据库的维护成本。
- 写操作开销:每一次写操作都会生成新的版本和undo log,比“锁机制”的写操作开销略高。
- 仅适用于InnoDB:MySQL的MyISAM引擎不支持MVCC,因为MyISAM不支持事务和undo log。
五、总结
MVCC的本质是“通过版本管理实现并发控制”,核心依赖事务ID、隐藏字段、undo log和Read View四大组件:写操作生成新的版本并记录undo log,读操作通过Read View过滤版本链,获取可见版本。其核心价值是在保证数据一致性的前提下,最大化提升数据库的并发读写性能,是现代关系型数据库实现高并发的关键机制。
ps:如果这篇帖子对于还在找工作和找实习的你有所帮助,可以关注我,给本贴点赞、评论、收藏并订阅专栏;同时不要吝啬您的花花
查看9道真题和解析