C++ 与 事务多版本并发控制(MVCC):在 C++ 存储内核中利用时间戳排序实现无锁读写冲突控制
各位开发者、架构师以及对高性能并发系统充满热情的同仁们大家好今天我们将深入探讨一个在现代数据库和存储系统中至关重要的主题多版本并发控制MVCC并聚焦于如何在 C 存储内核中利用时间戳排序机制实现无锁读写冲突控制。这不仅仅是一个理论概念的讲解更将伴随着详细的 C 代码示例力求将抽象的并发控制原理转化为可感知的实际实现。在高性能、高并发的数据密集型应用中如何有效地管理并发事务同时保证数据的一致性和隔离性是核心挑战。传统的基于锁的并发控制机制如两阶段锁2PL虽然能够保证事务的 ACID 属性但在高并发场景下往往面临死锁、活锁、锁粒度过粗导致的性能瓶颈等问题。MVCC 正是为了解决这些痛点而生它通过维护数据的多个历史版本允许多个事务同时读取数据而无需等待写事务释放锁从而显著提升系统的并发度。我们的目标是构建一个 C 存储内核它能够允许多个读事务同时进行且不被写事务阻塞。允许多个写事务并发执行但通过时间戳排序机制解决冲突。读操作在很大程度上是“无锁”的即不获取可能阻塞写操作的锁。写操作通过原子操作和时间戳验证来避免对读操作的直接阻塞并在冲突时优雅地回滚。我们将通过一个简化的键值存储模型来演示这些概念。1. MVCC 核心理念与挑战1.1 什么是 MVCCMVCCMulti-Version Concurrency Control多版本并发控制是一种并发控制技术它允许数据库管理系统维护数据项的多个版本。当一个事务修改数据时它不会直接覆盖旧数据而是创建一个新的数据版本。这样读事务可以读取旧版本的数据而写事务则创建新版本两者互不干扰。核心思想读不阻塞写写不阻塞读。这是 MVCC 最大的优势。读事务总是能找到一个合适的、已提交的数据版本进行读取而无需等待写事务完成。写事务则创建新版本也无需等待读事务。快照隔离Snapshot Isolation。大多数 MVCC 实现提供快照隔离这意味着每个事务都看到数据库的一个一致性快照这个快照是在事务开始时确定的。版本管理。每个数据项可能存在多个版本每个版本都有其生命周期创建时间、删除时间或创建事务ID、删除事务ID。1.2 MVCC 解决了什么问题传统的基于锁的并发控制例如两阶段锁 2PL在高并发环境下会遇到读写冲突读事务可能会阻塞写事务写事务也可能会阻塞读事务。死锁多个事务相互等待对方释放锁而陷入僵局。锁粒度问题粗粒度锁降低并发度细粒度锁增加管理开销。MVCC 通过允许多个版本共存有效地缓解了这些问题。读事务可以访问它们开始时可见的数据版本而写事务则在不影响读事务的情况下创建新版本。1.3 MVCC 的挑战尽管 MVCC 优势显著但也伴随着一些挑战存储开销维护数据的多个版本需要更多的存储空间。垃圾回收Garbage Collection, GC需要机制来回收不再可见的旧版本否则版本链会无限增长。GC 机制的设计和实现非常复杂。索引维护针对多版本数据的索引维护比单版本复杂。事务回滚虽然回滚通常更容易只需丢弃未提交的新版本但对于复杂的事务模型仍需仔细设计。2. 时间戳排序Timestamp Ordering, TO在 MVCC 框架下我们需要一种机制来决定事务的执行顺序和冲突处理方式。时间戳排序Timestamp Ordering是一种常见的并发控制协议它为每个事务分配一个全局唯一的、单调递增的时间戳Timestamp。这个时间戳通常在事务开始时获取并用于确定事务的逻辑顺序。2.1 核心原理时间戳排序的基本思想是如果事务 $T_i$ 的时间戳 $TS(T_i)$ 小于事务 $T_j$ 的时间戳 $TS(T_j)$那么在并发执行时系统必须保证 $T_i$ 看起来是在 $T_j$ 之前执行的。为了实现这一点每个数据项 $X$ 需要维护两个重要的时间戳读时间戳 (RTS(X))所有成功读取 $X$ 的事务中最大的事务时间戳。写时间戳 (WTS(X))所有成功写入 $X$ 的事务中最大的事务时间戳。这些时间戳在事务提交时更新以反映已提交事务对数据项的最新访问情况。2.2 读写规则当事务 $T_i$ (时间戳为 $TS(T_i)$) 尝试访问数据项 $X$ 时需要遵循以下规则读操作规则 (Read Rule):如果 $T_i$ 想要读取 $X$冲突检测如果 $TS(T_i) WTS(X)$这意味着一个比 $T_i$ 更年轻的事务已经写入并提交了 $X$。根据时间戳排序原则$T_i$ 应该在 $WTS(X)$ 之前执行所以它不应该看到 $WTS(X)$ 写入的值。这意味着 $T_i$ 尝试读取的是一个过时的数据或者 $T_i$ 违反了时间戳顺序应该回滚。成功读取如果 $TS(T_i) ge WTS(X)$则 $T_i$ 可以读取 $X$。读取成功后需要更新 $RTS(X) max(RTS(X), TS(T_i))$。写操作规则 (Write Rule):如果 $T_i$ 想要写入 $X$冲突检测 (读冲突)如果 $TS(T_i) RTS(X)$这意味着一个比 $T_i$ 更年轻的事务已经读取了 $X$。如果 $T_i$ 写入 $X$那么那个年轻事务的读操作将变得不合法它应该看到 $T_i$ 写入的值但它读的是旧值。因此$T_i$ 必须回滚。冲突检测 (写冲突)如果 $TS(T_i) WTS(X)$这意味着一个比 $T_i$ 更年轻的事务已经写入并提交了 $X$。$T_i$ 的写入是过时的不应该被接受。根据严格的时间戳排序 $T_i$ 应该回滚。Thomas Write Rule (TWR)在某些宽松的实现中如果 $TS(T_i) WTS(X)$但 $T_i$ 的写入并没有被任何已提交的读事务所依赖那么可以忽略 $T_i$ 的写入即不执行写入也不回滚。这提高了并发性但可能导致更复杂的正确性推理。在我们的实现中为了保证更强的隔离性例如快照隔离或可串行化我们将选择回滚。成功写入如果 $TS(T_i) ge RTS(X)$ 并且 $TS(T_i) ge WTS(X)$则 $T_i$ 可以写入 $X$。写入成功后需要更新 $WTS(X) TS(T_i)$。总结表格操作条件结果读 $X$$TS(T_i) WTS(X)$$T_i$ 回滚读 $X$$TS(T_i) ge WTS(X)$成功读取更新 $RTS(X) max(RTS(X), TS(T_i))$写 $X$$TS(T_i) RTS(X)$$T_i$ 回滚写 $X$$TS(T_i) WTS(X)$$T_i$ 回滚写 $X$$TS(T_i) ge RTS(X)$ 且 $TS(T_i) ge WTS(X)$成功写入更新 $WTS(X) TS(T_i)$这些规则通常在事务的提交阶段进行最终验证因为读写时间戳 (RTS/WTS) 只有在事务提交后才真正确定。在事务执行期间通常采用乐观的方式先记录读写操作然后在提交时进行验证和应用。3. C 存储内核中的 MVCC 与时间戳排序实现我们将构建一个简化的内存键值存储来演示 MVCC 和时间戳排序的集成。3.1 核心数据结构3.1.1TransactionContext事务上下文每个事务都需要一个上下文来存储其自身的信息例如事务 ID、读集合和写集合。#include atomic #include cstdint #include map #include set #include memory #include mutex #include iostream #include vector // 简化键值类型 using DataKey std::string; using DataValue std::string; // TransactionContext 定义 struct TransactionContext { uint64_t transaction_id; // 事务的开始时间戳 (作为唯一标识符) std::setDataKey read_set; // 事务读取的键集合用于提交时验证 std::mapDataKey, DataValue write_set; // 事务写入的键值对集合用于延迟写入 TransactionContext(uint64_t id) : transaction_id(id) {} // 禁止拷贝和移动确保事务上下文的唯一性 TransactionContext(const TransactionContext) delete; TransactionContext operator(const TransactionContext) delete; TransactionContext(TransactionContext) delete; TransactionContext operator(TransactionContext) delete; };3.1.2DataVersion数据版本每个数据项可能有多个版本。每个版本需要记录其创建事务 ID、删除事务 ID如果被删除和实际数据。// DataVersion 定义 struct DataVersion { uint64_t creator_tid; // 创建此版本的事务ID (作为提交时间戳) uint64_t deleter_tid; // 删除此版本的事务ID (0表示未被删除或 MAX_UINT64) DataValue value; // 实际存储的数据 std::atomicDataVersion* next_ptr; // 指向更旧版本的指针 DataVersion(uint64_t c_tid, const DataValue val) : creator_tid(c_tid), deleter_tid(0), value(val), next_ptr(nullptr) {} // 假设0表示未删除MAX_UINT64 是一个特殊的标记表示永久删除 static constexpr uint64_t NOT_DELETED 0; };这里next_ptr使用std::atomic是为了实现版本链的无锁遍历和原子更新头部。3.1.3DataItem数据项每个逻辑数据项由DataKey标识维护其所有版本的链表以及其读写时间戳。// DataItem 定义 struct DataItem { std::atomicDataVersion* head_version; // 指向最新版本的指针 std::atomicuint64_t latest_read_ts; // 成功读取此项的最大事务ID std::atomicuint64_t latest_write_ts; // 成功写入此项的最大事务ID DataItem() : head_version(nullptr), latest_read_ts(0), latest_write_ts(0) {} // 清理所有版本链用于析构或GC ~DataItem() { DataVersion* current head_version.load(std::memory_order_relaxed); while (current) { DataVersion* next current-next_ptr.load(std::memory_order_relaxed); delete current; current next; } } // 禁止拷贝和移动 DataItem(const DataItem) delete; DataItem operator(const DataItem) delete; DataItem(DataItem) delete; DataItem operator(DataItem) delete; };DataItem中的head_version,latest_read_ts,latest_write_ts都使用std::atomic这是实现“无锁读写冲突控制”的关键。读者可以直接通过load操作获取状态无需加锁。写者使用compare_exchange来原子地更新这些状态。3.1.4StorageKernel存储内核这是我们存储系统的核心负责事务管理、数据访问和冲突处理。// StorageKernel 定义 class StorageKernel { public: // 全局事务ID生成器 std::atomicuint64_t next_transaction_id; // 存储所有数据项的哈希表 // 注意这里的mutex保护的是data_store这个map自身的结构 // 而不是DataItem内部的版本链或RTS/WTS。 // 访问或修改map结构如插入新键时需要加锁但对已存在DataItem的读写操作则尽量无锁。 std::unordered_mapDataKey, std::unique_ptrDataItem data_store; std::mutex store_map_mutex; // 保护data_store的结构修改 StorageKernel() : next_transaction_id(1) {} // 事务ID从1开始 // 启动一个新事务 TransactionContext* begin_transaction(); // 事务的读操作 bool read(TransactionContext* tx, DataKey key, DataValue value); // 事务的写操作 bool write(TransactionContext* tx, DataKey key, const DataValue value); // 事务的删除操作 (在MVCC中删除也是一种特殊形式的写入) bool remove(TransactionContext* tx, DataKey key); // 提交事务 bool commit(TransactionContext* tx); // 回滚事务 void abort(TransactionContext* tx); // 垃圾回收 (简化实现实际复杂得多) void garbage_collect(uint64_t min_active_ts); };3.2 事务操作实现3.2.1begin_transaction()开始事务获取一个唯一的事务 ID作为事务的时间戳。TransactionContext* StorageKernel::begin_transaction() { uint64_t new_tid next_transaction_id.fetch_add(1, std::memory_order_relaxed); std::cout Transaction new_tid started. std::endl; return new TransactionContext(new_tid); }3.2.2read()读操作这是实现“无锁读”的核心。读事务会遍历数据项的版本链找到对其可见的最新版本。这个遍历过程不加锁因为next_ptr是std::atomic。bool StorageKernel::read(TransactionContext* tx, DataKey key, DataValue value) { // 1. 获取DataItem需要先找到对应的DataItem。这里使用map锁保护map结构。 std::unique_ptrDataItem* item_ptr nullptr; { std::lock_guardstd::mutex lock(store_map_mutex); auto it data_store.find(key); if (it data_store.end()) { // std::cout Tx tx-transaction_id : Read key key not found. std::endl; return false; // 键不存在 } item_ptr it-second; } DataItem item **item_ptr; // 2. 无锁遍历版本链寻找可见版本 DataVersion* current_version item.head_version.load(std::memory_order_acquire); DataVersion* chosen_version nullptr; while (current_version ! nullptr) { // MVCC可见性规则 // 一个版本V对事务Tx可见如果 // (1) V的创建者事务ID Tx的事务ID (即V在Tx开始前已经存在) // (2) V的删除者事务ID NOT_DELETED (即V未被删除) // 或者 V的删除者事务ID Tx的事务ID (即Tx在V被删除后才开始或者删除Tx尚未提交Tx不应该看到删除) // 这里我们简化处理认为 creator_tid 和 deleter_tid 是已提交的事务ID。 // 实际上更严谨的MVCC需要Transaction Manager来查询事务的提交状态。 // 检查创建者ID if (tx-transaction_id current_version-creator_tid) { // 检查删除者ID if (current_version-deleter_tid DataVersion::NOT_DELETED || tx-transaction_id current_version-deleter_tid) { chosen_version current_version; break; // 找到可见的最新版本 } } current_version current_version-next_ptr.load(std::memory_order_acquire); } if (chosen_version) { value chosen_version-value; tx-read_set.insert(key); // 记录到读集合用于提交时验证 // 3. 更新 latest_read_ts (原子操作) // 我们只在事务成功读取后且其 transaction_id 大于当前 latest_read_ts 时才更新。 // 这是一个CAS (Compare-And-Swap) 循环确保原子性。 uint64_t current_rts item.latest_read_ts.load(std::memory_order_acquire); while (current_rts tx-transaction_id) { if (item.latest_read_ts.compare_exchange_weak(current_rts, tx-transaction_id, std::memory_order_release, std::memory_order_acquire)) { break; // 更新成功 } // CAS 失败说明 other_rts 已经被其他线程更新重试 // current_rts 已经被 compare_exchange_weak 更新为最新的值 } // std::cout Tx tx-transaction_id : Read key key value std::endl; return true; } // std::cout Tx tx-transaction_id : Read key key no visible version. std::endl; return false; // 没有找到对当前事务可见的版本 }无锁读的体现读者在遍历版本链时不获取任何锁。head_version.load()获取一个快照指针current_version-next_ptr.load()也是如此。即使写者正在修改head_version或添加新版本读者也能基于其快照指针继续遍历不会被阻塞。latest_read_ts的更新使用了compare_exchange_weak进行原子操作同样避免了锁。3.2.3write()写操作写操作在事务执行期间通常是乐观的。它不会立即修改共享数据而是将修改记录在事务的私有写集合 (tx-write_set) 中。真正的冲突检测和数据版本创建发生在事务提交时。bool StorageKernel::write(TransactionContext* tx, DataKey key, const DataValue value) { // 写入操作通常是延迟的先将修改记录在事务的 write_set 中。 // 真正的冲突检测和版本创建发生在 commit 阶段。 // std::cout Tx tx-transaction_id : Staging write for key key value std::endl; tx-write_set[key] value; return true; } bool StorageKernel::remove(TransactionContext* tx, DataKey key) { // 删除操作在MVCC中可以看作是写入一个特殊的“删除标记”。 // 同样延迟到 commit 阶段处理。 // 这里我们使用一个特殊的值来表示删除或者在commit时查找并标记deleter_tid。 // 为了简化我们让write_set记录要删除的键值为一个特殊标记。 // 实际系统中可能需要一个单独的delete_set或一个更复杂的机制。 // 这里我们将删除也视为一种写入只是在commit时找到并标记旧版本为删除。 // std::cout Tx tx-transaction_id : Staging remove for key key std::endl; tx-write_set[key] ; // 空字符串作为删除标记需要特殊处理 return true; }3.2.4commit()提交事务提交阶段是事务并发控制的核心。在这里我们执行时间戳排序的冲突检测并原子地应用所有修改。bool StorageKernel::commit(TransactionContext* tx) { std::cout Tx tx-transaction_id : Attempting to commit... std::endl; // --- Phase 1: 提交验证 --- // 检查读集合 (Read Set) 中的数据项是否被比当前事务更年轻的事务写入。 // 如果是说明当前事务读取的数据已经过时需要回滚。 for (const auto key : tx-read_set) { // 如果此键也在写集合中说明是事务自己修改的无需检查外部写入冲突 if (tx-write_set.count(key)) { continue; } std::unique_ptrDataItem* item_ptr nullptr; { std::lock_guardstd::mutex lock(store_map_mutex); auto it data_store.find(key); if (it data_store.end()) { // 如果读取后被其他事务删除了也视为冲突 std::cout Tx tx-transaction_id : Commit failed - read key key was deleted by another transaction. std::endl; abort(tx); return false; } item_ptr it-second; } DataItem item **item_ptr; // 获取 item 的最新写入时间戳 uint64_t current_wts item.latest_write_ts.load(std::memory_order_acquire); // 如果 latest_write_ts 大于当前事务的 transaction_id // 则表示在当前事务开始后有其他更年轻的事务已经写入并提交了此键。 // 这违反了快照隔离或时间戳排序当前事务必须回滚。 if (current_wts tx-transaction_id) { std::cout Tx tx-transaction_id : Commit failed - read key key was written by a younger transaction (WTS current_wts TX_ID tx-transaction_id ). std::endl; abort(tx); return false; } } // 检查写集合 (Write Set) 中的数据项是否与已提交的事务冲突。 // 这对应时间戳排序的写规则TS(Ti) RTS(X) 或 TS(Ti) WTS(X) 时回滚。 for (const auto entry : tx-write_set) { const DataKey key entry.first; const DataValue new_value entry.second; // 如果是空字符串表示删除 std::unique_ptrDataItem* item_ptr nullptr; { std::lock_guardstd::mutex lock(store_map_mutex); auto it data_store.find(key); if (it data_store.end()) { // 如果是写入一个新键则创建DataItem。 // 否则如果尝试删除一个不存在的键这里不会创建。 if (new_value ! ) { // 写入新键 data_store[key] std::make_uniqueDataItem(); item_ptr data_store[key]; } else { // 尝试删除不存在的键无需回滚但也没有实际操作 continue; } } else { item_ptr it-second; } } DataItem item **item_ptr; // 检查写冲突 (与 RTS 冲突) // 如果 latest_read_ts 大于当前事务的 transaction_id // 则表示在当前事务开始后有其他更年轻的事务已经读取了此键。 // 如果当前事务现在写入将导致那个读事务看到不一致的数据。回滚。 uint64_t current_rts item.latest_read_ts.load(std::memory_order_acquire); if (current_rts tx-transaction_id) { std::cout Tx tx-transaction_id : Commit failed - write key key was read by a younger transaction (RTS current_rts TX_ID tx-transaction_id ). std::endl; abort(tx); return false; } // 检查写冲突 (与 WTS 冲突) // 如果 latest_write_ts 大于当前事务的 transaction_id // 则表示在当前事务开始后有其他更年轻的事务已经写入并提交了此键。 // 当前事务的写入是过时的回滚 (严格时间戳排序)。 uint64_t current_wts item.latest_write_ts.load(std::memory_order_acquire); if (current_wts tx-transaction_id) { std::cout Tx tx-transaction_id : Commit failed - write key key was written by a younger transaction (WTS current_wts TX_ID tx-transaction_id ). std::endl; abort(tx); return false; } } // --- Phase 2: 应用写入 --- // 如果所有验证都通过现在可以原子地应用所有写入。 // 这涉及创建新版本并更新 DataItem 的 head_version 和 latest_write_ts。 // 这一阶段也需要通过原子操作来保证多线程安全。 for (const auto entry : tx-write_set) { const DataKey key entry.first; const DataValue new_value entry.second; std::unique_ptrDataItem* item_ptr nullptr; { std::lock_guardstd::mutex lock(store_map_mutex); // 确保DataItem存在对于新键它已经在验证阶段创建 auto it data_store.find(key); if (it data_store.end()) { // 如果是删除一个不存在的键在验证阶段已经跳过这里不应该发生 // 但如果发生了可能意味着验证逻辑有问题或并发删除 continue; } item_ptr it-second; } DataItem item **item_ptr; if (new_value ) { // 表示删除操作 // 找到当前对本事务可见的最新版本并标记其 deleter_tid DataVersion* current_version item.head_version.load(std::memory_order_acquire); DataVersion* visible_version_to_delete nullptr; while (current_version ! nullptr) { if (tx-transaction_id current_version-creator_tid (current_version-deleter_tid DataVersion::NOT_DELETED || tx-transaction_id current_version-deleter_tid)) { visible_version_to_delete current_version; break; } current_version current_version-next_ptr.load(std::memory_order_acquire); } if (visible_version_to_delete) { // 原子地更新 deleter_tid确保只有一个事务能标记删除 uint64_t expected_deleter_tid DataVersion::NOT_DELETED; if (!visible_version_to_delete-deleter_tid.compare_exchange_strong( expected_deleter_tid, tx-transaction_id, std::memory_order_release, std::memory_order_acquire)) { // 如果 CAS 失败说明此版本已被其他事务删除或标记。 // 逻辑上这应该在验证阶段被捕获为写冲突。 // 为了简单我们在这里假设验证阶段已确保没有冲突。 // 实际系统需要更严格的错误处理。 } } else { // 没有找到可见版本可删除可能是并发删除或键不存在。 // 同样理论上应在验证阶段被捕获。 } } else { // 正常写入操作 // 创建新的版本 DataVersion* new_version new DataVersion(tx-transaction_id, new_value); // 原子地将新版本添加到版本链的头部 // 这是一个CAS循环处理并发写入 DataVersion* old_head item.head_version.load(std::memory_order_acquire); new_version-next_ptr.store(old_head, std::memory_order_relaxed); // 暂时链接到当前头部 while (!item.head_version.compare_exchange_weak(old_head, new_version, std::memory_order_release, std::memory_order_acquire)) { // CAS 失败说明 head_version 已经被其他线程更新重试 new_version-next_ptr.store(old_head, std::memory_order_relaxed); // 重新链接到新的头部 } } // 更新 item 的 latest_write_ts // 同样是CAS循环确保原子性 uint64_t current_wts item.latest_write_ts.load(std::memory_order_acquire); while (current_wts tx-transaction_id) { if (item.latest_write_ts.compare_exchange_weak(current_wts, tx-transaction_id, std::memory_order_release, std::memory_order_acquire)) { break; // 更新成功 } // CAS 失败current_wts 已被更新重试 } } // 更新读集合中未被写入的项的 latest_read_ts // 这一步对于保证时间戳排序的读规则至关重要 for (const auto key : tx-read_set) { if (tx-write_set.find(key) tx-write_set.end()) { // 仅处理只读的项 std::unique_ptrDataItem* item_ptr nullptr; { std::lock_guardstd::mutex lock(store_map_mutex); auto it data_store.find(key); if (it data_store.end()) { // 如果在读之后被删除了说明验证阶段已经失败这里不应再处理 continue; } item_ptr it-second; } DataItem item **item_ptr; uint64_t current_rts item.latest_read_ts.load(std::memory_order_acquire); while (current_rts tx-transaction_id) { if (item.latest_read_ts.compare_exchange_weak(current_rts, tx-transaction_id, std::memory_order_release, std::memory_order_acquire)) { break; // 更新成功 } } } } std::cout Tx tx-transaction_id : Committed successfully. std::endl; delete tx; // 清理事务上下文 return true; }无锁写冲突控制的体现写事务在提交时通过检查latest_read_ts和latest_write_ts来进行冲突验证。这些检查都是通过load原子操作完成的不会阻塞其他读写事务。如果验证通过实际的数据版本创建和head_version的更新也通过compare_exchange_weak原子操作完成同样避免了全局锁。3.2.5abort()回滚事务在我们的 MVCC 延迟写入模型中回滚非常简单因为所有修改都在事务的私有write_set中未曾暴露给共享存储。void StorageKernel::abort(TransactionContext* tx) { std::cout Tx tx-transaction_id : Aborted. std::endl; // 所有的写操作都只是暂存在 tx-write_set 中没有对实际数据进行修改。 // 所以回滚只需要销毁事务上下文即可。 delete tx; }3.3 垃圾回收 (Garbage Collection, GC)MVCC 系统需要一种机制来清理不再需要的旧数据版本。一个版本可以被回收的条件是没有任何活跃事务能够再访问到它。最常见的 GC 策略之一是基于最小活跃事务时间戳 (Min Active Transaction Timestamp, MATT)。系统需要追踪所有当前活跃事务中最小的transaction_id。任何creator_tid小于 MATT 的版本如果其deleter_tid也小于 MATT (或它自身已经被更新的版本所取代)则可以被安全地回收。简化的 GC 实现思路// 假设有一个机制来获取所有活跃事务的最小ID // 实际中可能需要一个全局的活跃事务列表来维护 uint64_t get_min_active_transaction_id(/* active_transactions_list */) { // 简化这里假设只有一个活跃事务或者返回一个足够小的ID // 真实场景中需要遍历所有正在运行的TransactionContext来找到最小的transaction_id return 1; // 示例实际需要动态获取 } void StorageKernel::garbage_collect(uint64_t min_active_ts) { std::cout Starting garbage collection with min_active_ts: min_active_ts std::endl; std::lock_guardstd::mutex lock(store_map_mutex); // GC 期间需要锁定 map 结构防止 DataItem 被删除或添加 for (auto const [key, item_ptr] : data_store) { DataItem item *item_ptr; // GC过程可能需要更复杂的锁机制或无锁算法来处理版本链的修改 // 特别是当其他事务同时在读写这个DataItem时。 // 这里为了简化我们假设在GC时对版本链的修改是受保护的。 // 一个更健壮的方法是使用 RCU (Read-Copy-Update) 或 epoch-based reclamation。 DataVersion* current item.head_version.load(std::memory_order_acquire); DataVersion* prev nullptr; DataVersion* new_head current; // 最终的新头部 // 找到第一个仍然可能被活跃事务访问的版本作为新的头部 while (new_head ! nullptr) { // 一个版本被认为“不活跃”且可回收如果 // 1. 它的 creator_tid min_active_ts // 2. 它的 deleter_tid min_active_ts (如果已删除) // 3. 或者它已被一个 creator_tid min_active_ts 的版本取代 // 这里的判断是如果版本V的 creator_tid 小于 min_active_ts // 并且其 deleter_tid 也小于 min_active_ts (如果被删除) // 那么这个版本及其后续版本都可以被清理。 // 实际上我们应该从尾部开始清理保留头部可见的。 // 这里我们采取另一种策略找到第一个必须保留的版本作为新的 head。 // 任何比 min_active_ts 早的版本如果已经被更新的版本覆盖且该更新版本也早于 min_active_ts // 就可以被清理。 // 简化的判断一个版本V可以被回收如果它已经被一个 creator_tid min_active_ts 的事务创建的版本所取代 // 并且V本身的 creator_tid min_active_ts 且 deleter_tid 已设置即已过期。 // 或者如果 item.head_version 是 V且 V 的 creator_tid min_active_ts并且 V 已经被删除。 // 这是一个非常简化的GC实际情况中需要更精确的可见性判断。 // 为了实现链表的中间删除我们需要跟踪前一个节点。 // 更常见的做法是遍历链表将需要保留的版本复制到新链表然后替换head。 // 或者使用一个“下一个可见版本”指针。 // 让我们尝试一种更直观的方式 // 遍历所有版本找出那些 creator_tid min_active_ts 且 deleter_tid ! NOT_DELETED // 并且 deleter_tid min_active_ts 的版本将它们从链表中移除并删除。 // 这仍然很复杂因为需要原子地修改 next_ptr。 // 最简单但效率不高的 GC 方式 // 1. 找到所有可以被删除的版本。 // 2. 将它们从链表中解除链接需要对链表头部的CAS操作或锁。 // 3. 释放内存。 // 考虑一个更安全的基于min_active_ts的保留策略 // 任何 DataVersion 如果 creator_tid min_active_ts则必须保留。 // 如果 creator_tid min_active_ts 且 deleter_tid ! NOT_DELETED 且 deleter_tid min_active_ts则可以删除。 DataVersion* current_node item.head_version.load(std::memory_order_acquire); DataVersion* last_kept_node nullptr; // 指向需要保留的链表末尾 DataVersion* to_delete_list nullptr; // 指向待删除的链表头部 // 遍历并重新构建链表 while (current_node ! nullptr) { // 判断此版本是否应该被保留 bool should_keep false; if (current_node-creator_tid min_active_ts) { should_keep true; // 正在进行的事务可能看到此版本 } else if (current_node-deleter_tid DataVersion::NOT_DELETED || current_node-deleter_tid min_active_ts) { should_keep true; // 未被删除或被一个比min_active_ts更新的事务删除可能仍对某些事务可见 } if (should_keep) { // 保持此节点 if (last_kept_node nullptr) { // 这是第一个要保留的节点它将是新的头部 // 新的头部应该还是item.head_version但是需要确保它的 next_ptr 指向正确 // 这一步非常复杂因为需要保证原子性 } last_kept_node current_node; current_node current_node-next_ptr.load(std::memory_order_acquire); } else { // 此节点可以被删除 DataVersion* next_to_delete current_node-next_ptr.load(std::memory_order_acquire); current_node-next_ptr.store(to_delete_list, std::memory_order_relaxed); // 将其添加到待删除链表头部 to_delete_list current_node; current_node next_to_delete; } } // 这个链表重构逻辑在并发环境下非常困难需要锁或RCU。 // 最简单粗暴的GC方式需要对DataItem加锁 // std::lock_guardstd::mutex item_lock(item_mutex); // 假设DataItem有一个内部锁 // current item.head_version; // prev nullptr; // while (current ! nullptr) { // // 简化如果版本已删除且创建者ID小于min_active_ts就删除它 // if (current-deleter_tid ! DataVersion::NOT_DELETED current-creator_tid min_active_ts) { // DataVersion* to_delete current; // if (prev) { // prev-next_ptr current-next_ptr; // } else { // item.head_version current-next_ptr; // 更新头部 // } // current current-next_ptr; // delete to_delete; // } else { // prev current; // current current-next_ptr; // } // } // 鉴于无锁GC的复杂性在讲座中我们仅限于概念说明。 // 实际实现通常使用一种混合方法GC线程在对特定DataItem进行操作时可能会短暂地对其加锁 // 或者使用更高级的RCU/Hazard Pointers等技术。 // 对于此讲座我们将其简化为一个概念性函数不提供完整的无锁实现。 } } std::cout Garbage collection finished. std::endl; }由于无锁垃圾回收的复杂性远超本次讲座的范围涉及 Hazard Pointers, RCU 等高级技术上述garbage_collect函数仅作为概念性框架并未提供一个完整的无锁实现。实际应用中GC 线程在进行清理时可能需要短暂地获取数据项的锁或者采用更复杂的无锁内存回收机制。4. 优势与局限4.1 优势高并发性读事务几乎完全无锁与写事务并行执行显著提升读密集型工作负载的性能。写事务之间通过时间戳排序进行冲突检测避免了传统锁机制的死锁和活锁问题。无死锁由于冲突通过事务回滚解决而不是通过等待锁解决因此天然避免了死锁。快照隔离每个事务都能看到一个一致性的数据库快照简化了应用层的并发编程模型。高吞吐量减少了锁竞争使得系统在高并发下能保持较高的吞吐量。4.2 局限性事务回滚冲突检测可能导致事务频繁回滚尤其是在写竞争激烈或事务执行时间较长的情况下这会浪费计算资源。内存开销存储多个数据版本需要更多的内存和存储空间。垃圾回收复杂性需要一个高效且正确的垃圾回收机制来回收旧版本这本身就是一项复杂的任务可能引入额外的性能开销。写倾斜Write Skew我们的时间戳排序 MVCC 实现提供了快照隔离但快照隔离可能允许写倾斜异常。如果需要更强的隔离级别如可串行化则需要更复杂的验证例如在提交时对读写集进行更全面的检查或者结合两阶段锁。事务粒度事务 ID 通常是全局递增的这在分布式系统中实现起来更复杂需要分布式时间戳服务。5. 总结与展望我们深入探讨了如何在 C 存储内核中利用 MVCC 和时间戳排序实现无锁读写冲突控制。通过原子操作 (std::atomic和compare_exchange)我们构建了一个能够实现高并发读、乐观写并进行提交时验证的系统。读操作能够无锁地遍历数据版本链而写操作则通过原子地更新版本链头部和读写时间戳来避免阻塞读操作。这种设计是现代高性能数据库系统如 PostgreSQL, Oracle, CockroachDB 等核心并发控制机制的简化版本。它展示了 C 在构建高性能、细粒度并发控制系统方面的强大能力。当然实际的生产系统会在此基础上进一步优化例如引入更复杂的垃圾回收算法、索引的多版本支持、分布式事务处理以及针对特定工作负载的混合并发控制策略。理解并掌握 MVCC 和时间戳排序的原理与实现对于构建高性能、可扩展的并发系统至关重要。希望本次讲座能为您打开一扇通向 C 高级并发编程的大门。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2483488.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!