Redis的事务:先以 MULTI 开始一个事务,然后将多个命令入队到事务中, 最后由 EXEC 命令触发事务。当碰到命令:MULTI (标记一个事务块的开始),在该连接上的命令不会执行:它们会排队(调用方会得到每个队列的回复)。当遇到命令:EXEC(执行所有事务块内的命令),它们被应用到一个单独的单元中(比如:没有其它连接操作之间的那个时间段)。如果是命令 DISCARD(取消事务,放弃执行事务块内的所有命令) 而不是 EXEC,那么所有的操作都会不执行(回滚)。因为命令是在事务里面排队的,所以你不能改变内部事务。

  • 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
  • 事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。

一个事务从开始到执行会经历以下三个阶段:

  • 开始事务。
  • 执行事务。
Redis 事务命令
  • DISCARD 取消事务,放弃执行事务块内的所有命令。
  • EXEC 执行所有事务块内的命令。
  • MULTI 标记一个事务块的开始。
  • UNWATCH 取消 WATCH 命令对所有 key 的监视。
  • WATCH key [key …] 监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。

例如:在SQL数据库中你可能回做如下操作:

WATCH{key} 命令用于监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断且回滚。EXEC 所做的和 DISCARD 一样(调用方一开始就能发现并重试)。那么你能做的是:使用命令:WATCH 某个键,以正常的方式,来检查给定键的数据,然后使用 MULTI/EXEC 命令执行你的改变。当你检查数据的时候,你会发现你实际上不需要事务,你可以用 UNWATCH 命令用于取消 WATCH 命令对所有 key 的监视。注意:在使用 EXECDISCARD 的时候,观察键也是可以重置的(如果执行EXEC 或者DISCARD,则不需要手动执行UNWATCH)。所以在Redis层,这只是概念上的:

  1. HEXISTS {custKey} "UniqueId"
  2. MULTI
  3. EXEC
  4. (or, if we find there was already an unique-id:)
  5. UNWATCH

这可能看起来很奇怪:只有跨越单个操作时才可以使用 MULTI/EXEC 命令,但重要的是我们现在也可以使用 {custKey} 从所有其它的连接中来跟踪变更:如果其他人更改这个Key,那么事务会被终止。

更复杂的事实是StackExchange.Redis使用的是多路复用器的方式。

我们不能只让并发调用方发布 WATCH / UNWATCH / MULTI / EXEC / DISCARD:这应该是混合在一起的。所以一个额外的抽象被给出:另外会让使事情更简单准确:约束。约束是预定义测试包括 WATCH 某种类型的测试并对结果进行检查。如果所有的约束都通过了,那么要么是以 MULTI / EXEC 发布(从事务开始,到执行整个事务块);要么是以 UNWATCH 发布(取消 WATCH 命令对所有 key 的监视)。阻止命令于其它调用方被混合在一起;所以例子可以是:

还应该注意的是,Redis已经为我们预料到了许多常见的场景(特别是:key/hash的存在,就像上面一样),还有单操作(single-operation)原子命令的存在。
通过 When 来访问,所以前面的示例也可以这样来实现:

  1. bool wasSet = db.HashSet(custKey, "UniqueID", newId, When.NotExists);

注意:When.NotExists 会使用命令 HSETNX 而不会使用 HSET

你应该记住Redis 2.6及以上的版本,它可以描述为:一个常用的工具,使多个操作在服务器端的以一个原子单元执行。在使用Lua脚本的时候,由于不需要服务于其它的连接,所以它的行为更像是一个事务处理,但是没有 MULTI / EXEC 那么复杂。这也避免了诸如调用方和服务器端之间带宽和延迟的问题。但是代价是在脚本执行的时候独占了服务器。

在Redis层(假设 HSETNX 不存在)我们可以有如下实现:

  1. 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",

注意:来自 ScriptEvaluateScriptEvaluateAsync 的响应是可变的,这依赖于你所写的脚本。响应结果可以被强制转换,在这个例子中是被转换为 bool 类型。