【work记录】系统能力大赛数据库中的MVCC学习记录

日期:2025.7.26,2025.7.27

个人总结:

写在前面:

本人的数据库基础知识几乎为0,大多就是代码层面的内容自己瞎总结的,并没有系统学习过。因为比赛的内容需要实现MVCC,而自己关于MVCC屁都不会,所以本篇只是在梳理自己的MVCC相关知识。

以下的大概内容是在梳理以下名词或者代码里的变量,以及关于代码的大概讲解。

涉及到UndoLogWriteRecordPageVersionInfoWatermarkVersionInfoUndoLink


思路梳理:

首先需要说明一个事情,就是之前用于事务回滚的WriteRecord和这个UndoLog是有一些不一样的。

两个虽说都是回滚,但是WriteRecord是用于例如客户端发送abort命令的时候,将实际的有关Insert,Delete,Update相关的操作给逆操作。这个逆操作是对于数据库实际的数据来说也是在回滚。

但是UndoLog是根据时间戳的判断,将record的内容回溯到之前的版本历史里,具体的说,大概是在扫描算子里去进行回溯,而数据库里目前实际的数据还是已经修改过的,但是我们可以利用这个去查询到之前的历史数据。

VersionInfoPageVersionInfo的关系,可以从代码大概看出来。

    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()) {
    //说明之前有他的link
    log.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
  • 没有解决写入冲突等
posted @ 2025-07-28 02:41  AdviseDY  阅读(7)  评论(0)    收藏  举报