资讯动态

Garnet 事务(Transactions)完全指南:从 RESP 命令到 MULTI/EXEC/WATCH 源码级原理

发布时间:2026/9/15 18:02:13 来源:尧图企业网站定制
Garnet 事务Transactions完全指南从 RESP 命令到 MULTI/EXEC/WATCH 源码级原理【免费下载链接】garnetGarnet is a remote cache-store from Microsoft Research that offers strong performance (throughput and latency), scalability, storage, recovery, cluster sharding, key migration, and replication features. Garnet can work with existing Redis clients.项目地址: https://gitcode.com/GitHub_Trending/garnet4/garnet本文是 GarnetMicrosoft Research 开源的远程缓存存储事务机制的技术指南覆盖两类事务客户端发起的事务Redis 风格 MULTI/EXEC/WATCH与服务端自定义事务。文章以website/docs/commands/transactions.md与website/docs/dev/transactions.md为骨架结合libs/server/Transaction/下源码与TxnPerfBench基准深入剖析状态机、2PL 锁、WATCH 乐观锁、版本映射WatchVersionMap、AOF 恢复与检查点一致性助你从“会调命令”进阶到“懂原理”。1. 概述Garnet 的两类事务Garnet 支持两种事务见 website/docs/dev/transactions.md客户端发起的事务Redis 风格通过MULTI/EXEC/DISCARD/WATCH/UNWATCH命令在客户端与服务器之间建立一个原子执行区间。服务端自定义事务Custom Server-side Transactions在服务器侧注册一个新的自定义事务随后任何 Garnet 客户端都可以调用它执行。其开发方法见 Extendsions 的 transactions 文档。本文以客户端事务为主线其命令参考见 website/docs/commands/transactions.md实现原理见 website/docs/dev/transactions.md。1.1 客户端事务的命令总览命令语法作用RESP 返回MULTIMULTI标记事务块开始后续命令进入排队状态Simple string:OKEXECEXEC原子执行所有排队的命令并恢复连接为正常状态Array reply每个元素是每条命令的结果或 Nil reply因 WATCH 的 key 被修改而中止DISCARDDISCARD清空所有排队命令恢复连接为正常状态Simple string:OKWATCHWATCH key [key ...]监视指定 key用于条件执行事务Simple string:OKUNWATCHUNWATCH清空之前所有被监视的 keySimple string:OK事务命令参考的权威描述位于 website/docs/commands/transactions.md其中所有命令在命令文档页面都有对应条目在 website/docs/commands 目录下。2. 客户端事务的用法与语义2.1 基本流程MULTI → 命令排队 → EXECMULTI SET key1 value1 SET key2 value2 EXECMULTI之后连接进入“事务排队”状态其后的命令不会被立即执行而是被排队EXEC触发原子执行——要么全部执行要么全部不执行执行完成后连接恢复为正常状态EXEC返回一个数组每个元素对应该事务中一条命令的返回值。2.2 乐观锁与条件事务WATCH / UNWATCHGarnet 的 WATCH 用于实现**乐观锁optimistic locking与CAScheck-and-set**语义客户端在MULTI之前用WATCH监视一个或多个 key在MULTI/EXEC区间外读取这些 key 并计算结果EXEC时若任何一个被监视的 key 在此期间被修改事务整体中止返回 Nil reply否则正常提交。官方文档给出的经典 CAS 示例见 website/docs/dev/transactions.mdWATCH mykey val GET mykey val val 1 # 非 Redis 命令在客户端侧完成 MULTI SET mykey $val EXEC在上面的示例中如果在EXEC之前mykey发生了变化事务将中止因为基于旧值计算的val已经失效。该模型的特点是不允许在 MULTI/EXEC 区间内使用读操作的结果但允许在区间外先读再监视 key若执行时 key 未变化则提交website/docs/dev/transactions.md。2.3 取消与清理DISCARD清空此前排队的全部命令并恢复连接正常状态UNWATCH清空全部被监视的 key注意在EXEC、DISCARD、UNWATCH执行之后Garnet 都会清空被监视的 key见 website/docs/dev/transactions.md 的 Unwatch 一节。3. 事务后端核心类与状态机从源码结构看libs/server/Transaction 目录客户端事务由以下类协作实现类文件职责TransactionManagerlibs/server/Transaction/TransactionManager.cs事务总控状态管理、命令排队、加锁、执行、提交WatchVersionMaplibs/server/Transaction/WatchVersionMap.cs记录被监视 key 的版本号用于 WATCH 校验WatchedKeysContainerlibs/server/Transaction/TxnWatchedKeysContainer.cs每会话保存被监视 key 及其版本快照TxnKeyEntrieslibs/server/Transaction/TxnKeyEntry.cs事务需要加锁的 key 集合key hash 锁类型TxnStatelibs/server/Transaction/TxnState.cs事务状态枚举RespCommandsInfolibs/server/Resp 相关文件命令元数据arity 等用于跳过命令、检测语法错误3.1 TxnState事务状态机libs/server/Transaction/TxnState.cs 定义四个状态None无事务在进行StartedMULTI之后进入事务管理器会排队此状态下除EXEC外的任何命令RunningEXEC之后进入事务管理器在此状态下真正运行排队的命令Aborted出现异常情况如嵌套MULTI时进入。状态迁移在 libs/server/Resp/RespServerSession.cs 的NetworkMULTI/NetworkEXEC中触发NetworkMULTI将状态置为Started并记录txnStartHead与operationCntTxnNetworkEXEC在Started时调用txnManager.Run()启动执行在Running时调用txnManager.Commit()提交。3.2 TransactionManager 的职责TransactionManager.cs 是事务引擎的核心职责如下。3.2.1 存储事务状态stateTxnState枚举跟踪当前状态txnStartHead记录MULTI之后网络缓冲区中第一条命令的位置operationCntTxn记录排队命令条数PerformWrites标记事务是否包含写操作决定是否需要写 AOFstoreTypesTransactionStoreTypesMain/Object/Unified标志位记录事务涉及哪些存储String 主存储、对象存储、统一存储。3.2.2 排队命令TrySkip 与 2PL 键锁定Started状态下TransactionManager会(1) 排队后续命令(2) 保存这些命令中用到的 key以便在执行时按2PL两阶段锁加锁。排队的实现很有特色——命令不复制到独立缓冲区而是“留在网络缓冲区里”通过RespServerSession的TrySkip函数跳过命令同时保存 key 在网络缓冲区中的真实内存位置的指针封装为TxnKeyEntry数组包含PinnedSpanByte与锁类型 Shared/Exclusive。TrySkip依赖RespCommandsInfo中的Arity参数个数来跳过正确数量的 token 并检测语法错误例如GET的 arity 为 2命令 token 加一个 key对于可接受可变参数的命令以负值记录最少参数个数例如SET的 arity 为 -3表示至少需要 3 个参数含命令 token。在TrySkip过程中调用TxnKeyManager.LockKeys——它是key-spec 驱动的读取命令的SimpleRespCommandInfokey 规格为参数中的每个 key 保存一个TxnKeyEntry。3.2.3 执行Run()当状态为Started且遇到EXEC时调用TransactionManager.Run()libs/server/Transaction/TransactionManager.cs 的Run方法其步骤为根据存储类型获取对应的TransactionalContextBeginTransaction遍历TxnKeyEntries锁定所有需要的 keyLockAllKeys/TryLockAllKeys锁前先对 key hash 排序以保证稳定哈希表与确定性加锁顺序调用WatchedKeyContainer.ValidateWatchVersion()校验被监视 key 的版本是否与 watch 时一致通过则继续执行失败则调用TransactionManager.Reset(true)重置true表示需要解锁事务中止若事务包含写操作且 AOF 开启写入TxnStart 标记到 AOF以保证中途失败时可原子恢复EnqueueTxn(AofEntryType.TxnStart, ...)。随后state置为Running网络readHead指向MULTI后的第一条命令开始真正执行这些命令。3.2.4 提交Commit()执行再次遇到EXEC且状态为Running时调用TransactionManager.Commit()TransactionManager.cs 的Commit方法解锁Run中锁定的所有 keyUnlockAllKeys重置TransactionManager与WatchedKeysContainer若事务有写操作且 AOF 开启向 AOF 追加TxnCommit 消息EnqueueTxn(AofEntryType.TxnCommit, ...)。提交时的Reset(true)还会通过stateMachineDriver.EndTransaction(txnVersion)释放事务版本并结束各存储的 TransactionalContextTransactionManager.cs。4. WATCH 乐观锁VersionMap 与 Modified Bit4.1 WatchVersionMap版本映射libs/server/Transaction/WatchVersionMap.cs 是一个每服务器实例一份的版本映射用于监控 key 的修改每次被监视的 key 被修改就将其版本号 1。实现上是一个固定大小的long[]哈希表map大小必须是 2 的幂构造时Debug.Assert(Utility.IsPowerOfTwo(size))ReadVersion(long keyHash)watch 之前调用读取某 key 的版本IncrementVersion(long keyHash)修改被监视 key 时调用使用Interlocked.Increment保证并发安全。4.2 Modified BitTsavorite 记录级修改标记Modified bit 用于在 Tsavorite 中追踪记录的修改状态记录被修改时其 modified bit 置为 1并保持为 1直到有人调用ResetModifiedAPI 将其重置为 0。为支持 WATCHGarnet 新增了ClientSession.ResetModified(ref Key key)API接收一个TKeyGarnet 传入FixedSpanByteKey将RecordInfo字 CAS 到同一字但modified bit 被重置的版本。4.3 Watch 流程客户端WATCH key时Garnet 调用ResetModifiedAPI并把 key 存入WatchedKeysContainer同时从版本映射读取该记录的版本号与 key 一起保存事务执行时遍历WatchedKeysContainer中所有 key若版本仍与 watch 时相同则继续否则中止。在 TxnWatchedKeysContainer.cs 的AddWatch中可以看到key 字节被复制到独立的 scratch buffertxnScratchBufferAllocator.CreateArgSlice随后计算hash并记录version versionMap.ReadVersion(hash)ValidateWatchVersion()则逐个比对versionMap.ReadVersion(key.hash) ! key.version任一不等即返回 false。4.4 版本递增的代价控制为避免给正常操作的关键路径带来开销版本递增只在部分情况执行website/docs/dev/transactions.md内存中的记录只为被监视的 key递增版本——Garnet 中被 watch 的 key 使用 Tsavorite 的 Modified bit 来追踪修改见上节磁盘上的记录为copy-update的 RMW 与 Upsert 递增版本。这是有意接受的代价因为 copy update 相对较少开销不关键。版本递增发生在MainSessionFunctions与ObjectSessionFunctions的以下回调中InPlaceUpdater若被监视、InPlaceWriter若被监视、InPlaceDeleter若被监视、PostInitialWriter、PostInitialUpdater、PostCopyUpdater、PostInitialDeleter。4.5 Unwatch 流程记录在 Tsavorite 中被修改时modified bit 自动置位用户调用UNWATCH时Garnet 只需重置WatchedKeysContainer每次执行完DISCARD、EXEC、UNWATCH命令后都会清空所有监视。5. 加锁与存储2PL、锁类型与统一存储5.1 TxnKeyEntry锁集合libs/server/Transaction/TxnKeyEntry.cs 中TxnKeyEntry是一个 9 字节结构体[StructLayout(LayoutKind.Explicit, Size 9)]8 字节keyHash 1 字节LockTypeShared/Exclusive/None实现ITransactionalKey。TxnKeyEntries提供AddKey(PinnedSpanByte keyArgSlice, LockType type)追加待加锁 key容量不足时倍增扩容LockAllKeys()/TryLockAllKeys(TimeSpan lock_timeout)加锁前先对 key hash 排序注释说明必须在稳定的 Tsavorite 哈希表——非 GROW 阶段——下排序排序本身也依赖此稳定性然后对统一存储上下文一次性加锁UnlockAllKeys()释放统一存储上的锁并清空集合IsReadOnly若所有锁都是 Shared 则为只读事务。5.2 事务涉及的存储类型TransactionStoreTypes是一个[Flags]枚举TransactionManager.csMain1String 主存储Object2对象存储Hash/List/Set/SortedSet 等对象类型Unified4统一存储。AddTransactionStoreType将StoreType映射为事务存储类型BeginTransaction/LocksAcquired/EndTransaction会对storeTypes中涉及的所有存储上下文执行对应操作。由此可以看出Garnet 的事务可以横跨字符串与对象类型并统一通过 unified store 的 transactional context 加锁。5.3 WATCH 键在 EXEC 时的加锁在Run()的第一步非内部事务时会调用watchContainer.SaveKeysToLock(this)将WatchedKeysContainer中所有仍处于 watched 状态的 key 以Shared 锁注册进TxnKeyEntriesTxnWatchedKeysContainer.cs与事务命令自身的 key 一起参与排序与加锁。6. 集群模式下的事务slot 验证当集群启用时clusterEnabled trueTransactionManager会为事务收集所有 key 以进行slot 验证GetSlotVerificationInput会先把排队命令的 key 复制到 scratch bufferCopyExistingKeysToScratchBuffer再调用watchContainer.SaveKeysToKeyList把 watched key 一并收集构造ClusterSlotVerificationInput只读标志 会话信息不指定 key spec——验证时会遍历该上下文中的全部 key在 RespServerSession.cs 的NetworkEXEC中可以看到执行事务前若txnManager.txnKeysParseState.Count 0会调用clusterSession.NetworkMultiKeySlotVerify(..., isTxn: true)做多 key slot 一致性检查失败则记录日志、重置事务并跳过执行。这保证了集群下事务涉及的所有 key 落在正确的 slot/节点上避免跨节点事务的非法访问。7. AOF 恢复与检查点一致性优化7.1 事务的 AOF 持久化在Run()中若PerformWrites appendOnlyFile ! null !StoredProcMode会写入AofEntryType.TxnStart在Commit()中写入AofEntryType.TxnCommitTransactionManager.cs、#L512-L517。这样 AOF 重放时可以成对识别 TxnStart/TxnCommit 边界原子恢复事务——即使事务执行中途失败也能准确回滚到事务开始前的一致性状态。对于多日志MultiLog配置ComputeSublogAccessVector会从已加锁的 key hash等于GarnetLog.HASH无需重算计算出 physical/virtual sublog 访问位图与参与重放的 replay task 数量随 TxnStart/TxnCommit 头写入供并行重放协调使用。7.2 检查点一致性Garnet 定期做检查点并在检查点之间推进版本号。为保证检查点一致性要求一个事务的所有操作处于同一版本 / 同一检查点窗口内。当前强制执行方式website/docs/dev/transactions.md当 TsavoriteStateMachine 处于Prepare 阶段时不允许新事务启动执行让检查点先完成若已有事务正在运行而 TsavoriteStateMachine 进入 Prepare则不允许版本切换直到事务执行完毕二者通过session.IsInPreparePhase以及Run函数开头处的两个 while 循环实现。在源码中对应Run()里txnVersion stateMachineDriver.AcquireTransactionVersion()获取事务版本执行前VerifyTransactionVersion验证版本有效提交时stateMachineDriver.EndTransaction(txnVersion)归还版本。8. 自定义服务端事务Custom Server-side Transactions除了客户端事务Garnet 还支持在服务器侧注册自定义事务CustomTransactionProcedure。其完整开发指南位于 website/docs/extensions/transactions.md。这里给出与本文主题相关的实现要点自定义事务通过TransactionManager.RunTransactionProc运行TransactionManager.cs流程为Prepare → Run → Main → Log → Commit → Finalizeproc.Prepare使用GarnetWatchApi即带 WATCH 语义的只读 API收集读集并校验Run加锁 WATCH 版本校验后进入Main在锁定的数据上执行主体逻辑TransactionalGarnetApi若有写操作则Log到 AOFAofEntryType.StoredProcedureCommit解锁并写提交标记最后执行FinalizeAOF 重放期间跳过因为提交会由 AOF 重放接管自定义事务同样受PerformWrites、AOF 与事务版本管理约束支持FailFastOnKeyLockFailure与KeyLockTimeout对应TryLockAllKeys(lock_timeout)的可失败快速路径见 TransactionManager.cs。9. 性能验证TxnPerfBench 微基准为了验证客户端事务性能仓库提供了TxnPerfBench位于 benchmark/Resp.benchmark/TxnPerfBench.cs包含四种负载READ_TXN一个事务内执行readPerTxn个GETWRITE_TXN一个事务内执行writePerTxn个SETREAD_WRITE_TXNSET与GET混合readPerTxnwritePerTxnWATCH_TXN先 watchreadPerTxn个 key再开事务读取这些被监视的 key 并写入writePerTxn个 key。示例运行命令见 website/docs/dev/transactions.md# 纯 WATCH 事务负载 dotnet run -c Release -f net10.0 -- -t 2 -b 1 --dbsize 1024 -x --client SERedis --op-workload WATCH_TXN --op-percent 100 # 混合 READ_TXN 与 WRITE_TXN各占 50% dotnet run -c Release -f net10.0 -- -t 2 -b 1 --dbsize 1024 -x --client SERedis --op-workload READ_TXN,WRITE_TXN --op-percent 50,50 # 纯 READ_WRITE_TXN 负载 dotnet run -c Release -f net10.0 -- -t 2 -b 1 --dbsize 1024 -x --client SERedis --op-workload READ_WRITE_TXN --op-percent 100参数说明运行前会先加载opts.DbSize条记录作为数据底数TxnPerfBench(..., int readPerTxn 4, int writePerTxn 4)可调节每事务的读写条数当前限制仅支持 batch size 为 1仅支持 SE.Redis 客户端与在线基准共用同一套选项体系。WATCH_TXN 负载的典型事务形如readPerTxn 2, writePerTxn 2WATCH x1 WATCH x2 MULTI GET x1 GET x2 SET x3 v3 SET x4 v4 EXEC10. 小结与进一步阅读本文从命令语义出发一直深入到状态机、2PL 键锁、WatchVersionMap/Modified Bit 乐观锁、统一存储事务上下文、集群 slot 验证、AOF 原子恢复与检查点一致性再到自定义事务与基准测试覆盖了 Garnet 客户端事务的完整链路。想继续深入可在当前仓库中查阅事务命令参考website/docs/commands/transactions.md事务实现文档website/docs/dev/transactions.md自定义事务开发website/docs/extensions/transactions.md事务核心源码libs/server/Transaction/TransactionManager.cs、libs/server/Transaction/TxnKeyEntry.cs、libs/server/Transaction/TxnWatchedKeysContainer.cs、libs/server/Transaction/WatchVersionMap.cs、libs/server/Transaction/TxnState.cs事务命令的会话层处理libs/server/Resp/RespServerSession.cs事务基准benchmark/Resp.benchmark/TxnPerfBench.cs提示Garnet 兼容现有 Redis 客户端事务命令MULTI/EXEC/DISCARD/WATCH/UNWATCH与 Redis 协议语义对齐可直接使用 SE.Redis 等客户端库进行开发与验证。【免费下载链接】garnetGarnet is a remote cache-store from Microsoft Research that offers strong performance (throughput and latency), scalability, storage, recovery, cluster sharding, key migration, and replication features. Garnet can work with existing Redis clients.项目地址: https://gitcode.com/GitHub_Trending/garnet4/garnet创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价