Concurrency Control
- 通关记录
- Task1 Timestamps
- Task2 Storage Format and Sequential Scan
- Task3 MVCC Executors
- Task3.1 Insert Executor
- Task3.2 Commit
- Task3.3 Update and Delete Executor
- Task3.4 Stop-the-world Garbage Collection
- Task4 Primary Key Index
- Task4.0 Index Scan
- Task4.1 Inserts
- Task4.2 Index Scan, Deletes and Updates
- Task4.3 Primary Key Updates
- Bonus1 Abort
CMU-15445汇总
本文对应的project版本为CMU-Fall-2023的project4
由于Andy要求,本博客只提供思路,不会公开任何代码
终于要完结15445的所有project了(一些leaderboard没写,以后再说吧,至少基础的部分都完成啦)
拖了好久,期间有实习毕设等各种各样的事情阻碍了进程
通关记录
本project用时约6天
Task1 Timestamps
Task1比较简单,就是为事务维护读取时间戳与提交时间戳,并维护watermark值(系统中所有活跃事务的最小读取时间戳,后续用于垃圾回收)。
在transaction_manager.cpp
中,设置事务的读取时间戳与提交时间戳,当前事务的读取时间戳为系统中最新提交事务的提交时间戳(last_commit_ts_
);当前事务的提交时间戳是系统中最新提交事务的提交时间戳+1(last_commit_ts_ + 1
)。
watermark值用map简单维护一下即可(current_reads_
)。
Task2 Storage Format and Sequential Scan
Task2也相对简单,实现版本的重构与sequential scan逻辑的重写(需要根据事务的读取时间戳查找可见版本)。
由于在bustub中,版本链的维护采用undo log的形式(每个undo log只存放partial tuple),最新记录存放在table heap中,需要沿着版本链逐步复原可见版本,故Task2需要实现一个版本重构函数ReconstructTuple
。
该函数接收一个undo log数组和base_tuple,需要以base_tuple为初始记录,遍历该undo log数组,恢复出最终的版本,逻辑并不复杂。对undo log的每一次迭代大致分为三部分:
- 遍历undo log的
modified_fields
字段,提取出需要修改的列 - 根据提取出的需要修改的列,利用
Schema::CopySchema
函数,提取出对应的partial schema - 利用partial schema,提取出partial tuple各个字段的值,并进行修改
实现完ReconstructTuple
函数后,则可以重写seq_scan_executor.cpp
中的记录扫描逻辑。原始的扫描逻辑只会查看table heap上的所有记录,而在新增了MVCC的版本链之后,可能需要从表堆开始,沿着版本链查找可见版本。
根据当前事务的read_ts
与表堆中tuple的时间戳ts
的大小关系,分为以下两种情况:
ts <= read_ts
或ts == txn_id
:表堆中的tuple对当前事务可见,直接返回else
:表堆中的tuple对当前事务不可见,需要沿着版本链查找可见版本
undo log之间通过undo link进行连接(undo link存放下一个undo log的位置,undo log存放着下一个undo link),在版本链上查找可见版本时,获取第一个undo link需要通过GetUndoLink
函数,后续的undo link则存放于undo log中。
Task3 MVCC Executors
Task3任务量会大一些,但拆分后也不难。
Task3.1 Insert Executor
首先重写insert executor,由于insert操作不会影响版本链(因为是新纪录,版本链是空的),故只是添加上设置tuple时间戳的代码,需要设置时间戳为当前事务id。同时,需要将该tuple的rid添加到当前事务的write set中,方便commit时修改tuple时间戳。
Task3.2 Commit
当一个事务commit时,我们需要将该事务write set中的所有记录的时间戳设置为该事务的提交时间戳。
Task3.3 Update and Delete Executor
需要重写update executor和delete executor,它们的逻辑非常类似,因此需要将一些代码打包成函数放在execution_common.cpp
中,方便重用。
以更新逻辑为例,主要分为以下步骤:
- 检查是否产生Write-Write Conflict,若产生则终止事务,否则继续往下
- 判断当前事务是否第一次更新该记录(查看表堆记录的时间戳与当前事务id是否一致)
- 若为第一次更新,则需要创建新的undo log,并更新table heap中的记录以及undo link
- 若不是第一次更新,则只需要修改之前创建过的undo log,更新table heap中的记录(确保事务在每条tuple上只存在一个undo log)
Task3.4 Stop-the-world Garbage Collection
由于bustub中的undo log管理在事务中,所以垃圾回收也以事务为单位,主要步骤如下:
- 遍历
txn_map_
的所有事务- 遍历当前事务的write set,从table heap中遍历write set中的每个记录,如果该事务创建的所有undo log均无效,则标记
- 回收被标记且已提交的事务
Task4 Primary Key Index
Task4会考虑主键依赖,并开始出现多线程的测试,debug会痛苦一些
Task4.0 Index Scan
与Task2顺序扫描类似,加上版本链查找逻辑即可
Task4.1 Inserts
插入逻辑需要考虑主键依赖,在插入之前,首先检测所插入的记录是否包含主键索引,如果所包含的主键索引在数据库中已存在,那么分为以下两种情况(如果只完成Task4.1及之前的所有任务,则不需要考虑这两种情况,均终止事务即可)。
- 若主键索引指向的记录已被删除,走更新流程
- 若主键索引指向的记录未被删除,终止当前事务
若主键索引不存在,则插入记录,并更新索引。若更新索引时更新失败(因为可能有多个线程并行运行),那么也要终止当前事务。
Task4.2 Index Scan, Deletes and Updates
删除与更新逻辑均涉及版本链的更新,在多线程环境下,需要保证删除与更新逻辑的线程安全性。这里采用VersionLInk类中的in_progress字段实现无锁安全性,具体方法是:
- 检查写写冲突
- 循环获取in_progress字段,直到它的值为false或者循环超时(我设置的超时次数为100次)
- 备份version_link
- 更新in_progress字段为true(使用UpdateVersionLink函数,参数中的check函数需要检查之前保存的version_link是否被修改过)
- in_progress字段更新成功,则进行记录的删除或更新逻辑;若更新失败,则回到第一步重新进行
记录的更新与删除逻辑与Task3.3一致,不再赘述
Task4.3 Primary Key Updates
当Update算子涉及到对主键的更新时,使用以前的更新逻辑会导致主键索引所指向的记录出错(因为主键的值被更新了,会映射到新的索引上)。
因此,这里的处理方法是,将记录删除后添加,不修改索引。
Bonus1 Abort
在事务终止时,需要回退事务的修改,遍历事务的writeset,根据undo log修改表堆记录即可。这里有两种实现方法,第一种是直接修改表堆记录,不改变version link,这会导致undo log的冗余;第二种是修改完表堆记录后,修改version link,跳过第一个undo log(因为它已经没用了)。