如何用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
相关推荐
IT19954 小时前
Fail2Ban 防护 MySQL 实战|登录失败 3 次,IP 封禁 24 小时
数据库·mysql
paopaokaka_luck6 小时前
流浪动物救助与收养系统(DeepSeek AI对话、 ECharts多维分析、救助流程衔接、领养与回访衔接、 捐赠确认与公示、志愿者协作)
java·前端·javascript·人工智能·mysql·echarts·推荐算法
lzhdim8 小时前
100条常用SQL语句
数据库·sql·mysql·mybatis
这个DBA有点耶10 小时前
2026年国产数据库“实际用户增长最快”核验指南:口径优先级、可查数据与选型动作清单
数据库·mysql·架构
程序员塔塔喵11 小时前
【MySQL篇|第三篇】脏读,幻读,不可重复读
数据库·mysql·不可重复读·幻读·脏读
这个DBA有点耶11 小时前
备份成功≠数据可恢复:静默损坏会让物理备份把坏页一起带走
数据库·mysql·代码规范
数据库小学妹11 小时前
MySQL MDL锁等待排查:DDL卡住与长事务定位
数据库·后端·mysql
lhdz_bj12 小时前
Mysql Server配置详解
mysql·配置·file·config·1024程序员节·option·选项
syagain_zsx15 小时前
ODB 与 MySQL:C++ 里的 ORM 实践(类即表、事务铁律与查询封装)
开发语言·数据库·c++·mysql·工具库·1024程序员节
时间售卖铺15 小时前
MySQL 备份
运维·数据库·mysql·云原生