CMU15445 Project4记录

BusTub MVCC 实现总结

核心架构

1. 时间戳管理

BusTub 使用时间戳来标识不同版本的数据:

// 时间戳类型
timestamp_t ts_;

// 特殊时间戳
const timestamp_t TXN_START_ID = (1UL << 31);  // 临时时间戳起始值
const timestamp_t INVALID_TS = UINT64_MAX;     // 无效时间戳

// 时间戳分类
- ts < TXN_START_ID:  已提交事务的提交时间戳
- ts >= TXN_START_ID: 运行中事务的临时时间戳

关键变量

  • last_commit_ts_: 最后一个提交的事务的时间戳
  • read_ts_: 事务的读时间戳(快照时间点)
  • commit_ts_: 事务的提交时间戳
  • watermark: 当前所有活跃事务中最小的 read_ts

2. 版本链结构

每个 tuple 维护一个版本链:

Table Heap (最新版本)
    ↓
[TupleMeta + Tuple]  ts=TXN_START_ID+5, undo_link → UndoLog₃
    ↓
UndoLog₃: {old_value, ts=10, prev → UndoLog₂}
    ↓
UndoLog₂: {old_value, ts=8, prev → UndoLog₁}
    ↓
UndoLog₁: {old_value, ts=5, prev → NULL}

数据结构

struct TupleMeta {
  timestamp_t ts_;       // 版本时间戳
  bool is_deleted_;      // 是否已删除
};

struct UndoLog {
  bool is_deleted_;                    // 是否是删除操作
  std::vector<bool> modified_fields_;  // 修改的字段标记
  Tuple tuple_;                        // 原始值(部分列)
  timestamp_t ts_;                     // 原始时间戳
  UndoLink prev_version_;              // 指向更早版本的链接
};

struct UndoLink {
  txn_id_t prev_txn_;    // 创建该 undo log 的事务 ID
  size_t prev_log_idx_;  // undo log 在事务中的索引
};

事务生命周期

1. Begin (开始事务)

auto TransactionManager::Begin(IsolationLevel isolation_level) -> Transaction * {
  auto txn_id = next_txn_id_++;  // 分配事务 ID
  auto txn = std::make_unique<Transaction>(txn_id, isolation_level);
  
  txn->read_ts_ = last_commit_ts_.load();  // 设置读时间戳为当前已提交的最大时间戳
  txn->commit_ts_ = INVALID_TS;
  
  running_txns_.AddTxn(txn->read_ts_);  // 加入运行事务集合
  return txn;
}

关键点

  • read_ts 确定了事务的快照点
  • 事务只能看到 ts <= read_ts 的已提交版本

2. Read (读取数据)

读取流程分为三个阶段:

阶段 1: 获取基础 Tuple

auto [base_meta, base_tuple, undo_link] = GetTupleAndUndoLink(txn_mgr, table_heap, rid);

阶段 2: 收集 Undo Logs

auto CollectUndoLogs(RID rid, const TupleMeta &base_meta, const Tuple &base_tuple,
                     std::optional<UndoLink> undo_link, Transaction *txn,
                     TransactionManager *txn_mgr) -> std::optional<std::vector<UndoLog>> {
  std::vector<UndoLog> undo_logs;
  
  // Case 1: 基础版本是当前事务创建的
  if (base_meta.ts_ == txn->GetTransactionTempTs()) {
    return undo_logs;  // 直接返回空,无需回滚
  }
  
  // Case 2: 基础版本在快照点之前
  if (base_meta.ts_ <= txn->GetReadTs()) {
    return undo_logs;  // 直接可见
  }
  
  // Case 3: 基础版本在快照点之后,需要回滚
  bool is_find = false;
  while (undo_link.has_value() && undo_link->IsValid()) {
    auto undo_log_opt = txn_mgr->GetUndoLogOptional(*undo_link);
    if (!undo_log_opt.has_value()) {
      break;  // Undo log 不存在
    }
    
    auto undo_log = undo_log_opt.value();
    
    // 遇到自己的事务,停止
    if (undo_log.ts_ == txn->GetTransactionTempTs()) {
      break;
    }
    
    // 找到可见版本
    if (undo_log.ts_ <= txn->GetReadTs()) {
      is_find = true;
      undo_logs.push_back(undo_log);
      break;
    }
    
    // 继续往前找
    undo_logs.push_back(undo_log);
    undo_link = undo_log.prev_version_;
  }
  
  if (!is_find) {
    return std::nullopt;  // 在快照点时刻,tuple 不存在
  }
  
  return undo_logs;
}

阶段 3: 重建 Tuple

