跳到主要内容

事务

Garnet 支持两种类型的事务

  1. 自定义服务器端事务
  2. 客户端发起的事务 (Redis)

自定义服务器端事务

自定义事务允许添加新事务并在服务器端将其注册到 Garnet。然后,可以从任何 Garnet 客户端调用此注册事务,以在 Garnet 服务器上执行事务。有关开发自定义服务器端事务的更多信息,请参阅扩展部分下的事务页面

客户端发起的事务 (Redis)

您可以在此处阅读更多信息:Redis 事务。在此设计中,事务操作在 MULTI/EXEC 范围内进行。此范围内的每个操作都是事务的一部分。该模型不允许您在 MULTI/EXEC 范围内使用读取结果,但允许您在之前读取和监控键(即 watch),如果在执行时它们未更改,则事务将提交。

示例

WATCH mykey
val = GET mykey
val = val + 1 # not Redis command this happens outside
MULTI
SET mykey $val
EXEC

在上述示例中,如果在 EXEC 命令之前 mykey 发生更改,则事务将中止,因为 val 的计算已失效。

事务后端

Garnet 中的事务使用以下类实现

  • TransactionManager
  • WatchVersionMap
  • WatchedKeyContainer
  • RespCommandsInfo

TransactionManager 类职责

存储事务状态:

  • Started:在 MULTI 命令后进入此状态,TxnManager 将在此状态下将除 EXEC 之外的任何命令排队
  • Running:在 EXEC 后进入此状态,TxnManager 将在此状态下运行排队的命令
  • Aborted:在发生任何不良情况时进入此状态

命令排队:

当 TxnManager 进入 Started 状态时,它将 (1) 随后将任何命令排队,并且 (2) 使用 2PL 在执行时保存这些命令中使用的任何键以进行锁定。为了排队命令,它们被留在网络缓冲区中。使用 RespServerSession 中的 TrySkip 函数。为了在执行时锁定键,我们使用 TxnKeyEntry 数组存储网络缓冲区中键的实际内存位置的指针,该数组具有 ArgSlice 和锁定类型(共享或独占)。

TrySkip 函数使用 RespCommandsInfo 类跳过正确数量的令牌并检测语法错误。RespCommandsinfo 存储每个命令的 Arity 或参数数量。例如,GET 命令的参数数量为两个。命令令牌 GET 和一个键。我们为可以有多个参数的命令存储具有负值的最小参数数量。SET 命令的参数数量为 -3 表示它至少需要三个参数(包括命令令牌)。

TrySkip 期间,我们调用 TransactionManager.GetKeys,它遍历参数并为参数中的每个键存储 TxnKeyEntry

执行

TxnStateStarted 且我们遇到 EXEC 时,我们调用 TransactionManager.Run()。此函数执行以下操作:

  1. 首先根据存储类型获取主存储和/或对象存储的 LockableContext
  2. 遍历 TxnKeyEntry 并锁定所有需要的键。
  3. 调用 WatchedKeyContainer.ValidateWatchVersion()
    • 它遍历所有被监视的键,并检查它们的版本是否与监视时相同
    • 如果通过,我们继续执行,否则,我们调用 TransactionManager.Reset(true) 来重置事务管理器。我们传递给 Resettrue 参数表示它还需要解锁键。
  4. 它在 AOF 中写入事务开始指示器,以便在事务中途失败时原子地恢复

之后,TxnState 设置为 Running,网络 readHead 设置为 MULTI 之后的第一个命令,此时我们开始实际运行这些命令。当执行再次到达 EXEC 并且我们处于 Running 状态时,它会调用 TransactionManager.Commit()。它执行以下操作:

  • 解锁我们在 Run 中锁定的所有键
  • 重置 TransactionManagerWatchedKeyContainer
  • 它还将提交消息附加到 AOF

恢复优化

Garnet 会定期进行检查点,并在这些检查点之间更改其版本。为了获得检查点一致性,我们要求事务操作具有相同的版本,或者换句话说,处于相同的检查点窗口中。

