最近在复习八股文,之前一直没有很清楚整个 MVCC 的流程,今天自己手动尝试实现了一下 MVCC
版本节点设计
- 一个版本节点包含创建它的 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;
}
}
行对象设计
- 行对象只封装一个版本连头,通过 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;
}
}
表对象设计
- 我们直接简化表的设计,封装一个List。
- 插入和更新时,需要带上trxId
- 简化查询,直接通过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;
}
}
***读视图的对象设计
- 读视图保存了视图创建时,的活跃节点集合,和当前预分配 trx_id, 以及创建事务的节点。
- 判断某个版本是否可见的策略很简单:
- 如果当前版本的事务 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;
}
}
事务对象设计
- 事务对象直接封装一个事务id即可
arduino
public class Transaction {
long trx_id;
Transaction(long _trx_id){
trx_id = _trx_id;
}
}
事务管理器对象设计
- 一个事务管理器的功能包括
- 开始事务,通过自增 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