StackExchange.Redis 系列 5:事务

  • 本系列博文是“伪”官方文档翻译,并非完全将官方文档进行翻译,而是我在查阅、测试原始文档并转换为自己东西后进行的“准”翻译。
  • 原始文档见此:https://stackexchange.github.io/StackExchange.Redis/
  • 本系列本博文基于 redis 5.0.6,系列中部分博文跟官方文档有出入,有不同见解 / 说明不当的地方,还请大家不吝拍砖。

Redis 中的事务说明

  • Redis 中的事务跟我们常说的数据库事务不同:
    • 数据库事务必须保证全部成功,否则就回滚。
    • 而 Redis 中的事务更偏向于“一组打包的批量执行脚本”,其中任何一条命令的执行失败,不会导致已经执行的命令回滚,也不会中断后续的命令执行。
  • 在使用数据库事务的时候,你可以在事务中使用条件判断。但在 Redis 事务中,你无法使用条件判断。(条件见下方说明)

如何使用事务?

  • 在 Redis 原生命令中使用 MULTI 来开启事务,EXEC 来执行事务(或者 DISCARD 来取消事务)
  • 一旦使用 MULTI,在 MULTI 之后的命令不会立即执行,它们会排队,直到接收到 EXEC命令。如果接收到的时 DISCARD 命令,则所有已传输的命令会全部抛弃。
    • 因为 Redis 命令会排队执行,所以无法在 Redis 中使用条件判断。

是否有方法可以在 redis 事务中使用条件判断?

答案是肯定的,可以通过使用 WATCH 和 UNWATCH 命令来实现 redis 中使用条件判断。

  • WATCH :监听一个 key,当这个 key 有任何变动,都会导致事务的回滚

使用 Redis 原生命令来使用事务

WATCH {custKey}
HEXISTS {custKey} "UniqueId"
(check the reply, then either:)
MULTI
HSET {custKey} "UniqueId" {newId}
EXEC
(or, if we find there was already an unique-id:)
UNWATCH #取消所有 key 被监听。

使用 StackExchange.Redis 来使用事务。

  • StackExchange.Redis 通过 Multiplexer 方法来实现事务。

    var newId = CreateNewId();
    var tran = db.CreateTransaction();
    tran.AddCondition(Condition.HashNotExists(custKey, "UniqueID"));
    tran.HashSetAsync(custKey, "UniqueID", newId);
    bool committed = tran.Execute();
    
  • 当所有限制条件都通过的时候,才会执行 EXEC,否则会使用 DISCARD 进行回滚。
  • 需要注意的是只有在异步方法中才能调用 “db.CreateTransaction()”方法,同步方法中无法点出该方法。
  • 当事务成功执行,则正常获取结果,否则,所有 Tasks 都将会 cancelled。

    内置命令 When 说明

  • 判断是否存在一个对象/不存在一个对象,这样的使用场景还是比较普遍的,因此 StackExchange.Redis 内置了一个 When 枚举参数来简化这种操作。
  • 使用 When 来简化上方的业务代码:

var newId = CreateNewId();
bool wasSet = db.HashSet(custKey, "UniqueID", newId, When.NotExists);
  • 这里的 When.NotExists 会触发 SETNX 命令而非 HSET 命令。

LUA

  • Redis 2.6 版本开始,提供了 LUA 脚本。因为 LUA 脚本在执行的时候会独占服务器,所以通过 LUA 脚本,可以更加语义化的实现”事务”。
  • 相对的,在执行 LUA 脚本的时候,整个服务器会被独占,导致其他请求等待。

EVAL "if redis.call('hexists', KEYS[1], 'UniqueId') then return redis.call('hset', KEYS[1], 'UniqueId', ARGV[1]) else return 0 end" 1 {custKey} {newId}

上述 Redis 原生命令可以用以下 StackExchange.Redis 代码来实现:

var wasSet = (bool) db.ScriptEvaluate(@"if redis.call('hexists', KEYS[1], 'UniqueId') then return redis.call('hset', KEYS[1], 'UniqueId', ARGV[1]) else return 0 end", new RedisKey[] { custKey }, new RedisValue[] { newId });
  • 需要注意的是,返回数据取决于你的脚本内容,你需要对结果按需进行转换。如上方例子,返回结果是 Boolean 类型