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中的历史版本。
相关推荐
tatasix14 分钟前
MySQL UPDATE语句执行链路解析
数据库·mysql
南城花随雪。27 分钟前
硬盘(HDD)与固态硬盘(SSD)详细解读
数据库
儿时可乖了28 分钟前
使用 Java 操作 SQLite 数据库
java·数据库·sqlite
懒是一种态度30 分钟前
Golang 调用 mongodb 的函数
数据库·mongodb·golang
天海华兮32 分钟前
mysql 去重 补全 取出重复 变量 函数 和存储过程
数据库·mysql
gma9991 小时前
Etcd 框架
数据库·etcd
爱吃青椒不爱吃西红柿‍️1 小时前
华为ASP与CSP是什么?
服务器·前端·数据库
Yz98762 小时前
hive的存储格式
大数据·数据库·数据仓库·hive·hadoop·数据库开发
武子康2 小时前
大数据-231 离线数仓 - DWS 层、ADS 层的创建 Hive 执行脚本
java·大数据·数据仓库·hive·hadoop·mysql
黑色叉腰丶大魔王2 小时前
《MySQL 数据库备份与恢复》
mysql