如何用java实现一个简单的并发版本控制MVCC

最近在复习八股文,之前一直没有很清楚整个 MVCC 的流程,今天自己手动尝试实现了一下 MVCC

版本节点设计

  1. 一个版本节点包含创建它的 trx_id,对象实体和,上一个版本的版本节点指针。
java 复制代码
public class VersionNode<T> {
    VersionNode<T> next;
    T entity;
    Long trx_id;
    public VersionNode(VersionNode<T> _next, T _entity,long _trx_id){
        next = _next;
        entity = _entity;
        trx_id = _trx_id;
    }
    public T getEntity() {
        return entity;
    }
    public long getTrxId() {
        return trx_id;
    }
}

行对象设计

  1. 行对象只封装一个版本连头,通过 ReadView 返回正确的版本。
java 复制代码
public class Row<T> {
    VersionNode<T> head;
    public Row(T entity,long trxId) {
        this.head = new VersionNode<>(null, entity, trxId);
    }
    public T getByReadView(ReadView rv) {
        VersionNode<T> temp = this.head;
        while (temp != null) {
            if (rv.isVisible(temp)) {
                return temp.entity;
            }
            temp = temp.next;
        }
        return null;
    }
}

表对象设计

  1. 我们直接简化表的设计,封装一个List。
  2. 插入和更新时,需要带上trxId
  3. 简化查询,直接通过index。
    • 查询支持 可重复读(RR), 读已提交(RC),以及读未提交(RU)。
    • RU 查询到row后,直接返回链表头部
    • RR/RC 查询到查询到ROW后,通过 ReadView来实现快照读取。
java 复制代码
public class Table<T> {
    List<Row> rows = new ArrayList<Row>();
    public int insert(T entity,long trxId){
        rows.add(new Row(entity,trxId));
        return rows.size() - 1;
    }
    public void update(int index,T entity,long trx_id){
        if(index >= rows.size()||index<0) { return; }
        else {
            Row row = rows.get(index);
            VersionNode temp = row.head;
            row.head = new VersionNode(temp, entity, trx_id);
        }
    }
    public T selectByIndex(int index,ReadView rv) {
        if(index >= rows.size()||index<0) { return null; }
        else { return (T) rows.get(index).getByReadView(rv); }
    }
    public T selectRU(int index){   //读未提交,直接返回最新版本即可
        if(index >= rows.size()||index<0) {
            return null;
        }
        return (T) rows.get(index).head.entity;
    }
}

***读视图的对象设计

  1. 读视图保存了视图创建时,的活跃节点集合,和当前预分配 trx_id, 以及创建事务的节点。
  2. 判断某个版本是否可见的策略很简单:
  • 如果当前版本的事务 id 等于 读事图的 id直接返回 true;
  • 如果当前版本的事务 id 大于,目前保存的最大自增事务id,表明这个是未来提交的,不可读。
  • 如果当前版本的事务 id 在活跃事务id集合中,表明还未提交,需要,不可读。如果不在,则表明已经提交,可读,返回true。
java 复制代码
public class ReadView {
    Set<Long> active ;
    long max_trx_id;
    long trx_id;
    public boolean isVisible(VersionNode _version){
        if (_version.getTrxId()>=max_trx_id) return false;
        else if(trx_id== _version.getTrxId()) return true;
        else if(active.contains(_version.getTrxId())) return false;
        return true;
    }
    public ReadView(Set<Long> _active, long max_trx_id,long trx_id){
        this.active = _active;
        this.max_trx_id = max_trx_id;
        this.trx_id = trx_id;
    }
}

事务对象设计

  1. 事务对象直接封装一个事务id即可
arduino 复制代码
public class Transaction {
    long trx_id;
    Transaction(long _trx_id){
        trx_id = _trx_id;
    }
}

事务管理器对象设计

  1. 一个事务管理器的功能包括
    • 开始事务,通过自增 id 创建事务对象
    • 提交事务,在活跃事务集合中移除即可。
    • 创建一个读视图 (方便创建当前活跃事务状态的快照,以及自增 id)。
java 复制代码
public class TranslationManage {
    Set<Long> active = new HashSet<Long>();
    AtomicLong max_trans_id = new AtomicLong(0);
    public Transaction start(){
        Transaction transaction= new Transaction(max_trans_id.getAndIncrement());
        active.add(transaction.trx_id);
        return transaction;
    }
    public void commit(Transaction transaction){
        active.remove(transaction.trx_id);
    }
    public ReadView newReadView(Transaction transaction){
        return new ReadView(new HashSet<>(this.active),max_trans_id.get(),transaction.trx_id);
    }
}

测试 Main 方法:

java 复制代码
package org.bao.myMVCC;

public class Main {

