当前位置: 首页 > article >正文

【MyDB】4-VersionManager 之 2-事务的隔离级别

【MyDB】4-VersionManager 之 2-事务的隔离级别

  • 事务的隔离级别
    • 读提交
    • 可重复读
    • 版本跳跃问题
  • 参考资料

事务的隔离级别

本章涉及代码: \top\xianghua\mydb\server\vm\Transaction.java

读提交

上面提到,如果一个记录的最新版本被加锁,当另一个事务想要修改或读取这条记录时,MYDB 就会返回一个较旧的版本的数据。这时就可以认为,最新的被加锁的版本,对于另一个事务来说,是不可见的。于是版本可见性的概念就诞生了。

版本的可见性与事务的隔离度是相关的。MYDB 支持的最低的事务隔离程度,是“读提交”(Read Committed),即事务在读取数据时, 只能读取已经提交事务产生的数据。保证最低的读提交的好处,第四章中已经说明(防止级联回滚与 commit 语义冲突)。

MYDB 实现读提交,为每个版本维护了两个变量,就是上面提到的 XMIN 和 XMAX:

  • XMIN:创建该版本的事务编号
  • XMAX:删除该版本的事务编号

XMIN 应当在版本创建时填写,而 XMAX 则在版本被删除,或者有新版本出现时填写。

XMAX 这个变量,也就解释了为什么 DM 层不提供删除操作,当想删除一个版本时,只需要设置其 XMAX,这样,这个版本对每一个 XMAX 之后的事务都是不可见的,也就等价于删除了。

如此,在读提交下,版本对事务的可见性逻辑如下:

(XMIN == Ti and                             // 由Ti创建且
    XMAX == NULL                            // 还未被删除
)
or                                          // 或
(XMIN is commited and                       // 由一个已提交的事务创建且
    (XMAX == NULL or                        // 尚未删除或
    (XMAX != Ti and XMAX is not commited)   // 由一个未提交的事务删除
))

若条件为 true,则版本对 Ti 可见。那么获取 Ti 适合的版本,只需要从最新版本开始,依次向前检查可见性,如果为 true,就可以直接返回。

以下方法判断某个记录对事务 t 是否可见:

private static boolean readCommitted(TransactionManager tm, Transaction t, Entry e) {
    long xid = t.xid;
    long xmin = e.getXmin();
    long xmax = e.getXmax();
    if(xmin == xid && xmax == 0) return true;

    if(tm.isCommitted(xmin)) {
        if(xmax == 0) return true;
        if(xmax != xid) {
            if(!tm.isCommitted(xmax)) {
                return true;
            }
        }
    }
    return false;
}

这里的 Transaction 结构只提供了一个 XID。

可重复读

读提交会导致的问题大家也都很清楚,八股也背了不少。那就是不可重复读和幻读。这里我们来解决不可重复读的问题。

不可重复度,会导致一个事务在执行期间对同一个数据项的读取得到不同结果。如下面的结果,加入 X 初始值为 0:

T1 begin
R1(X) // T1 读得 0
T2 begin
U2(X) // 将 X 修改为 1
T2 commit
R1(X) // T1 读的 1

可以看到,T1 两次读 X,读到的结果不一样。如果想要避免这个情况,就需要引入更严格的隔离级别,即可重复读(repeatable read)。

T1 在第二次读取的时候,读到了已经提交的 T2 修改的值,导致了这个问题。于是我们可以规定:

事务只能读取它开始时, 就已经结束的那些事务产生的数据版本

这条规定,增加于,事务需要忽略:

  1. 在本事务后开始的事务的数据;
  2. 本事务开始时还是 active 状态的事务的数据

对于第一条,只需要比较事务 ID,即可确定。而对于第二条,则需要在事务 Ti 开始时,记录下当前活跃的所有事务 SP(Ti),如果记录的某个版本,XMIN 在 SP(Ti) 中,也应当对 Ti 不可见。

于是,可重复读的判断逻辑如下:

  • XMIN==Ti && XMAX == NULL Ti创建且尚未被删除的记录
  • XMIN is committed and XMIN < Ti and XMIN is not in SP(Ti)
    • XMIN is committed 表示该版本是由一个已提交的事务创建的。
    • XMIN < Ti 表示该版本是在当前事务 Ti 开始之前创建的。
    • XMIN is not in SP(Ti) 表示该版本的创建事务不在当前事务 Ti 开始时的活跃事务集合中,因此该版本对当前事务是可见的。
  • XMAX == NULL or (XMAX != Ti and (XMAX is not committed or XMAX > Ti or XMAX is in SP(Ti)))
    • XMAX == NULL 表示该版本尚未被删除。
    • XMAX != Ti 表示删除该版本的事务不是当前事务 Ti。
    • XMAX is not committed 表示删除操作尚未提交。
    • XMAX > Ti 表示删除操作发生在当前事务 Ti 之后。
    • XMAX is in SP(Ti) 表示删除操作发生在当前事务 Ti 开始之前但未提交。
// 可重复读隔离级别下的事务可见性逻辑
(XMIN == Ti and                 // 由Ti创建且
 (XMAX == NULL                  // 尚未被删除
))
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开始前还未提交
))))

于是,需要提供一个结构,来抽象一个事务,以保存事务开始时,正在活跃的其他事务数据:

public class Transaction {
    public long xid; // 事务的ID
    public int level; // 事务的隔离级别
    public Map<Long, Boolean> snapshot; // 事务的快照,用于存储活跃事务的ID
    public Exception err; // 事务执行过程中的错误
    public boolean autoAborted; // 标志事务是否自动中止
    public long startTime; // 添加开始时间属性

