4.2 版本管理器——Entry与事务隔离级别

2024-09-01 08:12

本文主要是介绍4.2 版本管理器——Entry与事务隔离级别,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

VM是基于两段锁协议实现调度序列的可串行化,并实现了MVCC以消除读写阻塞。同时也实现了两种隔离级别,所以我们还需要明确版本的概念; DM 层向上层提供了数据项(Data Item)的概念,VM 通过管理所有的数据项,向上层提供了记录(Entry)的概念。上层模块通过 VM 操作数据的最小单位,就是记录。VM 则在其内部,为每个记录,维护了多个版本(Version)。每当上层模块对某个记录进行修改时,VM 就会为这个记录创建一个新的版本

Entry

EntryDataItem基础上进一步封装的数据,其结构可以再看一下下图:

image.png

对一条记录,Entry维护了其结构。理论上MVCC可以保存多版本的记录,然而这里的实现中,VM并没有向上层提供update的操作,update由表和字段的管理器(TBM)来实现了 从上图可以看出,Entry中含有三个字段,后续会讨论他们的作用

Entry定义

对于一条记录来说,MYDB 使用 Entry 类维护了其结构。虽然理论上,MVCC 实现了多版本,但是在实现中,VM 并没有提供 Update 操作,对于字段的更新操作由后面的表和字段管理(TBM)实现。所以在 VM 的实现中,一条记录只有一个版本。 由于一条记录存储在一条 Data Item 中,所以 Entry 中保存一个 DataItem 的引用即可:

Entry格式数据

[XMIN] [XMAX] [DATA]

  1. XMIN 是创建该条记录(版本)的事务编号

  2. XMAX 则是删除该条记录(版本)的事务编号

  3. DATA 就是这条记录持有的数据

public class Entry {// 定义了XMIN的偏移量为0private 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 xmaxbyte[] 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中使用XMINXMAX来实现读已提交

  • XMIN:创建该版本的事务编号,当一个事务创建了一个新的版本后,XMIN会记录下来

  • XMAX:删除该版本的事务编号,当一个版本被删除或者有新版本出现时,XMAX会记录删除该版本的事务的编号

如何利用**XMIN****XMAX**来控制读已提交的逻辑?

要满足读已提交的逻辑,那么就要根据XMINXMAX以及当前的事务编号,三者进行对比,然后决定这个Entry是否对当前事务可见 这里的逻辑如下(满足其一即可见):

  1. 如果版本的XMIN等于当前事务的事务编号,并且XMAX为空(表示尚未被删除),则该版本对当前事务可见(即该版本由当前事务创建并且还没被删除)

  2. 如果版本的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) {// 获取事务的IDlong xid = t.xid;// 获取记录的创建版本号long xmin = e.getXmin();// 获取记录的删除版本号long xmax = e.getXmax();// 如果记录的创建版本号等于事务的ID并且记录未被删除,则返回trueif (xmin == xid && xmax == 0) return true;// 如果记录的创建版本已经提交if (tm.isCommitted(xmin)) {// 如果记录未被删除,则返回trueif (xmax == 0) return true;// 如果记录的删除版本号不等于事务的IDif (xmax != xid) {// 如果记录的删除版本未提交,则返回true// 因为没有提交,代表该数据还是上一个版本可见的if (!tm.isCommitted(xmax)) {return true;}}}// 其他情况返回falsereturn false;
}

可重复读

在数据库中,可重复读(Repeatable Read)是一种事务隔离级别,它解决了读提交隔离级别下的不可重复读问题。在可重复读隔离级别下,一个事务执行期间多次读取同一数据项,可以保证读取到的结果是一致的,不会因为其他事务的并发操作而导致数据的不一致性。 不可重复读问题指的是,在读提交隔离级别下,一个事务在执行过程中多次读取同一数据项,但由于其他事务的并发修改操作,导致每次读取到的数据值不同,出现了不一致的情况。可重复读隔离级别通过更严格的规则来解决这个问题。 在可重复读隔离级别下,事务只能读取它开始时已经提交的事务产生的数据版本。这意味着,在事务开始时已经提交的所有事务所产生的数据对当前事务是可见的,而在事务开始后产生的其他事务所产生的数据对当前事务则是不可见的。这样可以确保事务在执行期间读取到的数据是一致的,不会受到其他事务的影响。

MYDB中的实现

还是利用XMINXMAX来判断是否可见 这里的逻辑如下(满足其一即可见):

  1. 如果版本的XMIN等于当前事务的事务编号,并且XMAX为空(表示尚未被删除),则该版本对当前事务可见(即该版本由当前事务创建并且还没被删除)

  2. 如果版本的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 {// 事务的IDpublic long xid;// 事务的隔离级别public int level;// 事务的快照,用于存储活跃事务的IDpublic 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();// 设置事务IDt.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,返回falseif (xid == TransactionManagerImpl.SUPER_XID) {return false;}// 否则,检查事务ID是否在快照中return snapshot.containsKey(xid);}
}
判断函数repeatableRead

