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中的历史版本。
相关推荐
阿华的代码王国23 分钟前
MySQL ------- 索引(B树B+树)
数据库·mysql
Hello.Reader1 小时前
StarRocks实时分析数据库的基础与应用
大数据·数据库
执键行天涯1 小时前
【经验帖】JAVA中同方法,两次调用Mybatis,一次更新,一次查询,同一事务,第一次修改对第二次的可见性如何
java·数据库·mybatis
liupenglove1 小时前
golang操作mysql利器-gorm
mysql·golang
yanglamei19621 小时前
基于GIKT深度知识追踪模型的习题推荐系统源代码+数据库+使用说明,后端采用flask,前端采用vue
前端·数据库·flask
叫我:松哥1 小时前
基于Python flask的医院管理学院,医生能够增加/删除/修改/删除病人的数据信息,有可视化分析
javascript·后端·python·mysql·信息可视化·flask·bootstrap
工作中的程序员1 小时前
ES 索引或索引模板
大数据·数据库·elasticsearch
严格格1 小时前
三范式,面试重点
数据库·面试·职场和发展
微刻时光2 小时前
Redis集群知识及实战
数据库·redis·笔记·学习·程序人生·缓存
单字叶2 小时前
MySQL数据库
数据库·mysql