日期:2025.7.26,2025.7.27
个人总结:
写在前面:
本人的数据库基础知识几乎为0,大多就是代码层面的内容自己瞎总结的,并没有系统学习过。因为比赛的内容需要实现MVCC,而自己关于MVCC屁都不会,所以本篇只是在梳理自己的MVCC相关知识。
以下的大概内容是在梳理以下名词或者代码里的变量,以及关于代码的大概讲解。
涉及到UndoLog
,WriteRecord
,PageVersionInfo
,Watermark
,VersionInfo
,UndoLink
。
思路梳理:
首先需要说明一个事情,就是之前用于事务回滚的WriteRecord
和这个UndoLog
是有一些不一样的。
两个虽说都是回滚,但是WriteRecord
是用于例如客户端发送abort命令的时候,将实际的有关Insert,Delete,Update相关的操作给逆操作。这个逆操作是对于数据库实际的数据来说也是在回滚。
但是UndoLog
是根据时间戳的判断,将record的内容回溯到之前的版本历史里,具体的说,大概是在扫描算子里去进行回溯,而数据库里目前实际的数据还是已经修改过的,但是我们可以利用这个去查询到之前的历史数据。
而VersionInfo
和PageVersionInfo
的关系,可以从代码大概看出来。
struct PageVersionInfo {std::shared_mutex mutex_;/** 存储所有槽的先前版本信息。注意:不要使用 `[x]` 来访问它,因为* 即使不存在也会创建新元素。请使用 `find` 来代替。*/std::unordered_map<slot_offset_t, VersionUndoLink> prev_version_;};/** 保护版本信息 */std::shared_mutex version_info_mutex_;/** 存储表堆中每个元组的先前版本。 */std::unordered_map<page_id_t, std::shared_ptr<PageVersionInfo>> version_info_;
这是在事务manager的类里截出来的一部分。
而这里,可以看到version_info_是一个map,通过pageid可以得到对应的PageVersionInfo。然后PageVersionInfo里面存有的是这个page里面每一个record对应的历史版本的link。
这个link(UndoLink
),我们简单的理解,就是UndoLog里的一个指针(参考链表里的指向下一个点的那种设计意义)。
详情看以下代码:
/** 表示此tuple的前一个版本的链接 */struct UndoLink {/* 之前的版本可以在其中的事务中找到 */txn_id_t prev_txn_{INVALID_TXN_ID};/* 在 `prev_txn_` 中前一个版本的日志索引 */int prev_log_idx_{0};friend auto operator==(const UndoLink &a, const UndoLink &b) {return a.prev_txn_ == b.prev_txn_ && a.prev_log_idx_ == b.prev_log_idx_;}friend auto operator!=(const UndoLink &a, const UndoLink &b) { return !(a == b); }/* Checks if the undo link points to something. */bool IsValid() { return prev_txn_ != INVALID_TXN_ID; }};struct UndoLog {/* 此日志是否为删除标记 */bool is_deleted_;/* 此撤销日志修改的字段 */std::vector<bool> modified_fields_;/* 修改后的字段 */std::vector<Value> tuple_;RmRecord* tuple_test_;/* 此撤销日志的时间戳 */timestamp_t ts_{INVALID_TS};/* 撤销日志的前一个版本 */UndoLink prev_version_{};};
可以看到,UndoLog里面存着一个UndoLink。
但是这个UndoLink里面我们可以发现存的是一个事务的id和事务里的索引。
这里我们要结合一个官方给了的一个函数看起:
/** @return 此事务中撤销日志的索引 */inline auto AppendUndoLog(UndoLog log) -> UndoLink {std::scoped_lock<std::mutex> lck(latch_);undo_logs_.emplace_back(std::move(log));return {txn_id_, static_cast<int>(undo_logs_.size() - 1)};}
这个是事务里的一个函数,而这里我们是把UndoLog加入进去之后,返回了事务的id以及vector里的一个下标。这个vector是存储undolog的一个vector里面。
所以再结合这个undolink里面,我们可以知道,我们如果拿到了一个UndoLink的话,就要先通过事务ID找到对应的事务,然后通过vector的下标成功的找到对应的undolog。这就是我们沿着版本链逐渐回溯的操作内容。
而回溯的内容,我们写成一个函数UndoLog TransactionManager::GetUndoLog(UndoLink link)
。
UndoLog TransactionManager::GetUndoLog(UndoLink link) {std::shared_lock lock(txn_map_mutex_);auto it = txn_map.find(link.prev_txn_);assert(it!=txn_map.end());auto &tx = it->second;assert(link.prev_log_idx_<tx->GetUndoLogNum());return tx->GetUndoLog(link.prev_log_idx_);
}
中间加个读锁,保证并发操作。
关于MVCC的大概使用:
关于增删改的操作里,我们其实很简单,只需要添加一个CreateUndoLog的操作就好了。其他的差不多就是扫描算子的事情了。
在当前事务创建了一个undolog,然后我们在扫描算子里,要找到对于当前事务来说可见的那个record。这个可见的record,我们是通过版本链去追溯过来。
先说这个CreateUndoLog的内容:
auto TransactionManager::MVCCCreateUndoLog(Context *context, const Rid &rid, bool is_deleted,const std::vector<bool> &modified_fields, const RmRecord &record,std::string tab_name) -> UndoLink {UndoLog log;log.is_deleted_ = is_deleted;log.ts_ = context->txn_->get_read_ts();log.tuple_test_ = new RmRecord(record);log.tuple_.clear();if (sm_manager_ == nullptr) {std::cout << "sm_manager nullptr\n";assert(0);}const TabMeta &table = sm_manager_->db_.get_table(tab_name);for (const auto &col: table.cols) {Value val = GV::get_value(record, col.offset, col.type);log.tuple_.push_back(val);}if (modified_fields.empty()) {log.modified_fields_.clear();log.modified_fields_.resize(log.tuple_.size(), true);}else {log.modified_fields_ = modified_fields;}/*** 接下来要获取到之前的link,把log的link给赋值,然后log就初始化成功了。* 在之后就是更新这个UndoLink,在version_info里和page_version_info里都要更新好*/auto link_opt = MVCCGetUndoLink(context, tab_name, rid);//如果之前没有他的link,我们可以通过他的vaild来判断出来//所以是nullopt的情况我们可以无视if (link_opt.has_value()) {//说明之前有他的linklog.prev_version_ = link_opt.value();}//以上内容,log已经初始化完毕。//把undolog加入到事务里面UndoLink link = context->txn_->AppendUndoLog(log);//更新版本链VersionUndoLink ver_link{link, true};UpdateVersionLink(tab_name, rid, ver_link);return link;
}
大概的思路就是,把UndoLog初始化后,然后把这个UndoLog加入到我们事务里的UndoLogs里面,并且更新版本链就好了。更新版本链我们额外写一个函数去实现就好了,本身难度并不高。
但是有一个地方有点要注意的是,比赛的内容中关于
/** 存储表堆中每个元组的先前版本。 */std::unordered_map<PageId, std::shared_ptr<PageVersionInfo>, PageIdHash> version_info_;
这里version_info_里原本的key是page_id_t,找到page_id就好了。但是说实在的我不知道该怎么返回一个table里面对应的page_id,毕竟rid里面只有page_no,但是本质上我们可以用table的fd和page_no结合起来,这其实应该是没差的。而这个就是原本里面自带的PageId。所以我把Key改掉了,换成了PageId。但是这里有一个遗留问题是:获取PageId里的fd是通过sm_manager的,但是我发现在sm_manager里关于map的操作并没有带锁,也就是说并发操作应该是有问题的,我感觉有点疑惑,之前的题是咋过的??? 但是懒得修改了,所以如果后期MVCC没有通过的话,就自己创建一个Key(用tab_name和page_no)吧,这样并发上也没有问题了。
另外还有关于找到可见的record,这里也是用一个函数去实现的:
先叠一个甲,目前写的代码里,只保证了基础操作的通过,还并没有通过测试。(因为有些内容还没有写完)
auto TransactionManager::MVCCGetOKRecord(Context *context, const std::string &tab_name, const Rid &rid,RmRecord record) -> std::unique_ptr<RmRecord> {auto version_link_opt = GetVersionLink(tab_name, rid);if (!version_link_opt.has_value()) {return std::make_unique<RmRecord>(record);}VersionUndoLink version_link = version_link_opt.value();if (!version_link.in_progress_) {return std::make_unique<RmRecord>(record);}UndoLink link = version_link.prev_;while (link.IsValid()) {UndoLog log = GetUndoLog(link);//代表这个事务是自己搞的,那么就是可见if (link.prev_txn_ == context->txn_->get_transaction_id()) {if (log.is_deleted_) {return nullptr;}return std::make_unique<RmRecord>(record);}//如果这个事务的提交时间戳比自己读操作要早,并且合法txn_id_t tx_id = link.prev_txn_;std::shared_lock lock(context->tx_manager_->txn_map_mutex_);Transaction *tx = context->tx_manager_->txn_map.find(tx_id)->second;lock.unlock();if (tx->get_commit_ts() != INVALID_TS && tx->get_commit_ts() <= context->txn_->get_read_ts()&& tx->get_read_ts() <= tx->get_commit_ts()) {if (log.is_deleted_) {return nullptr;}return std::make_unique<RmRecord>(record);}record = *log.tuple_test_;link = log.prev_version_;}// return std::make_unique<RmRecord>(record);// 先暂且返回一个nullptr吧return nullptr;
}
这里我关于UndoLog里面的record,我的定义是之前的旧数据,而不是修改后的新数据。也就是说,如果一个数据从100改到了200,那么record会记录的是100,其他的变量的定义没什么变化,时间戳是修改时的时间戳,is_deleted是记录的是否是删除操作。
但是这里有一点麻烦的地方,会导致现在有bug的地方是:
这里突然发现了一个问题,就是这个事务的历史记录貌似并不存在刷盘的操作,也就是说数据库关闭了之后,这些历史记录就都没了。所以之前的想法是,我们的历史记录里的数据存的是之前的旧数据,那么如果数据库一直都不关闭的话,那么每一个record起码都会有一个自己的记录,但是现在如果有这个关闭数据库的行为的话,那么就会有可能有的record的历史记录不存在了。
所以可能后期会将record更换成修改后的新数据? 暂时不确定。
这里的todo有:
没有实现update操作2.0
没有解决那些没有版本链的record
没有解决写入冲突等