Garnet 事务(Transactions)完全指南:从 RESP 命令到 MULTI/EXEC/WATCH 源码级原理
2026/9/15 18:02:02 网站建设 项目流程

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

本文是 Garnet(Microsoft Research 开源的远程缓存存储)事务机制的技术指南,覆盖两类事务:客户端发起的事务(Redis 风格 MULTI/EXEC/WATCH)服务端自定义事务。文章以website/docs/commands/transactions.mdwebsite/docs/dev/transactions.md为骨架,结合libs/server/Transaction/下源码与TxnPerfBench基准深入剖析状态机、2PL 锁、WATCH 乐观锁、版本映射(WatchVersionMap)、AOF 恢复与检查点一致性,助你从“会调命令”进阶到“懂原理”。

1. 概述:Garnet 的两类事务

Garnet 支持两种事务(见 website/docs/dev/transactions.md):

  1. 客户端发起的事务(Redis 风格):通过MULTI/EXEC/DISCARD/WATCH/UNWATCH命令在客户端与服务器之间建立一个原子执行区间。
  2. 服务端自定义事务(Custom Server-side Transactions):在服务器侧注册一个新的自定义事务,随后任何 Garnet 客户端都可以调用它执行。其开发方法见 Extendsions 的 transactions 文档。

本文以客户端事务为主线:其命令参考见 website/docs/commands/transactions.md,实现原理见 website/docs/dev/transactions.md。

1.1 客户端事务的命令总览

命令语法作用RESP 返回
MULTIMULTI标记事务块开始,后续命令进入排队状态Simple string:OK
EXECEXEC原子执行所有排队的命令,并恢复连接为正常状态Array reply(每个元素是每条命令的结果),或 Nil reply(因 WATCH 的 key 被修改而中止)
DISCARDDISCARD清空所有排队命令,恢复连接为正常状态Simple string:OK
WATCHWATCH key [key ...]监视指定 key,用于条件执行事务Simple string:OK
UNWATCHUNWATCH清空之前所有被监视的 keySimple string:OK

事务命令参考的权威描述位于 website/docs/commands/transactions.md,其中所有命令在命令文档页面都有对应条目(在 website/docs/commands 目录下)。

2. 客户端事务的用法与语义

2.1 基本流程:MULTI → 命令排队 → EXEC

MULTI SET key1 value1 SET key2 value2 EXEC
  • MULTI之后,连接进入“事务排队”状态,其后的命令不会被立即执行,而是被排队;
  • EXEC触发原子执行——要么全部执行,要么全部不执行
  • 执行完成后连接恢复为正常状态,EXEC返回一个数组,每个元素对应该事务中一条命令的返回值。

2.2 乐观锁与条件事务:WATCH / UNWATCH

Garnet 的 WATCH 用于实现**乐观锁(optimistic locking)CAS(check-and-set)**语义:

  1. 客户端在MULTI之前用WATCH监视一个或多个 key;
  2. MULTI/EXEC区间外读取这些 key 并计算结果;
  3. EXEC时,若任何一个被监视的 key 在此期间被修改,事务整体中止(返回 Nil reply);否则正常提交。

官方文档给出的经典 CAS 示例(见 website/docs/dev/transactions.md):

WATCH 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;
  • 注意:在EXECDISCARDUNWATCH执行之后,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并记录txnStartHeadoperationCntTxnNetworkEXECStarted时调用txnManager.Run()启动执行,在Running时调用txnManager.Commit()提交。

3.2 TransactionManager 的职责

TransactionManager.cs 是事务引擎的核心,职责如下。

3.2.1 存储事务状态
  • stateTxnState枚举)跟踪当前状态;
  • txnStartHead记录MULTI之后网络缓冲区中第一条命令的位置;
  • operationCntTxn记录排队命令条数;
  • PerformWrites标记事务是否包含写操作(决定是否需要写 AOF);
  • storeTypesTransactionStoreTypesMain/Object/Unified标志位)记录事务涉及哪些存储(String 主存储、对象存储、统一存储)。
3.2.2 排队命令:TrySkip 与 2PL 键锁定

Started状态下,TransactionManager会:(1) 排队后续命令;(2) 保存这些命令中用到的 key,以便在执行时按2PL(两阶段锁)加锁。

排队的实现很有特色——命令不复制到独立缓冲区,而是“留在网络缓冲区里”:通过RespServerSessionTrySkip函数跳过命令,同时保存 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方法),其步骤为:

  1. 根据存储类型获取对应的TransactionalContextBeginTransaction);
  2. 遍历TxnKeyEntries锁定所有需要的 keyLockAllKeys/TryLockAllKeys,锁前先对 key hash 排序以保证稳定哈希表与确定性加锁顺序);
  3. 调用WatchedKeyContainer.ValidateWatchVersion()校验被监视 key 的版本是否与 watch 时一致;
    • 通过则继续执行;失败则调用TransactionManager.Reset(true)重置(true表示需要解锁),事务中止;
  4. 若事务包含写操作且 AOF 开启,写入TxnStart 标记到 AOF,以保证中途失败时可原子恢复(EnqueueTxn(AofEntryType.TxnStart, ...))。

随后state置为Running,网络readHead指向MULTI后的第一条命令,开始真正执行这些命令。

3.2.4 提交:Commit()