    public static void main(String[] args) {

        TranslationManage translationManage = new TranslationManage();

        Table<Person> personTable = new Table<>();

        // 初始数据(已提交数据)
        int index = personTable.insert(new Person("小明", 11), 0);

        /*
         * =========================================================
         * 1. READ UNCOMMITTED(读未提交)
         * =========================================================
         */

        System.out.println("========== RU 读未提交 ==========");

        Transaction ruTran = translationManage.start();

        // 未提交事务修改数据
        personTable.update(index,
                new Person("RU未提交数据", 20),
                ruTran.trx_id);

        // RU:直接读取最新版本(无视事务状态)
        Person ruPerson = personTable.selectRU(index);

        System.out.println("RU读取结果: " + ruPerson);

        /*
         * =========================================================
         * 2. READ COMMITTED(读已提交)
         * =========================================================
         */

        System.out.println("\n========== RC 读已提交 ==========");

        Transaction rcTran = translationManage.start();

        // 当前事务自己修改
        personTable.update(index,
                new Person("RC自己的版本", 30),
                rcTran.trx_id);

        // 第一次查询 -> 创建 RV1
        ReadView rv1 = translationManage.newReadView(rcTran);

        Person rcP1 = personTable.selectByIndex(index, rv1);

        System.out.println("第一次读取: " + rcP1);

        // 另一个事务
        Transaction otherTran = translationManage.start();

        personTable.update(index,
                new Person("其他事务提交版本", 40),
                otherTran.trx_id);

        // 提交事务
        translationManage.commit(otherTran);

        // 第二次查询 -> RC重新生成ReadView
        ReadView rv2 = translationManage.newReadView(rcTran);

        Person rcP2 = personTable.selectByIndex(index, rv2);

        System.out.println("第二次读取: " + rcP2);

        /*
         * =========================================================
         * 3. REPEATABLE READ(可重复读)
         * =========================================================
         */

        System.out.println("\n========== RR 可重复读 ==========");

        Transaction rrTran = translationManage.start();

        // 第一次查询创建 RV
        ReadView rrRV = translationManage.newReadView(rrTran);

        Person rrP1 = personTable.selectByIndex(index, rrRV);

        System.out.println("第一次读取: " + rrP1);

        // 新事务修改并提交
        Transaction rrOtherTran = translationManage.start();

        personTable.update(index,
                new Person("RR期间别人提交的数据", 50),
                rrOtherTran.trx_id);

        translationManage.commit(rrOtherTran);

        // RR:继续使用旧 ReadView
        Person rrP2 = personTable.selectByIndex(index, rrRV);

        System.out.println("第二次读取: " + rrP2);

        /*
         * =========================================================
         * 4. RC 与 RR 对比总结
         * =========================================================
         */

        System.out.println("\n========== 对比 ==========");

        System.out.println("RC:每次select重新创建ReadView");
        System.out.println("RR:整个事务复用同一个ReadView");

    }
}

输出结果:

cmd 复制代码
========== RU 读未提交 ==========
RU读取结果: Person{name=RU未提交数据, age=20}

========== RC 读已提交 ==========
第一次读取: Person{name=RC自己的版本, age=30}
第二次读取: Person{name=其他事务提交版本, age=40}

========== RR 可重复读 ==========
第一次读取: Person{name=其他事务提交版本, age=40}
第二次读取: Person{name=其他事务提交版本, age=40}

========== 对比 ==========
RC:每次select重新创建ReadView
RR:整个事务复用同一个ReadView
相关推荐
kisy夏33 分钟前
多千帆运营平台
大数据·爬虫·mysql
AI人工智能+电脑小能手6 小时前
【大白话说Java面试题 第87题】【Mysql篇】第17题:分布式事务的实现原理?
java·数据库·分布式·mysql·面试
键盘上的猫头鹰10 小时前
【MySQL 教程(八)】索引、事务、用户管理、导入导出与分页查询
数据库·python·mysql
Rick199312 小时前
索引的排序和分组
数据库·mysql
不爱编程的小陈12 小时前
事务的进化:从MySQL单机事务到TiDB分布式事务的探究
分布式·mysql·tidb
ServBay12 小时前
不要再盲选了,PostgreSQL、MySQL与SQLite真实性能对比
数据库·mysql·sqlite
無限進步D12 小时前
MySQL 创建和管理表
数据库·mysql
Rick199313 小时前
mysql联合索引经典实例
java·数据库·mysql
独隅14 小时前
MySQL 接入不同 AI 大模型进行数据管理的全面指南(MySQL + AI)
数据库·人工智能·mysql
我是一颗柠檬16 小时前
【MySQL全面教学】MySQL备份与恢复Day14(2026年)
数据库·后端·mysql