前言
VM是基于两段锁协议实现调度序列的可串行化,并实现了MVCC以消除读写阻塞。同时也实现了两种隔离级别,所以我们还需要明确版本的概念; DM 层向上层提供了数据项(Data Item)的概念,VM 通过管理所有的数据项,向上层提供了记录(Entry)的概念。上层模块通过 VM 操作数据的最小单位,就是记录。VM 则在其内部,为每个记录,维护了多个版本(Version)。每当上层模块对某个记录进行修改时,VM 就会为这个记录创建一个新的版本
Entry
Entry
是DataItem
基础上进一步封装的数据,其结构可以再看一下下图:
对一条记录,Entry
维护了其结构。理论上MVCC可以保存多版本的记录,然而这里的实现中,VM并没有向上层提供update
的操作,update
由表和字段的管理器(TBM)来实现了 从上图可以看出,Entry
中含有三个字段,后续会讨论他们的作用
Entry定义
对于一条记录来说,MYDB 使用 Entry
类维护了其结构。虽然理论上,MVCC 实现了多版本,但是在实现中,VM 并没有提供 Update 操作,对于字段的更新操作由后面的表和字段管理(TBM)实现。所以在 VM 的实现中,一条记录只有一个版本。 由于一条记录存储在一条 Data Item 中,所以 Entry 中保存一个 DataItem 的引用即可:
Entry格式数据
[XMIN] [XMAX] [DATA]
-
XMIN 是创建该条记录(版本)的事务编号
-
XMAX 则是删除该条记录(版本)的事务编号
-
DATA 就是这条记录持有的数据
public class Entry {
// 定义了XMIN的偏移量为0
private static final int OF_XMIN = 0;
// 定义了XMAX的偏移量为XMIN偏移量后的8个字节
private static final int OF_XMAX = OF_XMIN+8;
// 定义了DATA的偏移量为XMAX偏移量后的8个字节
private static final int OF_DATA = OF_XMAX+8;
// uid字段,可能是用来唯一标识一个Entry的
private long uid;
// DataItem对象,用来存储数据的
private DataItem dataItem;
// VersionManager对象,用来管理版本的
private VersionManager vm;
创建一个新的Entry
对象
public static Entry newEntry(VersionManager vm, DataItem dataItem, long uid) {
if (dataItem == null) {
return null;
}
Entry entry = new Entry();
entry.uid = uid;
entry.dataItem = dataItem;
entry.vm = vm;
return entry;
}
加载一个Entry
LoadEntry 用来加载一个Entry。它首先从VersionManager中读取数据,然后创建一个新的Entry
/**
* 通过uid和vm加载entry
* @param vm 用来管理版本的
* @param uid 唯一标识
* @return
* @throws Exception
*/
public static Entry loadEntry(VersionManager vm, long uid) throws Exception {
DataItem di = ((VersionManagerImpl)vm).dm.read(uid);
return newEntry(vm, di, uid);
}
生成日志格式的Entry
数据
WrapEntryRaw 生成日志格式的Entry数据
/**
* 生成日志格式数据
*/
public static byte[] wrapEntryRaw(long xid, byte[] data) {
// 将事务id转为8字节数组
byte[] xmin = Parser.long2Byte(xid);
// 创建一个空的8字节数组,等待版本修改或删除是才修改
byte[] xmax = new byte[8];
// 拼接成日志格式
return Bytes.concat(xmin, xmax, data);
}
获取记录中持有的数据
Data 以拷贝的形式返回内容;获取记录中持有的数据,也就需要按照上面这个结构来解析:
// 以拷贝的形式返回内容
public byte[] data() {
// 加锁,确保数据安全
dataItem.rLock();
try {
// 获取日志数据
SubArray sa = dataItem.data();
// 创建一个去除前16字节的数组,因为前16字节表示 xmin and xmax
byte[] data = new byte[sa.end - sa.start - OF_DATA];
// 拷贝数据到data数组上
System.arraycopy(sa.raw, sa.start+OF_DATA, data, 0, data.length);
return data;
} finally {
//释放锁
dataItem.rUnLock();
}
}
修改记录中持有的数据
setXmax()
当需要对数据进行修改时,就需要设置 xmax
的值;
/**
* 设置删除版本的事务编号
* @param xid
*/
public void setXmax(long xid) {
// 在修改或删除之前先拷贝好旧数值
dataItem.before();
try {
// 获取需要删除的日志数据
SubArray sa = dataItem.data();
// 将事务编号拷贝到 8~15 处字节
System.arraycopy(Parser.long2Byte(xid), 0, sa.raw, sa.start+OF_XMAX, 8);
} finally {
// 生成一个修改日志
dataItem.after(xid);
}
}
事务隔离级别
读已提交
读已提交级别意味着只能读取到其他事务已经提交的数据,可能导致不可重复读问题
MYDB中的实现
MYDB
中使用XMIN
与XMAX
来实现读已提交
-
XMIN
:创建该版本的事务编号,当一个事务创建了一个新的版本后,XMIN
会记录下来 -
XMAX
:删除该版本的事务编号,当一个版本被删除或者有新版本出现时,XMAX
会记录删除该版本的事务的编号
如何利用**XMIN**
与**XMAX**
来控制读已提交的逻辑?
要满足读已提交的逻辑,那么就要根据XMIN
和XMAX
以及当前的事务编号,三者进行对比,然后决定这个Entry
是否对当前事务可见 这里的逻辑如下(满足其一即可见):
-
如果版本的
XMIN
等于当前事务的事务编号,并且XMAX
为空(表示尚未被删除),则该版本对当前事务可见(即该版本由当前事务创建并且还没被删除) -
如果版本的
XMIN
对应的事务已经提交,并且XMAX
为空(尚未被删除),或者XMAX
不是当前事务的事务编号,并且XMAX
对应的事务也已经提交,则该版本对当前事务可见(即由一个已提交的事务创建且尚未被删除;或者由一个已提交的事务创建且只是被未提交的事务删除)
在读提交隔离级别下,事务只能看到已经提交的版本,而不能看到尚未提交的版本或被尚未提交的事务删除的版本。这样可以确保读取的数据是稳定和一致的,同时避免了读取到不一致或未提交的数据的可能性。
(XMIN == Ti and // 由Ti创建且
XMAX == NULL // 还未被删除
)
or // 或
(XMIN is commited and // 由一个已提交的事务创建且
(XMAX == NULL or // 尚未删除或
(XMAX != Ti and XMAX is not commited) // 由一个未提交的事务删除
))
判断函数readCommited
// 用来在读提交的隔离级别下,某个记录是否对事务t可见
private static boolean readCommitted(TransactionManager tm, Transaction t, Entry e) {
// 获取事务的ID
long xid = t.xid;
// 获取记录的创建版本号
long xmin = e.getXmin();
// 获取记录的删除版本号
long xmax = e.getXmax();
// 如果记录的创建版本号等于事务的ID并且记录未被删除,则返回true
if (xmin == xid && xmax == 0) return true;
// 如果记录的创建版本已经提交
if (tm.isCommitted(xmin)) {
// 如果记录未被删除,则返回true
if (xmax == 0) return true;
// 如果记录的删除版本号不等于事务的ID
if (xmax != xid) {
// 如果记录的删除版本未提交,则返回true
// 因为没有提交,代表该数据还是上一个版本可见的
if (!tm.isCommitted(xmax)) {
return true;
}
}
}
// 其他情况返回false
return false;
}
可重复读
在数据库中,可重复读(Repeatable Read)是一种事务隔离级别,它解决了读提交隔离级别下的不可重复读问题。在可重复读隔离级别下,一个事务执行期间多次读取同一数据项,可以保证读取到的结果是一致的,不会因为其他事务的并发操作而导致数据的不一致性。 不可重复读问题指的是,在读提交隔离级别下,一个事务在执行过程中多次读取同一数据项,但由于其他事务的并发修改操作,导致每次读取到的数据值不同,出现了不一致的情况。可重复读隔离级别通过更严格的规则来解决这个问题。 在可重复读隔离级别下,事务只能读取它开始时已经提交的事务产生的数据版本。这意味着,在事务开始时已经提交的所有事务所产生的数据对当前事务是可见的,而在事务开始后产生的其他事务所产生的数据对当前事务则是不可见的。这样可以确保事务在执行期间读取到的数据是一致的,不会受到其他事务的影响。
MYDB中的实现
还是利用XMIN
与XMAX
来判断是否可见 这里的逻辑如下(满足其一即可见):
-
如果版本的
XMIN
等于当前事务的事务编号,并且XMAX
为空(表示尚未被删除),则该版本对当前事务可见(即该版本由当前事务创建并且还没被删除) -
如果版本的
XMIN
对应的事务已经提交,且XMIN
小于当前事务的事务编号,并且XMIN
不在当前的事务开始前活跃的事务集合中(SP(Ti),Ti为当前事务
)且满足下列条件之一:-
XMAX
为空(该版本尚未被删除) -
XMAX
不是当前事务的事务编号,并且XMAX
对应的事务尚未提交,并且XMAX
大于当前事务的事务编号(由其他事务删除,但是删除他的事务尚未提交或者这个事务在当前事务之后才开始) -
XMAX
在当前事务开始前活跃的事务集合中(SP(Ti)
)
-
(XMIN == Ti and // 由Ti创建且
(XMAX == NULL or // 尚未被删除
))
or // 或
(XMIN is commited and // 由一个已提交的事务创建且
XMIN < XID and // 这个事务小于Ti且
XMIN is not in SP(Ti) and // 这个事务在Ti开始前提交且
(XMAX == NULL or // 尚未被删除或
(XMAX != Ti and // 由其他事务删除但是
(XMAX is not commited or // 这个事务尚未提交或
XMAX > Ti or // 这个事务在Ti开始之后才开始或
XMAX is in SP(Ti) // 这个事务在Ti开始前还未提交
))))
事务结构
由于可重复读事务的可见性逻辑,需要提供一个结构,用来抽象事务,以保存快照数据;
// vm对一个事务的抽象
public class Transaction {
// 事务的ID
public long xid;
// 事务的隔离级别
public int level;
// 事务的快照,用于存储活跃事务的ID
public Map<Long, Boolean> snapshot;
// 事务执行过程中的错误
public Exception err;
// 标志事务是否自动中止
public boolean autoAborted;
// 创建一个新的事务
public static Transaction newTransaction(long xid, int level, Map<Long, Transaction> active) {
Transaction t = new Transaction();
// 设置事务ID
t.xid = xid;
// 设置事务隔离级别
t.level = level;
// 如果隔离级别不为0,创建快照
if (level != 0) {
t.snapshot = new HashMap<>();
// 将活跃事务的ID添加到快照中
for (Long x : active.keySet()) {
t.snapshot.put(x, true);
}
}
// 返回新创建的事务
return t;
}
// 判断一个事务ID是否在快照中
public boolean isInSnapshot(long xid) {
// 如果事务ID等于超级事务ID,返回false
if (xid == TransactionManagerImpl.SUPER_XID) {
return false;
}
// 否则,检查事务ID是否在快照中
return snapshot.containsKey(xid);
}
}
判断函数repeatableRead
private static boolean repeatableRead(TransactionManager tm, Transaction t, Entry e) {
// 获取事务的ID
long xid = t.xid;
// 获取条目的创建版本号
long xmin = e.getXmin();
// 获取条目的删除版本号
long xmax = e.getXmax();
// 如果条目的创建版本号等于事务的ID并且条目未被删除,则返回true
if (xmin == xid && xmax == 0) return true;
// 如果条目的创建版本已经提交,并且创建版本号小于事务的ID,并且创建版本号不在事务的快照中
if (tm.isCommitted(xmin) && xmin < xid && !t.isInSnapshot(xmin)) {
// 如果条目未被删除,则返回true
if (xmax == 0) return true;
// 如果条目的删除版本号不等于事务的ID
if (xmax != xid) {
// 如果条目的删除版本未提交,或者删除版本号大于事务的ID,或者删除版本号在事务的快照中,则返回true
if (!tm.isCommitted(xmax) || xmax > xid || t.isInSnapshot(xmax)) {
return true;
}
}
}
// 其他情况返回false
return false;
}
整体判断
public static boolean isVisible(TransactionManager tm, Transaction t, Entry e) {
if(t.level == 0) {
return readCommitted(tm, t, e);
} else {
return repeatableRead(tm, t, e);
}
}