MVCC(多版本并发控制)原理实现
多版本并发控制(MVCC,Multi-Version Concurrency Control),是一种并发控制的方法。
MySQL InnoDB巧妙地利用了隐藏列、事务ID、Read View以及Undo Log等技术,实现了多版本并发控制,使得不同事务能够几乎同时访问数据库而不相互阻塞,极大地提升了数据库系统的并发性能和用户体验。
一、实现组件
事务ID和系统版本号(syscanf_version)
每个事务都有一个全局唯一的事务ID(trx_id),这个ID随着事务的开始而递增。
在InnoDB内部,还有一个系统版本号(syscanf_version),也是随着事务的执行不断增长。
隐藏列与行格式
InnoDB表中每一行数据除了用户定义的列外,还有额外的隐藏列:
-
DB_TRX_ID: 记录最后一次修改该行数据的事务ID。
-
DB_ROLL_PTR: 回滚指针,指向Undo Log中的相应条目,用于撤销操作或者构建历史版本。
-
DB_ROW_ID(可选): 对于没有主键的表,InnoDB会自动生成一个隐含的ROW ID作为聚簇索引的一部分。
Undo Log(回滚日志)
当事务对数据进行修改时,InnoDB不仅在当前的数据页上更新数据,还会在Undo Log(两种类型)中记录旧值以及修改前的状态。
INSERT Undo Log: 记录插入操作,主要用于事务回滚时删除新插入的行。
UPDATE/DELETE Undo Log: 记录更新或删除操作,包含被修改前的行数据,用于事务回滚时恢复原状,同时也为其他事务提供历史版本的数据。
Read View(读视图)
在"可重复读"隔离级别下,每个事务启动时会创建并固定一个Read View,之后的所有一致性非锁定读都会基于这个视图来判断数据可见性,Read View包含了以下关键信息。
- m_ids[]: 数组存储了所有未提交且活跃的事务ID列表。
- low_limit_id: 所有小于等于这个值的事务ID已经提交完成。
- up_limit_id: 下一个即将分配给事务的ID,表示尚未分配事务ID的最大值。
- creator_trx_id: 创建此Read View的事务自身的事务ID。
二、数据可见性判断
当事务执行SELECT查询时,针对每行数据,根据Read View和该行的DB_TRX_ID来判断是否可见:
-
DB_TRX_ID小于low_limit_id: 说明该行是在当前事务开始之前就已经提交的,因此对该事务是可见的。
-
DB_TRX_ID大于等于up_limit_id: 说明该行是由在ReadView之后才开启的事务修改或插入的,因此对当前事务不可见。
-
DB_TRX_ID位于low_limit_id和up_limit_id之间:
- 若DB_TRX_ID不在ReadView的m_ids列表中,则该事务已提交,数据行对当前事务可见。
- 若DB_TRX_ID在ReadView的m_ids列表中,则该事务尚未提交,数据行对当前事务不可见。
对于不可见的行,通过DB_ROLL_PTR找到对应的Undo Log,并从中获取在Read View创建时刻该行的最新已提交版本,以便当前事务查看。
在"可重复读"隔离级别下,由于Read View在事务开始时就固定了,所以即使后续有新的事务插入满足查询条件的新行,这些新行也不会影响当前事务的查询结果,从而避免了幻读问题。
三、可见性描述
一个代码片段,用于简单演示Read View与事务ID的对比逻辑。
java
// 假设Transaction类代表一个MySQL InnoDB中的事务,它有trxId属性表示当前事务ID
class Transaction {
long trxId; // 当前事务ID
// 创建一个新的读视图
ReadView createReadView() {
return new ReadView(this.trxId);
}
}
java
// 代表InnoDB中的一行记录,包含DB_TRX_ID等隐藏列
class InnodbRow {
long dbTrxId; // 最后修改该行的事务ID
Object[] data; // 用户数据
RollbackPointer rollbackPtr; // 回滚指针
// 判断此行对于给定ReadView是否可见
boolean isVisibleTo(ReadView readView) {
if (dbTrxId < readView.lowLimitId) {
// 已提交事务修改,对当前事务可见
return true;
} else if (readView.isTrxIdInRange(dbTrxId)) {
// 未提交事务或已提交但在此视图创建后,对当前事务不可见
return false;
} else {
// 不可能出现的情况,理论上应该为已提交事务
throw new IllegalStateException("Invalid transaction ID state");
}
}
}
java
// ReadView类存储了事务在可重复读隔离级别下看到的数据版本范围
class ReadView {
long lowLimitId; // 已提交事务的最小ID界限
Set<Long> activeTransactionIds; // 当前未提交的事务ID集合
long upLimitId; // 下一个待分配的事务ID(即活跃事务的最大值)
long creatorTrxId; // 创建此ReadView的事务ID
// 检查给定的事务ID是否在当前活跃的未提交事务范围内
boolean isTrxIdInRange(long trxId) {
return activeTransactionIds.contains(trxId);
}
}
java
// 假设有两个事务tx1和tx2,以及一行数据row
Transaction tx1 = new Transaction(); // 初始化事务tx1并获取其事务ID
Transaction tx2 = new Transaction(); // 初始化事务tx2并获取其事务ID
InnodbRow row = getRowFromDatabase(); // 获取数据库中一行记录
// 在tx1中创建读视图并检查row的可见性
ReadView readView1 = tx1.createReadView();
boolean isVisibleToTx1 = row.isVisibleTo(readView1);
// 如果isVisibleToTx1为true,则tx1可以查看该行数据;否则,根据MVCC规则,tx1应查找undo log中的历史版本。