当前位置: 首页 > article >正文

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 高级并发编程的大门。

相关文章:

C++ 与 事务多版本并发控制(MVCC):在 C++ 存储内核中利用时间戳排序实现无锁读写冲突控制

各位开发者、架构师,以及对高性能并发系统充满热情的同仁们,大家好!今天,我们将深入探讨一个在现代数据库和存储系统中至关重要的主题:多版本并发控制(MVCC),并聚焦于如何在 C 存储内…...

C++ Move 构造函数的优化原理

C Move构造函数的优化原理 在C11中,移动语义的引入彻底改变了资源管理的方式,而Move构造函数则是实现高效资源转移的核心机制之一。传统拷贝构造函数在涉及动态内存或大型对象时可能带来高昂的性能开销,而Move构造函数通过“窃取”临时对象的…...

从零开始:人工神经网络入门实战 - 用TensorFlow实现MNIST手写数字识别

1. 引言:为什么MNIST是神经网络的"Hello World"? 当你第一次接触编程时,通常会写一个"Hello World"程序。在深度学习领域,MNIST手写数字识别就是那个经典的"Hello World"!这个由美国国…...

ICRA 2025自动叉车顶会论文拆解:ADAPT如何在真实复杂场景完成托盘搬运?

ICRA 2025 最新AGV顶会论文拆解:ADAPT自动叉车系统,如何在真实复杂户外场景完成托盘搬运?如果说仓库 AGV 研究已经逐渐成熟,那么真正更难的,其实是户外、非结构化、天气变化大、障碍物复杂的施工场地搬运。 这篇来自 A…...

2025届毕业生推荐的五大AI学术平台实测分析

Ai论文网站排名(开题报告、文献综述、降aigc率、降重综合对比) TOP1. 千笔AI TOP2. aipasspaper TOP3. 清北论文 TOP4. 豆包 TOP5. kimi TOP6. deepseek 因人工智能技术迅猛发展,AI辅助毕业论文写作成众多学子实际可选之路,…...

2026最权威的十大AI论文工具推荐

Ai论文网站排名(开题报告、文献综述、降aigc率、降重综合对比) TOP1. 千笔AI TOP2. aipasspaper TOP3. 清北论文 TOP4. 豆包 TOP5. kimi TOP6. deepseek 当今,人工智能技术于学术写作范畴的运用愈发广泛,该技术的关键价值在…...

2026届最火的AI辅助论文网站实际效果

Ai论文网站排名(开题报告、文献综述、降aigc率、降重综合对比) TOP1. 千笔AI TOP2. aipasspaper TOP3. 清北论文 TOP4. 豆包 TOP5. kimi TOP6. deepseek 当前,主要被划分成三类的AI论文写作辅佐平台分别是:文献检索跟整理&a…...

探索三维流固耦合中岩石试样孔隙度变化的奇妙世界

三维流固耦合,考虑岩石试样孔隙度变化在工程和科学研究领域,三维流固耦合问题一直是备受关注的焦点,而当考虑到岩石试样孔隙度变化时,这个问题更是增添了不少复杂性与趣味性。 三维流固耦合基础概念 简单来说,流固耦合…...

D模型生成:从二维图像重建三维结构

