CMU15445 数据库项目4 Concurrency control

完成了!我已经High到不行了!!!!!!!!!!!!

Project4 于 2024 年 5 月 5 日完成,这是整个 bustub 的最后一个 Project。刚好是五一假期的最后一天。

image

image

什么人均 CMU15445 啊,咋 Project1 401 人有提交,可最终通过 Project4 的就四十人都没有(得意)。

所有 Project 的难度,大概是 Project0 < Project1 < Project2 < Project3 < Project4。我看到很多人说 Project2 和 Project4 接近,且比 Project3 难。有可能是因为我看完了《算法导论》吧。

需要写的有 GC、MVCC、TOCC、主键更新。这些东西写了才知道为什么要分乐观控制协议和悲观控制协议。即:

  1. 如果修改发生冲突的概率非常高,那么还不如先把所有要用的数据锁了,再去更新它,事务提交的成功率会高一些。
  2. 如果冲突概率非常低,事务提交成功率也会比较高,所以就没必要锁所有要更新的数据了。

MVCC

多版本并发控制,需要保证任何一个事务在执行的过程中,都只能看见该事务启动时的数据。就好像是在启动事务的时候光速复制了一次数据库一样。

实现这一点有三种方案:Append-Only、Time-Travel、Delta。

Append-Only,将每个版本的行,以链表形式组织。Time-Travel,与 Append-Only 类似,但是它使用了另一个数据库表,一般叫做 Time-Travel 表。Delta 是将新旧行的不同之处,以链表形式存储。

bustub 中使用的是 Delta 方式。所以在遍历这种版本链的时候,还需要顺便修改读取到的行。

image

建议实现一个遍历链表的类,之后完成剩下的任务会轻松一些。

遍历的流程是,先通过 VersionLink,获取到 UndoLink,之后调用 TransactionManager 的 GetUndoLog,获得 UndoLog。到下一节点需要用 UndoLog.prev_version。

对这种Delta的MVCC,主键插入、更新都有不少问题。后面稍微提了一点。

GC

GC,就是垃圾回收。回收的是事务产生的 UndoLogs、已删除且不会用到的行。

为了确定哪些 UndoLog 可以回收,需要确保它不会有任何正在执行的事务会访问到它。所以在新建、提交和 Abort 的时候需要记录一些信息。

事务 ID 随时间推进而增加。每次启动事务的时候,需要对原子计数器 next_txn_id 递增。

据此,一种很容易想到的做法是用 std::set。新增事务的时候插入事务 ID,提交、Abort 时删除 ID。任何小于 *std::set::begin() 的事务的 UndoLogs 都可以移除。

在 bustub 给的代码注释中,可以发现还有一种用哈希表的做法,其均摊时间复杂度是 O(1),但是我没有想到是怎么做的。

至于已经删除不会用到的行,实际上不需要管。如果一个表有索引,插入数据的时候依然会更新在该行上。没有索引,在插入的时候检查该行是否已经被删除,且版本链为空,也可以直接插入值。

TOCC

Timestamp Ordering Concurrency Control。实际上在实现时也使用了 MVCC 的功能。它在 MVCC 的基础上,在 Commit 的时候,检查写入-写入、读取-写入冲突。一共是以下步骤:

  1. 获取在当前事务 T 启动之后的已提交事务的写集合 W。
  2. T 的写集合与 W 求交集,如果不为空,Abort。
  3. T 的读集合与 W 求交集,如果不为空,Abort。

关于读集合,实际上并没有真正用集合,而是保存 T 所有执行的 SQL 中的条件语句,用它们来验证是否和 W 有交集。具体做法就是遍历 W 中每一行的所有在 T 启动之后出现的版本,再分别调用条件语句即可。

至于 Join,其实不需要做这一步。一个 Join 由两个 Scan 组成,只需要保存这两个 Scan 的条件即可。

主键更新

由于主键本身是索引,是 键:Record id 对,对有主键的表做插入、更新、删除,都不要删除主键本身。所以有一个边界情况,即删除后再插入。这种时候需要存储一个新的 UndoLog,以确保事务还能确保该行是已经被删除的。

一个死锁

不得不说现在的编译器确实强大,已经可以自动检测死锁并报错了。但是如果死锁是跨线程的,它暂时还没法检测出来。以下两个图就是一个跨线程的死锁。因为在运行的时候我本该呼呼叫的风扇没有什么动静,用 top 一看没找到任何 CPU 占用 50% 以上的程序,由此推断肯定是出现死锁了。

Commit 中:

image

Abort 中:

image

所以这个死锁就是: 线程 6 已经获取了 事务ID 与 事务指针映射关系 的锁,需要获取 2 号页的写锁。而线程 7 已经获取了 2 号页的写锁,需要获取 事务ID 与 事务指针映射关系 的锁。

解决方案是先获取 UndoLog,然后再尝试去获得页的写锁。如图

image

一个意外更新

MVCC 中,如果一个值更新了某一行,那么这一行的 timestamp 必须设置为 txn->GetTransactionTempTs(),所以我在 Abort 中设置了相关的 ASSERT。执行的过程中果然出错了。问题出在执行器的执行顺序上:

  1. 获得某一行的数据
  2. 更新 timestamp
  3. 修改该行,写入到缓冲区

所以需要这么修改

  1. 获取页锁
  2. 获取某一行数据
  3. 更新 timestamp
  4. 修改该行,写入到缓冲区
  5. 释放页锁

所以这是一个 Race condition。