执行再次遇到EXEC且状态为Running时,调用TransactionManager.Commit()(TransactionManager.cs 的Commit方法):

  • 解锁Run中锁定的所有 key(UnlockAllKeys);
  • 重置TransactionManagerWatchedKeysContainer
  • 若事务有写操作且 AOF 开启,向 AOF 追加TxnCommit 消息EnqueueTxn(AofEntryType.TxnCommit, ...))。

提交时的Reset(true)还会通过stateMachineDriver.EndTransaction(txnVersion)释放事务版本,并结束各存储的 TransactionalContext(TransactionManager.cs)。

4. WATCH 乐观锁:VersionMap 与 Modified Bit

4.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 Bit:Tsavorite 记录级修改标记

Modified bit 用于在 Tsavorite 中追踪记录的修改状态:记录被修改时其 modified bit 置为 1,并保持为 1,直到有人调用ResetModifiedAPI 将其重置为 0

为支持 WATCH,Garnet 新增了ClientSession.ResetModified(ref Key key)API:接收一个TKey(Garnet 传入FixedSpanByteKey),将RecordInfo字 CAS 到同一字但modified bit 被重置的版本。

4.3 Watch 流程

  1. 客户端WATCH key时,Garnet 调用ResetModifiedAPI,并把 key 存入WatchedKeysContainer
  2. 同时从版本映射读取该记录的版本号,与 key 一起保存;
  3. 事务执行时,遍历WatchedKeysContainer中所有 key,若版本仍与 watch 时相同则继续,否则中止。

在 TxnWatchedKeysContainer.cs 的AddWatch中可以看到:key 字节被复制到独立的 scratch buffer(txnScratchBufferAllocator.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 相对较少,开销不关键。

版本递增发生在MainSessionFunctionsObjectSessionFunctions的以下回调中:InPlaceUpdater(若被监视)、InPlaceWriter(若被监视)、InPlaceDeleter(若被监视)、PostInitialWriterPostInitialUpdaterPostCopyUpdaterPostInitialDeleter

4.5 Unwatch 流程

  • 记录在 Tsavorite 中被修改时,modified bit 自动置位;
  • 用户调用UNWATCH时,Garnet 只需重置WatchedKeysContainer
  • 每次执行完DISCARDEXECUNWATCH命令后都会清空所有监视。

5. 加锁与存储:2PL、锁类型与统一存储

5.1 TxnKeyEntry:锁集合

libs/server/Transaction/TxnKeyEntry.cs 中TxnKeyEntry是一个 9 字节结构体([StructLayout(LayoutKind.Explicit, Size = 9)]):8 字节keyHash+ 1 字节LockType(Shared/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.cs):

  • Main(1):String 主存储;
  • Object(2):对象存储(Hash/List/Set/SortedSet 等对象类型);
  • Unified(4):统一存储。

AddTransactionStoreTypeStoreType映射为事务存储类型;BeginTransaction/LocksAcquired/EndTransaction会对storeTypes中涉及的所有存储上下文执行对应操作。由此可以看出,Garnet 的事务可以横跨字符串与对象类型,并统一通过 unified store 的 transactional context 加锁。

5.3 WATCH 键在 EXEC 时的加锁

Run()的第一步(非内部事务时)会调用watchContainer.SaveKeysToLock(this):将WatchedKeysContainer中所有仍处于 watched 状态的 key 以Shared 锁注册进TxnKeyEntries(TxnWatchedKeysContainer.cs),与事务命令自身的 key 一起参与排序与加锁。

6. 集群模式下的事务:slot 验证

当集群启用时(clusterEnabled == true),TransactionManager会为事务收集所有 key 以进行slot 验证

  • GetSlotVerificationInput会先把排队命令的 key 复制到 scratch buffer(CopyExistingKeysToScratchBuffer),再调用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.TxnCommit(TransactionManager.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 → Finalize
    1. proc.Prepare(使用GarnetWatchApi,即带 WATCH 语义的只读 API)收集读集并校验;
    2. Run(加锁 + WATCH 版本校验)后进入Main,在锁定的数据上执行主体逻辑(TransactionalGarnetApi);
    3. 若有写操作则Log到 AOF(AofEntryType.StoredProcedure);
    4. Commit解锁并写提交标记;最后执行Finalize(AOF 重放期间跳过,因为提交会由 AOF 重放接管);
  • 自定义事务同样受PerformWrites、AOF 与事务版本管理约束;
  • 支持FailFastOnKeyLockFailureKeyLockTimeout(对应TryLockAllKeys(lock_timeout)的可失败快速路径,见 TransactionManager.cs)。

9. 性能验证:TxnPerfBench 微基准

为了验证客户端事务性能,仓库提供了TxnPerfBench(位于 benchmark/Resp.benchmark/TxnPerfBench.cs),包含四种负载:

  • READ_TXN:一个事务内执行readPerTxnGET
  • WRITE_TXN:一个事务内执行writePerTxnSET
  • READ_WRITE_TXNSETGET混合(readPerTxn+writePerTxn);
  • WATCH_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 = 2):

WATCH x1 WATCH x2 MULTI GET x1 GET x2 SET x3 v3 SET x4 v4 EXEC

10. 小结与进一步阅读

本文从命令语义出发,一直深入到状态机、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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询