auto ReconstructTuple(const Schema *schema, const Tuple &base_tuple,
                      const TupleMeta &base_meta,
                      const std::vector<UndoLog> &undo_logs) -> std::optional<Tuple> {
  std::vector<Value> values;
  for (size_t i = 0; i < schema->GetColumnCount(); i++) {
    values.push_back(base_tuple.GetValue(schema, i));
  }
  
  bool is_deleted = base_meta.is_deleted_;
  
  // 从新到旧应用 undo logs
  for (const auto &undo_log : undo_logs) {
    if (undo_log.is_deleted_) {
      // 这个版本是删除操作,tuple 应该存在
      is_deleted = true;
      values.clear();
      continue;
    }
    
    is_deleted = false;
    
    if (values.empty()) {
      // 之前是删除状态,现在完整恢复
      for (size_t i = 0; i < schema->GetColumnCount(); i++) {
        values.push_back(undo_log.tuple_.GetValue(schema, i));
      }
    } else {
      // 部分字段更新
      auto valid_schema = GetUndoLogSchema(schema, undo_log);
      auto idx = 0;
      for (size_t i = 0; i < undo_log.modified_fields_.size(); i++) {
        if (undo_log.modified_fields_[i]) {
          values[i] = undo_log.tuple_.GetValue(&valid_schema, idx++);
        }
      }
    }
  }
  
  if (is_deleted) {
    return std::nullopt;  // Tuple 在快照点时刻已删除
  }
  
  return Tuple(values, schema);
}

3. Write (写入数据)

写操作需要检测写-写冲突 (Write-Write Conflict)

auto HasWriteWriteConflict(const TupleMeta &meta, Transaction *txn) -> bool {
  const auto temp_ts = txn->GetTransactionTempTs();
  
  // 情况1: 另一个未提交事务修改了这个 tuple
  if (meta.ts_ >= TXN_START_ID) {
    return meta.ts_ != temp_ts;
  }
  
  // 情况2: 在我们开始读取后,有事务提交了对这个 tuple 的修改
  return meta.ts_ > txn->GetReadTs();
}

冲突处理

  • 检测到冲突 → SetTainted()throw ExecutionException
  • 事务被标记为 TAINTED → 最终 Abort()

写操作详细流程

Insert 操作

1. 检查主键是否存在
   - 如果存在且未删除 → 主键冲突 → Abort
   - 如果存在且已删除 → 可以"插入到墓碑" (Update)
   
2. 插入新 tuple
   - TupleMeta{temp_ts, false}
   - 不创建 undo log (undo_link = std::nullopt)
   
3. 更新主键索引
   - 只更新主键索引,非主键索引使用 MVCC

Update 操作

1. 收集所有要更新的 tuples

2. 检查主键是否改变
   - 主键不变: 原地更新
   - 主键改变: DELETE old + INSERT new (两阶段)
   
3. 对于每个 tuple:
   a) 检测写-写冲突
   b) 判断是否 self-modification
      - Self-modification: 修改现有 undo log
      - First-modification: 创建新 undo log
   c) 使用 UpdateTupleAndUndoLink 原子更新
   
4. 两阶段索引更新(主键改变时):
   Phase 1: 删除所有旧主键索引项
   Phase 2: 插入所有新主键索引项

Delete 操作

1. 收集所有要删除的 tuples

2. 对于每个 tuple:
   a) 检测写-写冲突
   b) 创建 undo log 保存原始值
   c) 标记为 deleted: TupleMeta{temp_ts, true}
   d) 不删除主键索引 (支持历史查询)

4. Commit (提交事务)

auto TransactionManager::Commit(Transaction *txn) -> bool {
  std::unique_lock<std::mutex> commit_lck(commit_mutex_);
  
  // 1. 分配提交时间戳
  auto new_commit_ts = last_commit_ts_.load() + 1;
  
  // 2. 更新所有修改的 tuples 的时间戳
  for (auto &[table_oid, rids] : txn->write_set_) {
    auto table_info = catalog_->GetTable(table_oid);
    for (auto rid : rids) {
      auto meta = table_info->table_->GetTupleMeta(rid);
      if (meta.ts_ == txn->GetTransactionTempTs()) {
        meta.ts_ = new_commit_ts;  // 临时时间戳 → 提交时间戳
        table_info->table_->UpdateTupleMeta(meta, rid);
      }
    }
  }
  
  // 3. 更新 undo logs 的时间戳
  for (auto &log : txn->undo_logs_) {
    if (log.ts_ == txn->GetTransactionTempTs()) {
      log.ts_ = new_commit_ts;
    }
  }
  
  // 4. 更新全局状态
  txn->commit_ts_ = new_commit_ts;
  last_commit_ts_.store(new_commit_ts);
  txn->state_ = TransactionState::COMMITTED;
  
  // 5. 更新 watermark 并移除运行事务
  running_txns_.UpdateCommitTs(txn->commit_ts_);
  running_txns_.RemoveTxn(txn->read_ts_);
  
  return true;
}