为了强制执行此操作,我们目前执行以下操作:

  • 当 TsavoriteStateMachine 处于 Prepare 阶段时,我们不允许事务开始执行以让检查点完成
  • 如果存在正在运行的事务,并且 TsavoriteStateMachine 移动到 Prepare,我们不允许版本更改发生,直到事务完成执行。
  • 这两个操作都通过 session.IsInPreparePhaseRun 函数开头的两个 while 循环实现

WATCH 命令

它用于实现乐观锁。

  • 为事务提供检查和设置 (CAS) 行为。
  • 监控键以检测它们的更改。
  • 如果在 EXEC 命令之前至少一个被监视的键被修改,则整个事务中止。
  • 它通过 TsavoriteKV 中的 Modified 位和 Garnet 中的 VersionMap 实现

版本映射

它监控对键的修改。每次被监视的键被修改时,我们都会在其版本映射中增加其版本。

  • 它通过 Hash Index 实现
  • 为了防止关键路径中正常操作的开销,我们只在某些情况下增加版本
    • 对于内存中的记录,我们只增加被监视键的版本。Garnet 中被监视的键使用 Tsavorite 中的 Modified 位来跟踪修改(更多关于 Modified 位的内容见下文)

    • 对于磁盘中的记录,我们为复制更新 RMW 和 Upsert 增加版本。我们有意接受此开销,因为复制更新不那么频繁,并且开销不关键。

    • 增加 MainStoreFunctionsObjectStoreFunctions 中的版本

      • 如果被监视,则为 InPlaceUpdater
      • 如果被监视,则为 ConcurrentWriter
      • 如果被监视,则为 ConcurrentDeleter
      • PostSingleWriter
      • PostInitialUpdater
      • PostCopyUpdater
      • PostSingleDeleter

Modified 位

Modified 位跟踪 Tsavorite 中记录的修改。每个记录的 Modified 位在被修改时设置为“1”,并保持“1”直到有人使用 ResetModified API 将其重置为零。

WATCH

  • 我们添加了一个 ClientSesssion.ResetModified(ref Key key) API。
    • RecordInfo 字 CAS 到同一个字,但重置 Modified 位
  • 当有人在 Garnet 中监视一个键时,我们调用 ResetModified API 并将该键存储在 WatchedKeyContainer 中。
  • 在监视时,我们从版本映射中读取该记录的版本,并将其与键一起存储在 WatchedKeyContainer 中。
  • 在事务执行时,我们遍历 WatchedKeyContainer 中的所有键,如果它们的版本仍然相同,我们继续执行事务。

UNWATCH

  • 当 Tsavorite 中的记录被修改时,Modified 位会自动设置
  • 当用户在 Garnet 中调用 Unwatch API 时,我们只是简单地重置 WatchedKeyContainer
  • 在每个 DISCARDEXECUNWATCH 命令之后,我们都会取消监视所有内容

测试

我们编写了一个微基准测试 TxnPerfBench 来测试客户端事务。该基准测试包含四种不同的工作负载:

  • READ_TXN
  • WRITE_TXN
  • READ_WRITE_TXN
  • WATCH_TXN

它看起来像在线基准测试,并且可以有不同百分比的不同工作负载

dotnet run -c Release -t 2 -b 1 --dbsize 1024 -x --client SERedis --op-workload WATCH_TXN --op-percent 100
dotnet run -c Release -t 2 -b 1 --dbsize 1024 -x --client SERedis --op-workload READ_TXN,WRITE_TXN --op-percent 50,50
dotnet run -c Release -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)

  • 我们目前只支持批处理大小为一。
  • 我们目前只支持 SE.Redis 客户端。

READ_TXN

运行一个包含 readPerTxnGET 请求的事务;

WRITE_TXN

运行一个包含 writePerTxnSET 请求的事务;

READ_WRITE_TXN

运行 SETGET 请求的混合(readPerTxn, writePerTxn

WATCH_TXN

此工作负载监视 readPerTxn 个键。然后启动一个事务,读取被监视的键,并写入 writePerTxn 个键。

readPerTxn = 2
writePerTxn = 2

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