    public static Transaction newTransaction(long xid, IsolationLevel isolationLevel, Map<Long, Transaction> active) {
        Transaction t = new Transaction();
        t.xid = xid;
        t.isolationLevel = isolationLevel;
        t.startTime = System.currentTimeMillis();
        // 当隔离级别等于可重复读和串行化时需要创建快照
        if(isolationLevel != IsolationLevel.READ_COMMITTED && isolationLevel != IsolationLevel.READ_UNCOMMITTED) {
            t.snapshot = new HashMap<>();
            for(Long x : active.keySet()) {
                t.snapshot.put(x, true);
            }
        }
        return t;
    }

    public boolean isInSnapshot(long xid) {
        if(xid == TransactionManagerImpl.SUPER_XID) {
            return false;
        }
        return snapshot.containsKey(xid);
    }
}

构造方法中的 active,保存着当前所有 active 的事务。于是,可重复读的隔离级别下,一个版本是否对事务可见的判断如下:

private static boolean repeatableRead(TransactionManager tm, Transaction t, Entry e) {
    long xid = t.xid;
    long xmin = e.getXmin();
    long xmax = e.getXmax();

    // 当前事务创建且未删除的数据版本是可见的
    if (xmin == xid && xmax == 0) return true;

    // 已提交事务创建的版本,且不在当前事务快照中的版本是可见的
    if (tm.isCommitted(xmin) && xmin < xid && !t.isInSnapshot(xmin)) {
        // 如果记录未被删除,或删除版本未提交或不在快照中,则该版本可见
        if (xmax == 0 || (xmax != xid && (!tm.isCommitted(xmax) || xmax > xid || t.isInSnapshot(xmax)))) {
            return true;
        }
    }
    return false;
}

版本跳跃问题

说到版本跳跃之前,顺便提一嘴,MVCC 的实现,使得 MYDB 在撤销或是回滚事务很简单:只需要将这个事务标记为 aborted 即可。根据前一章提到的可见性,每个事务都只能看到其他 committed 的事务所产生的数据,一个 aborted 事务产生的数据,就不会对其他事务产生任何影响了,也就相当于,这个事务不曾存在过。

版本跳跃问题,考虑如下的情况,假设 X 最初只有 x0 版本,T1 和 T2 都是可重复读的隔离级别:

T1 begin
T2 begin
R1(X) // T1读取x0
R2(X) // T2读取x0
U1(X) // T1将X更新到x1
T1 commit
U2(X) // T2将X更新到x2
T2 commit

这种情况实际运行起来是没问题的,但是逻辑上不太正确。T1 将 X 从 x0 更新为了 x1,这是没错的。但是 T2 则是将 X 从 x0 更新成了 x2,跳过了 x1 版本。

读提交是允许版本跳跃的,而可重复读则是不允许版本跳跃的。解决版本跳跃的思路也很简单:

如果 Ti 需要修改 X,而 X 已经被 Ti 不可见的事务 Tj 修改了,那么要求 Ti 回滚。

上一节中就总结了,Ti 不可见的 Tj,有两种情况:

  1. XID(Tj) > XID(Ti):这意味着Tj在时间上晚于Ti开始,因此Ti应该回滚,避免版本跳跃。
  2. Tj in SP(Ti):则Tj在Ti开始之时仍在活跃,但Ti在开始之前并不能看到Tj的修改,因此Ti也应该回滚。

于是版本跳跃的检查也就很简单了,取出要修改的数据 X 的最新提交版本,并检查该最新版本的创建者对当前事务是否可见:

tm.isCommitted(xmax) && (xmax > t.xid t.isInSnapshot(xmax));

  • tm.isCommitted(xmax),被不可见的事务xmax修改
  • 不可见的事务xmax
    • xmax > t.xid
    • t.isInSnapshot(xmax)
public static boolean isVersionSkip(TransactionManager tm, Transaction t, Entry e) {
    long xmax = e.getXmax();
    // 读未提交隔离级别下不考虑版本跳跃问题
    if(t.level == 0) {
        return false;
    } else {
        // 读提交及以上隔离级别需要检查版本跳跃
        return tm.isCommitted(xmax) && (xmax > t.xid || t.isInSnapshot(xmax));
  }
}

参考资料

MYDB 6. 记录的版本与事务隔离 | 信也のブログ (shinya.click)

事务的隔离级别 | EasyDB (blockcloth.cn)


http://www.kler.cn/a/526955.html

相关文章:

  • C++模板初识
  • SQL注入漏洞之绕过[前端 服务端 waf]限制 以及 防御手法 一篇文章给你搞定
  • PyTorch 快速入门
  • 回顾:Maven的环境搭建
  • shell脚本
  • EasyExcel写入和读取多个sheet
  • pytorch实现半监督学习
  • CSS入门知识
  • VUE之组件通信(一)
  • win11本地部署 DeepSeek-R1 大模型!免费开源,媲美OpenAI-o1能力,断网也能用
  • 【数据机构】_复杂度
  • 【leetcode详解】T3175(一点反思)
  • arm-linux-gnueabihf安装
  • Retrieval-Augmented Generation for Large Language Models: A Survey——(1)Overview
  • 数据库性能优化(sql优化)_SQL执行计划03_yxy
  • Chapter 3-19. Detecting Congestion in Fibre Channel Fabrics
  • VS安卓仿真器下载失败怎么办?
  • maven mysql jdk nvm node npm 环境安装
  • KNIME:开源 AI 数据科学
  • Janus-Pro 论文解读:DeepSeek 如何重塑多模态技术格局
  • 【Block总结】ODConv动态卷积,适用于CV任务|即插即用
  • 全网多平台媒体内容解析工具使用指南
  • Java锁自定义实现到aqs的理解
  • 007 JSON Web Token
  • Python爬虫:requests模块深入及案例
  • 【Postman 接口测试】接口测试基础知识