5. Abort (中止事务)

保证并发正确性的关键

void TransactionManager::Abort(Transaction *txn) {
  // 遍历所有修改过的 tuples
  for (auto &[table_oid, rids] : txn->write_set_) {
    auto table = catalog_->GetTable(table_oid);
    
    for (auto rid : rids) {
      auto [meta, tuple, undo_link] = GetTupleAndUndoLink(this, table->table_.get(), rid);
      
      if (undo_link.has_value() && undo_link->IsValid()) {
        // 情况1: 有 undo log (UPDATE/DELETE 操作)
        auto log = GetUndoLog(undo_link.value());
        auto rebuild_tuple = ReconstructTuple(&table->schema_, tuple, meta, {log});
        
        if (!rebuild_tuple.has_value()) {
          // 原来是 INSERT 操作 → 标记为删除
          auto new_meta = TupleMeta{log.ts_, true};
          UpdateTupleAndUndoLink(this, rid, log.prev_version_, 
                                table->table_.get(), txn, new_meta, tuple);
        } else {
          // 原来是 UPDATE/DELETE 操作 → 恢复原值
          auto new_meta = TupleMeta{log.ts_, false};
          UpdateTupleAndUndoLink(this, rid, log.prev_version_, 
                                table->table_.get(), txn, new_meta, rebuild_tuple.value());
        }
      } else {
        // 情况2: 没有 undo log (本事务首次 INSERT)
        // 标记为删除,时间戳设为 0
        auto new_meta = TupleMeta{0, true};
        UpdateTupleAndUndoLink(this, rid, std::nullopt, 
                              table->table_.get(), txn, new_meta, tuple);
      }
    }
  }
  
  // 标记事务状态
  std::unique_lock<std::shared_mutex> lck(txn_map_mutex_);
  txn->state_ = TransactionState::ABORTED;
  running_txns_.RemoveTxn(txn->read_ts_);
}

Abort 的三种回滚场景

  1. INSERT 回滚: 标记为 deleted
  2. UPDATE 回滚: 恢复为原始值和原始时间戳
  3. DELETE 回滚: 恢复 tuple 并清除 deleted 标记

并发控制机制

1. 乐观并发控制

BusTub 采用乐观策略:

  • 读取阶段: 不加锁,通过快照读取
  • 执行阶段: 在私有空间执行(临时时间戳)
  • 提交阶段: 检测冲突,无冲突则提交,有冲突则 Abort

2. 冲突检测

Read-Write 冲突: 无冲突

  • 读取使用 read_ts,只看快照
  • 写入使用 temp_ts,不影响已有快照

Write-Write 冲突: 需要检测

if (meta.ts_ >= TXN_START_ID && meta.ts_ != temp_ts) {
  // 另一个未提交事务正在修改
  return CONFLICT;
}
if (meta.ts_ > txn->GetReadTs()) {
  // 快照后有事务提交了修改
  return CONFLICT;
}

3. 原子操作

关键函数 UpdateTupleAndUndoLink 保证原子性:

auto UpdateTupleAndUndoLink(
    TransactionManager *txn_mgr, RID rid, std::optional<UndoLink> undo_link,
    TableHeap *table_heap, Transaction *txn, const TupleMeta &meta, const Tuple &tuple,
    std::function<bool(const TupleMeta&, const Tuple&, RID, std::optional<UndoLink>)> check)
    -> bool {
  // 1. 获取表页写锁
  auto page_write_guard = table_heap->AcquireTablePageWriteLock(rid);
  auto page = page_write_guard.AsMut<TablePage>();
  
  // 2. 读取当前状态
  auto [base_meta, base_tuple] = page->GetTuple(rid);
  auto current_undo_link = txn_mgr->GetUndoLink(rid);
  
  // 3. 检查是否被其他事务修改(CAS 检查)
  if (check != nullptr && !check(base_meta, base_tuple, rid, current_undo_link)) {
    return false;  // 冲突,回滚
  }
  
  // 4. 更新 tuple 和 undo_link
  if (meta != base_meta || !IsTupleContentEqual(tuple, base_tuple)) {
    table_heap->UpdateTupleInPlaceWithLockAcquired(meta, tuple, rid, page);
  }
  txn_mgr->UpdateUndoLink(rid, undo_link);
  
  return true;
}

垃圾回收 (Garbage Collection)

Watermark 机制

auto Watermark::GetWatermark() -> timestamp_t {
  if (current_reads_.empty()) {
    return commit_ts_;  // 没有活跃事务,返回最新提交时间戳
  }
  return current_reads_.begin()->first;  // 返回最小的 read_ts
}

GC 流程

