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 的三种回滚场景:
- INSERT 回滚: 标记为 deleted
- UPDATE 回滚: 恢复为原始值和原始时间戳
- 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 实现展示了现代数据库系统的核心并发控制技术:
优点
- 高并发性能: 读不阻塞写,写不阻塞读
- 快照隔离: 避免脏读、不可重复读
- 乐观策略: 冲突较少时性能优异
- 版本管理: 支持历史查询和时间旅行
挑战
- 空间开销: 需要维护多个版本
- GC 复杂性: 需要及时清理旧版本
- 写冲突: 高写竞争下性能下降
- Abort 成本: 需要完整回滚所有修改
教训
- Abort 实现至关重要: 不正确的 Abort 会导致并发测试失败
- 原子操作:
UpdateTupleAndUndoLink保证并发安全 - 时间戳管理: 临时时间戳 vs 提交时间戳的转换
- Undo Log 链: 高效的版本重建机制
参考
- CMU 15-445/645 Database Systems (Fall 2024)
- BusTub Project 4 Documentation
- “SQL优化(六) MVCC PostgreSQL实现事务和多版本并发控制的精华” https://cloud.tencent.com/developer/article/1146292

浙公网安备 33010602011771号