private static boolean repeatableRead(TransactionManager tm, Transaction t, Entry e) {// 获取事务的IDlong xid = t.xid;// 获取条目的创建版本号long xmin = e.getXmin();// 获取条目的删除版本号long xmax = e.getXmax();// 如果条目的创建版本号等于事务的ID并且条目未被删除,则返回trueif (xmin == xid && xmax == 0) return true;// 如果条目的创建版本已经提交,并且创建版本号小于事务的ID,并且创建版本号不在事务的快照中if (tm.isCommitted(xmin) && xmin < xid && !t.isInSnapshot(xmin)) {// 如果条目未被删除,则返回trueif (xmax == 0) return true;// 如果条目的删除版本号不等于事务的IDif (xmax != xid) {// 如果条目的删除版本未提交,或者删除版本号大于事务的ID,或者删除版本号在事务的快照中,则返回trueif (!tm.isCommitted(xmax) || xmax > xid || t.isInSnapshot(xmax)) {return true;}}}// 其他情况返回falsereturn 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);}}

这篇关于4.2 版本管理器——Entry与事务隔离级别的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1126397

相关文章

SpringBoot日志级别与日志分组详解

《SpringBoot日志级别与日志分组详解》文章介绍了日志级别(ALL至OFF)及其作用,说明SpringBoot默认日志级别为INFO,可通过application.properties调整全局或... 目录日志级别1、级别内容2、调整日志级别调整默认日志级别调整指定类的日志级别项目开发过程中,利用日志

Spring 中的切面与事务结合使用完整示例

《Spring中的切面与事务结合使用完整示例》本文给大家介绍Spring中的切面与事务结合使用完整示例,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考... 目录 一、前置知识:Spring AOP 与 事务的关系 事务本质上就是一个“切面”二、核心组件三、完

Ubuntu如何升级Python版本

《Ubuntu如何升级Python版本》Ubuntu22.04Docker中,安装Python3.11后,使用update-alternatives设置为默认版本,最后用python3-V验证... 目China编程录问题描述前提环境解决方法总结问题描述Ubuntu22.04系统自带python3.10,想升级

更改linux系统的默认Python版本方式

《更改linux系统的默认Python版本方式》通过删除原Python软链接并创建指向python3.6的新链接,可切换系统默认Python版本,需注意版本冲突、环境混乱及维护问题,建议使用pyenv... 目录更改系统的默认python版本软链接软链接的特点创建软链接的命令使用场景注意事项总结更改系统的默

Linux升级或者切换python版本实现方式

《Linux升级或者切换python版本实现方式》本文介绍在Ubuntu/Debian系统升级Python至3.11或更高版本的方法,通过查看版本列表并选择新版本进行全局修改,需注意自动与手动模式的选... 目录升级系统python版本 (适用于全局修改)对于Ubuntu/Debian系统安装后,验证Pyt

解决Failed to get nested archive for entry BOOT-INF/lib/xxx.jar问题

《解决FailedtogetnestedarchiveforentryBOOT-INF/lib/xxx.jar问题》解决BOOT-INF/lib/xxx.jar替换异常需确保路径正确:解... 目录Failed to get nested archive for entry BOOT-INF/lib/xxx

MySQL 升级到8.4版本的完整流程及操作方法

《MySQL升级到8.4版本的完整流程及操作方法》本文详细说明了MySQL升级至8.4的完整流程,涵盖升级前准备(备份、兼容性检查)、支持路径(原地、逻辑导出、复制)、关键变更(空间索引、保留关键字... 目录一、升级前准备 (3.1 Before You Begin)二、升级路径 (3.2 Upgrade

SpringBoot集成EasyExcel实现百万级别的数据导入导出实践指南

《SpringBoot集成EasyExcel实现百万级别的数据导入导出实践指南》本文将基于开源项目springboot-easyexcel-batch进行解析与扩展,手把手教大家如何在SpringBo... 目录项目结构概览核心依赖百万级导出实战场景核心代码效果百万级导入实战场景监听器和Service(核心

Nginx进行平滑升级的实战指南(不中断服务版本更新)

《Nginx进行平滑升级的实战指南(不中断服务版本更新)》Nginx的平滑升级(也称为热升级)是一种在不停止服务的情况下更新Nginx版本或添加模块的方法,这种升级方式确保了服务的高可用性,避免了因升... 目录一.下载并编译新版Nginx1.下载解压2.编译二.替换可执行文件,并平滑升级1.替换可执行文件

Spring Boot集成/输出/日志级别控制/持久化开发实践

《SpringBoot集成/输出/日志级别控制/持久化开发实践》SpringBoot默认集成Logback,支持灵活日志级别配置(INFO/DEBUG等),输出包含时间戳、级别、类名等信息,并可通过... 目录一、日志概述1.1、Spring Boot日志简介1.2、日志框架与默认配置1.3、日志的核心作用