MVCC(多版本并发控制)原理实现

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来判断是否可见:

  1. DB_TRX_ID小于low_limit_id: 说明该行是在当前事务开始之前就已经提交的,因此对该事务是可见的。

  2. DB_TRX_ID大于等于up_limit_id: 说明该行是由在ReadView之后才开启的事务修改或插入的,因此对当前事务不可见。

  3. 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中的历史版本。
相关推荐
零炻大礼包25 分钟前
【SQL server】数据库远程连接配置
数据库
zmgst34 分钟前
canal1.1.7使用canal-adapter进行mysql同步数据
java·数据库·mysql
令狐少侠201134 分钟前
explain执行计划分析 ref_
mysql
随心............36 分钟前
python操作MySQL以及SQL综合案例
数据库·mysql
€☞扫地僧☜€37 分钟前
docker 拉取MySQL8.0镜像以及安装
运维·数据库·docker·容器
CopyDragon41 分钟前
设置域名跨越访问
数据库·sqlite
xjjeffery42 分钟前
MySQL 基础
数据库·mysql
写bug的小屁孩1 小时前
前后端交互接口(三)
运维·服务器·数据库·windows·用户界面·qt6.3
恒辉信达1 小时前
hhdb数据库介绍(8-4)
服务器·数据库·mysql
齐 飞2 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb