事务
Garnet 支持两种类型的事务
- 自定义服务器端事务
- 客户端发起的事务 (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
。
执行
当 TxnState
为 Started 且我们遇到 EXEC
时,我们调用 TransactionManager.Run()
。此函数执行以下操作:
- 首先根据存储类型获取主存储和/或对象存储的
LockableContext
。 - 遍历
TxnKeyEntry
并锁定所有需要的键。 - 调用
WatchedKeyContainer.ValidateWatchVersion()
- 它遍历所有被监视的键,并检查它们的版本是否与监视时相同
- 如果通过,我们继续执行,否则,我们调用
TransactionManager.Reset(true)
来重置事务管理器。我们传递给Reset
的true
参数表示它还需要解锁键。
- 它在 AOF 中写入事务开始指示器,以便在事务中途失败时原子地恢复
之后,TxnState 设置为 Running,网络 readHead
设置为 MULTI
之后的第一个命令,此时我们开始实际运行这些命令。当执行再次到达 EXEC 并且我们处于 Running 状态时,它会调用 TransactionManager.Commit()
。它执行以下操作:
- 解锁我们在
Run
中锁定的所有键 - 重置
TransactionManager
和WatchedKeyContainer
- 它还将提交消息附加到 AOF
恢复优化
Garnet 会定期进行检查点,并在这些检查点之间更改其版本。为了获得检查点一致性,我们要求事务操作具有相同的版本,或者换句话说,处于相同的检查点窗口中。
为了强制执行此操作,我们目前执行以下操作:
- 当 TsavoriteStateMachine 处于
Prepare
阶段时,我们不允许事务开始执行以让检查点完成 - 如果存在正在运行的事务,并且 TsavoriteStateMachine 移动到
Prepare
,我们不允许版本更改发生,直到事务完成执行。 - 这两个操作都通过
session.IsInPreparePhase
和Run
函数开头的两个 while 循环实现
WATCH 命令
它用于实现乐观锁。
- 为事务提供检查和设置 (CAS) 行为。
- 监控键以检测它们的更改。
- 如果在 EXEC 命令之前至少一个被监视的键被修改,则整个事务中止。
- 它通过
TsavoriteKV
中的Modified
位和Garnet
中的VersionMap
实现
版本映射
它监控对键的修改。每次被监视的键被修改时,我们都会在其版本映射中增加其版本。
- 它通过
Hash Index
实现 - 为了防止关键路径中正常操作的开销,我们只在某些情况下增加版本
-
对于内存中的记录,我们只增加被监视键的版本。Garnet 中被监视的键使用 Tsavorite 中的
Modified
位来跟踪修改(更多关于 Modified 位的内容见下文) -
对于磁盘中的记录,我们为复制更新 RMW 和 Upsert 增加版本。我们有意接受此开销,因为复制更新不那么频繁,并且开销不关键。
-
增加
MainStoreFunctions
和ObjectStoreFunctions
中的版本- 如果被监视,则为
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
- 在每个
DISCARD
、EXEC
、UNWATCH
命令之后,我们都会取消监视所有内容
测试
我们编写了一个微基准测试 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
运行一个包含 readPerTxn
个 GET
请求的事务;
WRITE_TXN
运行一个包含 writePerTxn
个 SET
请求的事务;
READ_WRITE_TXN
运行 SET
和 GET
请求的混合(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