从二维图像重建三维结构:D模型的革命性突破 在计算机视觉和人工智能领域,从二维图像重建三维结构一直是一项极具挑战性的任务。传统的三维建模方法依赖多视角图像或深度传感器,而近年来,基于深度学习的D模型(如Diffus…...

海康云台 ISPAI 二次开发

最近做了个视频会议的项目,硬件用的海康球机DS-2DC4A212IW-DE/C,甲方要求在会议内封装一个云台可以进行拖拽 控制摄像头方向以及焦距的功能,官方给的SDK还不能直接复用,只能手搓了,下面是代码可直接复用,需…...

1111111111111111111111

111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111...

零基础学唱歌全套教程 声乐技巧入门到进阶资源

很多想自学唱歌的朋友,应该都有过这样的困扰:想入门却不知道从哪一步开始,网上找的教程要么太零散,知识点前后接不上;要么内容太晦涩,完全摸不着门道。这段时间我整理了一批适配不同学习阶段的唱歌相关教程…...

2025 直播电商行业发展白皮书解读:规模、生态与规范化趋势

直播电商作为数字经济与零售业态深度融合的典型模式,近年来保持稳健增长并逐步进入规范化发展阶段。本文基于《2025 直播电商行业发展白皮书》核心内容,从行业规模、生态结构、技术应用、治理现状与发展方向等维度,对行业整体态势进行梳理与分…...

Vibe Coding 有哪些实用技巧?这篇文章讲透工作流、提示词和避坑方法

Vibe Coding 是什么?一篇讲清它的技巧、工作流与避坑方法 这两年,AI 编程工具越来越强,很多开发者开始用自然语言驱动代码生成。围绕这种开发方式,一个很火的词出现了:Vibe Coding。 简单说,Vibe Coding 就…...

YOLO12保姆级教程:2025最新目标检测模型,5分钟开箱即用

YOLO12保姆级教程:2025最新目标检测模型,5分钟开箱即用 1. 前言:为什么选择YOLO12? 目标检测是计算机视觉领域最基础也最重要的任务之一。2025年最新发布的YOLO12模型,凭借其革命性的注意力为中心架构,在…...

一种风速测量仪的设计与制作

风速、风向的测量在气象预报、环境监测、风力发电、航空航天等领域中有着重要意义。随着传感器技术、微处理器技术和网络通信技术的发展,相比传统的人工观测,数字化、智能化的气象仪器在观测精度、速度和稳定性等方面都有较大优势,因此针对数…...

Qwen2.5-VL-7B-Instruct快速部署:纯本地无网络依赖,一键启动视觉助手

Qwen2.5-VL-7B-Instruct快速部署:纯本地无网络依赖,一键启动视觉助手 1. 工具概览与核心优势 1.1 什么是Qwen2.5-VL-7B-Instruct Qwen2.5-VL-7B-Instruct是阿里通义千问团队推出的多模态大模型,专为视觉-语言交互任务设计。这个70亿参数的…...

C++ 智能指针在 STL 容器中的应用

C智能指针在STL容器中的应用 在现代C开发中,智能指针和STL容器是两大核心工具。智能指针通过自动管理内存,显著降低了资源泄漏的风险;而STL容器则提供了高效的数据存储和操作方式。将两者结合使用,既能确保内存安全,又…...

新手必学!3个OpenClaw基础Skill快速上手,5分钟搞定实操任务

新手必学!3个OpenClaw基础Skill快速上手,5分钟搞定实操任务在前两篇内容里,我们先是吃透了OpenClaw Skill的核心概念,又完成了全平台的环境部署、Skill安装加载与必装技能配置,理论和准备工作都已经到位。很多新手安装…...

3大核心功能解密:Greasy Fork如何成为浏览器扩展的终极解决方案

3大核心功能解密:Greasy Fork如何成为浏览器扩展的终极解决方案 【免费下载链接】greasyfork An online repository of user scripts. 项目地址: https://gitcode.com/gh_mirrors/gr/greasyfork 你是否曾为浏览器功能不足而烦恼?想要屏蔽烦人的广…...

2025届学术党必备的五大降重复率方案推荐榜单

Ai论文网站排名(开题报告、文献综述、降aigc率、降重综合对比) TOP1. 千笔AI TOP2. aipasspaper TOP3. 清北论文 TOP4. 豆包 TOP5. kimi TOP6. deepseek 想要切实有效地把文章的AI生成可能性降低下来,就要从语言组织以及内容编排这两个…...

Go协程goroutine泄漏检测

Go协程泄漏检测:高效排查隐形资源黑洞 在Go语言的高并发场景中,goroutine的轻量级特性使其成为开发者首选,但若管理不当,goroutine泄漏会像隐形黑洞般吞噬系统资源。这类泄漏通常因协程阻塞或未正确关闭导致,最终引发…...

CSDN程序员副业图谱技术文章推荐

CSDN程序员副业图谱技术文章推荐CSDN作为国内知名的技术社区,收录了大量关于程序员副业和技术图谱的文章。以下是一些相关的高质量中文文献和技术资源:程序员副业方向《程序员如何开启副业:技术变现的多种途径》《技术副业实战:从…...

C++ constexpr 编译期计算的应用技巧

C constexpr 编译期计算的艺术 在现代C中,constexpr关键字彻底改变了编译期计算的游戏规则。它允许开发者将复杂的计算任务从运行时转移到编译期,从而提升程序性能并增强代码的可维护性。从简单的常量计算到复杂的元编程,constexpr的应用场景…...

第11天:函数组合、记忆化与定时器

今天复习了函数组合、记忆化、setTimeout 和 setInterval,以下是知识点梳理与问答整理。一、函数组合(Compose / Pipe)1. 什么是函数组合?我的回答:把上一个函数的返回值作为下一个函数的参数,形成流水线式…...

植物大战僵尸游戏辅助工具:解锁9大隐藏功能提升玩家效率的完整指南

植物大战僵尸游戏辅助工具:解锁9大隐藏功能提升玩家效率的完整指南 【免费下载链接】pvztoolkit 植物大战僵尸 PC 版综合修改器 项目地址: https://gitcode.com/gh_mirrors/pv/pvztoolkit 在游戏辅助工具领域,开源项目往往能提供最具创新性的解决…...

湖南长沙正规的空调工厂名声

在湖南长沙,寻找一家正规的空调工厂并非易事,但长沙荣幸商贸有限责任公司(以下简称“荣幸商贸”)凭借其卓越的服务和优质的产品,成为了众多消费者的首选。本文将通过具体数据和案例,为您详细介绍荣幸商贸的…...

JL杰理AC696N开发板PWM波形生成与控制(1):频率、占空比

引言PWM这玩意儿,做调光、调速、甚至模拟音频都离不开。JL杰理AC696N的定时器自带PWM输出功能,配置起来不算复杂,但真要调出稳定的波形,有几个坑是绕不开的。比如初始化的时候LED会闪一下、占空比设0反而输出一个高电平、想换个引…...

【Git】TortoiseGit无法push远程仓库

问题 无法使用TortoiseGit push远程仓库,但是使用Git Bash命令正常,提示如下错误。 TortoiseGitPlink Fatal Error No supported authentication methods available(server sent: publickey) 原因 这个问题的核心原因在于:TortoiseGit 默认…...

架桥记:耐达讯自动化CC-Link IE转EtherCAT的工业协议融合实战

在工业自动化行业中,生产线的智能化升级常面临一个核心难题:如何让基于不同通信协议的设备“读懂”彼此,协同工作?特别是当代表日系高速网络技术的CC-Link IE,遇上盛行于欧系设备的实时以太网EtherCAT时,协…...