如何用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
相关推荐
阿维的博客日记1 小时前
求解深分页问题,last pk适合什么情况
java·mysql·深分页
__water2 小时前
【下载配置Mysql】
mysql
Dxy12393102164 小时前
MySQL 连表查询更新:从理论到实践
数据库·mysql
阿丰资源5 小时前
基于Springboot+mysql的在线兼职平台(附源码)
spring boot·后端·mysql
怪祝浙5 小时前
从简单项目入手Java(学生系统)V6(Web版本 Spring Boot3 MySQL Vue3 MyBatis)
java·spring boot·mysql
噢,我明白了5 小时前
MySql数据库数据基础操作(增删改查)
数据库·mysql·增删改查
tongluowan0076 小时前
MySql中Binlog,Redolog,Undolog的应用场景及作用的时机
mysql·日志文件
振宇i6 小时前
MySQL数据库修改表结构语句
数据库·mysql
czlczl200209256 小时前
MySQL InnoDB 加锁全解析
数据库·mysql