Task1 Timestamps
维护事务txn的read_ts与commit_ts,以及事务管理器txn_mng的水位(当前活跃事务的read_ts最小值)
为保障事务的隔离性,新事务应该可见其启动时就提交的事务修改,且每个事务的提交时间戳是互斥递增的。通过last_commit_ts_实现。
Task2 Storage Format and Sequential Scan
需要实现 execution_common 中的ReconstrcuctTuple,TxnMgrDbg和更新SeqScanExecutor实现。
在开始之前要理解好文档给出的三张示意图:
整体结构示意:
UndoLog版本链:
UndoLog数据结构:
相信大家都了解过mvcc机制,它的核心是通过将每次更新(删除)操作都记录成undolog 保存到事务中并挂到版本链上,数据库只保留最后一次修改的结果,在扫表时要根据版本链获取当前事务实际可见的,核心数据结构为UndoLog和UndoLink。
ReconstrcuctTuple需要我们实现从UndoLog数组中还原给定版本的Tuple,对于删除(也可能有数据)和非删除的情况分类讨论,最后判断tuple有没有被删除,删除了直接返回nullopt。
接下来更新SeqScanExecutor实现,文档给出了三种情况,但实际处理只有两种:
- 当前可见链表头,不需要还原。
- 需要还原,通过
txn_mng的GetUndoLink获取版本链(任务4.2后改为GetVersionLink),GetUndoLog获取日志,拼接出所有可见的日志后,调用ReconstructTuple还原。 需要注意的是undolog中的ts时间戳有两阶段: - 事务未提交时格式为 transaction temporary timestamp (TXN_START_ID + txn_id)
- 事务已经提交:commit_ts 当我们遇到一个未提交或提交时间戳小于read_ts_的日志,就应该停止遍历。
接着实现TxnMgrDbg,该函数打印出整个表每个数据的版本链,在后续任务中非常有用,实现完全参照注释格式来就行。
Task3 MVCC Executors
实现MVCC下的增删改操作。 每个事务通过AppendWriteSet维护一个更新集合,在Commit时更新UndoLog的时间戳。
从3.3开始,事情变得困难了起来,更新和删除的通用解决流程为
- 判断写写冲突
- 构造UndoLog
- 更新或插入UndoLog
写写冲突只要和tuplemeta比较,是否已经有未提交的修改,或是时间戳更大的事务已经提交(在当前事务之后,因此不应该读到当前事务修改)。
构造UndoLog时需要注意,UndoLog存储的是这次修改与前一版本的差异,要能从UndoLog还原出之前版本,因此保留的数据是老数据。
接下来是保存UndoLog,我的实现分成三种情况:1. 同一事务,2. 其他事务,3. 不存在老版本
case2和3的区别在于3可能为同一事务插入后立即更新。case 1的update版本需要实现UndoLog合并。
这里的测试可能会遇到一些困难,需要好好结合
TxnMgrDbg的输出来判断实现是否正确。
3.4 垃圾回收:直接遍历大于水位的所有日志,记录下来日志所在的事务id,没有记录到的事务就可以被清理。
Task4 Primary Key Index
难度继续加码,Task4要考虑主键索引以及多线程增删改,不过好消息是只会有一个索引。 这里我参考了博客的实现。
插入的处理按照文档三步走,在插入索引之前,首先查询一下是否已经存在该主键,否则继续正常流程,最后插入索引时也判断是否插入成功。 完成这一步后应该能够得到80分。 修改index_scan,让其也支持MVCC,将seq_scan中还原tuple的逻辑抽象成公有方法给index_scan调用就行了。 接下来考虑对主键的删除与更新,我们直接来看原文:
Once an entry is created in the index, it will always point to the same RID and will NOT be removed even if the tuple is marked deleted, so that an earlier transaction can still access the history with the index scan executor.
索引在被创建后,由于会被其他事务访问,因此不会被删除。delete_executor索引更新部分直接过滤掉主键即可。
At this point, you will also need to revisit your insert executor. Consider the case that insert executor inserts into a tuple which is removed by delete executor. Your implementation should update the deleted tuple instead of creating a new entry, because an index entry always points to the same RID once created. You will need to correctly handle the write-write conflict detection and unique constraint detection.
下一步修改insert实现,区分出tuple被删除但主键还存在的情况,这种情况走更新的逻辑:创建UndoLog,处理UndoLog,更新Tuple。否则还是走老逻辑。
接下来处理update,首先识别出是否更新了主键,如果是主键更新就不更新Tuple,保留下来tuple,只更新TupleMeta。由于会出现update i = i+1的联动更新,因此要待所有更新处理完后,统一处理主键更新。
主键更新时也有三种情况:
- 更新的主键现在不存在,则插入该主键,
- 更新主键存在,且在更新集合中,则将其覆盖,
- 更新主键存在,不在更新集合中,则发生冲突。
最后一步是支持多线程(因为实现时没看明白,所以放到了最后一步做)
首先要将UpdateUndoLink改为UpdateVersionLink,GetUndoLink改为GetVersionLink,用以支持多线程情况。VersionLink中的in_progress_标记,结合UpdateVersionLink的checker参数,能够模拟原子cas操作,封装出对VersionLink修改的互斥锁:加锁LockVersionLink和解锁UnlockVersionLink。
加锁负责将in_progress_从false设置为true,失败则自旋(测试要求并发事务至少一个成功更新,因此不自旋,直接退出也可以),加锁成功后还需要再检查一次写写冲突。
解锁负责将in_progress_从true设置为false,该操作理论上不会失败,因此失败了就直接SetTainted。
然后把增删改的GetVersionLink都换成LockVersionLink,再补上UnlockVersionLink就可以。
(这里UpdateTupleInPlace是不需要传checker的
由于是第一年MVCC的lab,确实有难度,代码实现起来也比较繁琐,不过做完了确实成就感满满,Bonus Task没有找到博客,我就不继续挑战了,附上评分截图。