void TransactionManager::GarbageCollection() {
  const auto watermark = GetWatermark();
  
  // 1. 遍历所有表和 tuples
  for (auto table_name : table_names) {
    auto table_info = catalog_->GetTable(table_name);
    auto iter = table_info->table_->MakeEagerIterator();
    
    while (!iter.IsEnd()) {
      auto rid = iter.GetRID();
      auto [meta, tuple, undo_link] = GetTupleAndUndoLink(this, table_heap, rid);
      
      // 2. 遍历 undo log 链,找到所有对 watermark 不可见的版本
      while (undo_link.has_value() && undo_link->IsValid()) {
        auto undo_log_opt = GetUndoLogOptional(*undo_link);
        if (!undo_log_opt.has_value()) break;
        
        auto undo_log = undo_log_opt.value();
        
        if (is_version_visible(undo_log.ts_)) {
          // 找到第一个可见版本,停止
          // 该事务及之前的事务的 undo logs 需要保留
          break;
        }
        
        // 记录有不可见 undo logs 的事务
        txns_with_visible_undo_logs.insert(undo_link->prev_txn_);
        undo_link = undo_log.prev_version_;
      }
      
      ++iter;
    }
  }
  
  // 3. 删除已提交且所有 undo logs 都不再需要的事务
  std::unique_lock<std::shared_mutex> lck(txn_map_mutex_);
  for (auto it = txn_map_.begin(); it != txn_map_.end();) {
    auto txn_id = it->first;
    auto &txn = it->second;
    
    if (txn->state_ == TransactionState::COMMITTED || 
        txn->state_ == TransactionState::ABORTED) {
      if (txns_with_visible_undo_logs.find(txn_id) == txns_with_visible_undo_logs.end()) {
        // 这个事务的所有 undo logs 都不再可见,可以删除
        it = txn_map_.erase(it);
        continue;
      }
    }
    ++it;
  }
}

部分关键设计决策

1. 为什么 Delete 不删除主键索引

// Task 4.2 要求
// Delete 操作不删除主键索引,支持历史查询

原因

  • MVCC 需要支持读取历史版本
  • 删除索引会导致历史查询无法通过索引定位到 tuple
  • 保留索引,通过 is_deleted_ 和时间戳来判断可见性

2. Insert 到墓碑 (Tombstone)

if (existing_tuple.is_deleted_) {
  // 不是真正的 INSERT,而是 UPDATE
  // 复用已删除 tuple 的 RID
}

优势

  • 避免 RID 无限增长
  • 主键索引不需要删除和重新插入
  • 性能优化

3. 主键更新的两阶段处理

// Phase 1: 删除所有旧主键索引项
for (auto &[index, old_key, new_key, rid] : index_updates) {
  index->DeleteEntry(old_key, rid, txn);
}

// Phase 2: 插入所有新主键索引项
for (auto &[index, old_key, new_key, rid] : index_updates) {
  index->InsertEntry(new_key, rid, txn);
}

原因

  • 避免主键值交换时的冲突
  • 例如: A: 1→2, B: 2→1,如果不是两阶段,会出现临时冲突

4. Self-Modification vs First-Modification

bool has_self_undo = 
  undo_link.has_value() && 
  undo_link->prev_txn_ == txn->GetTransactionId() &&
  base_meta.ts_ == temp_ts;

if (has_self_undo) {
  // 修改已有 undo log
  auto existing_log = txn->GetUndoLog(undo_link->prev_log_idx_);
  auto merged_log = GenerateUpdatedUndoLog(..., existing_log);
  txn->ModifyUndoLog(undo_link->prev_log_idx_, merged_log);
} else {
  // 创建新 undo log
  auto new_log = GenerateNewUndoLog(...);
  auto new_link = txn->AppendUndoLog(new_log);
}

原因

  • 同一事务多次修改同一 tuple,只需要一个 undo log
  • Undo log 记录的是"第一次修改前的原始值"

总结

BusTub 的 MVCC 实现展示了现代数据库系统的核心并发控制技术:

优点

  1. 高并发性能: 读不阻塞写,写不阻塞读
  2. 快照隔离: 避免脏读、不可重复读
  3. 乐观策略: 冲突较少时性能优异
  4. 版本管理: 支持历史查询和时间旅行

挑战

  1. 空间开销: 需要维护多个版本
  2. GC 复杂性: 需要及时清理旧版本
  3. 写冲突: 高写竞争下性能下降
  4. Abort 成本: 需要完整回滚所有修改

教训

  1. Abort 实现至关重要: 不正确的 Abort 会导致并发测试失败
  2. 原子操作: UpdateTupleAndUndoLink 保证并发安全
  3. 时间戳管理: 临时时间戳 vs 提交时间戳的转换
  4. Undo Log 链: 高效的版本重建机制

参考

posted @ 2025-11-19 13:34  Turbines/Pigs  阅读(0)  评论(0)    收藏  举报