MULTIEXECDISCARD 和 WATCH 命令是 Redis 事务功能的基础 Redis 事务允许在一次单独的步骤中执行一组命令, 并且可以保证如下两个重要事项:
>Redis 会将一个事务中的所有命令序列化, 然后按顺序执行 Redis 不可能在一个 Redis 事务的执行过程中插入执行另一个客户端发出的请求这样便能保证 Redis 将这些命令作为一个单独的隔离操作执行 > 在一个 Redis 事务中, Redis 要么执行其中的所有命令, 要么什么都不执行因此, Redis 事务能够保证原子性 EXEC 命令会触发执行事务中的所有命令因此, 当某个客户端正在执行一次事务时, 如果它在调用 MULTI 命令之前就从 Redis 服务端断开连接, 那么就不会执行事务中的任何操作; 相反, 如果它在调用 EXEC 命令之后才从 Redis 服务端断开连接, 那么就会执行事务中的所有操作当 Redis 使用只增文件 (AOF:Append-only File) 时, Redis 能够确保使用一个单独的 write(2)系统调用, 这样便能将事务写入磁盘然而, 如果 Redis 服务器宕机, 或者系统管理员以某种方式停止 Redis 服务进程的运行, 那么 Redis 很有可能只执行了事务中的一部分操作 Redis 将会在重新启动时检查上述状态, 然后退出运行, 并且输出报错信息使用 redis-check-aof 工具可以修复上述的只增文件, 这个工具将会从上述文件中删除执行不完全的事务, 这样 Redis 服务器才能再次启动
从 2.2 版本开始, 除了上述两项保证之外, Redis 还能够以乐观锁的形式提供更多的保证, 这种形式非常类似于检查再设置 (CAS:Check And Set) 操作本文稍后会对 Redis 的乐观锁进行描述
一相关命令
1. MULTI
用于标记事务块的开始 Redis 会将后续的命令逐个放入队列中, 然后才能使用 EXEC 命令原子化地执行这个命令序列
这个命令的运行格式如下所示:
MULTI
这个命令的返回值是一个简单的字符串, 总是 OK
2. EXEC
在一个事务中执行所有先前放入队列的命令, 然后恢复正常的连接状态
当使用 WATCH 命令时, 只有当受监控的键没有被修改时, EXEC 命令才会执行事务中的命令, 这种方式利用了检查再设置 (CAS) 的机制
这个命令的运行格式如下所示:
EXEC
这个命令的返回值是一个数组, 其中的每个元素分别是原子化事务中的每个命令的返回值 当使用 WATCH 命令时, 如果事务执行中止, 那么 EXEC 命令就会返回一个 Null 值
3. DISCARD
清除所有先前在一个事务中放入队列的命令, 然后恢复正常的连接状态
如果使用了 WATCH 命令, 那么 DISCARD 命令就会将当前连接监控的所有键取消监控
这个命令的运行格式如下所示:
DISCARD
这个命令的返回值是一个简单的字符串, 总是 OK
4. WATCH
当某个事务需要按条件执行时, 就要使用这个命令将给定的键设置为受监控的
这个命令的运行格式如下所示:
WATCH key [key ...]
这个命令的返回值是一个简单的字符串, 总是 OK
对于每个键来说, 时间复杂度总是 O(1)
5. UNWATCH
清除所有先前为一个事务监控的键
如果你调用了 EXEC 或 DISCARD 命令, 那么就不需要手动调用 UNWATCH 命令
这个命令的运行格式如下所示:
UNWATCH
这个命令的返回值是一个简单的字符串, 总是 OK
时间复杂度总是 O(1)
二使用方法
使用 MULTI 命令便可以进入一个 Redis 事务这个命令的返回值总是 OK 此时, 用户可以发出多个 Redis 命令 Redis 会将这些命令放入队列, 而不是执行这些命令一旦调用 EXEC 命令, 那么 Redis 就会执行事务中的所有命令
相反, 调用 DISCARD 命令将会清除事务队列, 然后退出事务
以下示例会原子化地递增 foo 键和 bar 键的值:
正如从上面的会话所看到的一样, EXEC 命令的返回值是一个数组, 其中的每个元素都分别是事务中的每个命令的返回值, 返回值的顺序和命令的发出顺序是相同的
当一个 Redis 连接正处于 MULTI 请求的上下文中时, 通过这个连接发出的所有命令的返回值都是 QUEUE 字符串 (从 Redis 协议的角度来看, 返回值是作为状态回复(Status Reply) 来发送的)当调用 EXEC 命令时, Redis 会简单地调度执行事务队列中的命令
三事务内部的错误
在一个事务的运行期间, 可能会遇到两种类型的命令错误:
一个命令可能会在被放入队列时失败因此, 事务有可能在调用 EXEC 命令之前就发生错误例如, 这个命令可能会有语法错误(参数的数量错误命令名称错误, 等等), 或者可能会有某些临界条件(例如: 如果使用 maxmemory 指令, 为 Redis 服务器配置内存限制, 那么就可能会有内存溢出条件)
在调用 EXEC 命令之后, 事务中的某个命令可能会执行失败例如, 我们对某个键执行了错误类型的操作 (例如, 对一个字符串(String) 类型的键执行列表 (List) 类型的操作)
可以使用 Redis 客户端检测第一种类型的错误, 在调用 EXEC 命令之前, 这些客户端可以检查被放入队列的命令的返回值: 如果命令的返回值是 QUEUE 字符串, 那么就表示已经正确地将这个命令放入队列; 否则, Redis 将返回一个错误如果将某个命令放入队列时发生错误, 那么大多数客户端将会中止事务, 并且丢弃这个事务
然而, 从 Redis 2.6.5 版本开始, 服务器会记住事务积累命令期间发生的错误然后, Redis 会拒绝执行这个事务, 在运行 EXEC 命令之后, 便会返回一个错误消息最后, Redis 会自动丢弃这个事务
在 Redis 2.6.5 版本之前, 如果发生了上述的错误, 那么在客户端调用了 EXEC 命令之后, Redis 还是会运行这个出错的事务, 执行已经成功放入事务队列的命令, 而不会关心先前发生的错误从 2.6.5 版本开始, Redis 在遭遇上述错误时, 会采用先前描述的新行为, 这样便能轻松地混合使用事务和管道在这种情况下, 客户端可以一次性地将整个事务发送至 Redis 服务器, 稍后再一次性地读取所有的返回值
相反, 在调用 EXEC 命令之后发生的事务错误, Redis 不会进行任何特殊处理: 在事务运行期间, 即使某个命令运行失败, 所有其他的命令也将会继续执行
这种行为在协议层面上更加清晰在以下示例中, 当事务正在运行时, 有一条命令将会执行失败, 即使这条命令的语法是正确的:
上述示例的 EXEC 命令的返回值是批量的字符串, 包含两个元素, 一个是 OK 代码, 另一个是 - ERR 错误消息客户端会根据自身的程序库, 选择一种合适的方式, 将错误信息提供给用户
需要注意的是, 即使某个命令执行失败, 事务队列中的所有其他命令仍然会执行 Redis 不会停止执行事务中的命令
再看另一个示例, 再次使用 telnet 通信协议, 观察命令的语法错误是如何尽快报告给用户的:
这一次, 由于 INCR 命令的语法错误, Redis 根本就没有将这个命令放入事务队列
四为什么 Redis 不支持回滚?
如果你具备关系型数据库的知识背景, 你就会发现一个事实: 在事务运行期间, 虽然 Redis 命令可能会执行失败, 但是 Redis 仍然会执行事务中余下的其他命令, 而不会执行回滚操作, 你可能会觉得这种行为很奇怪
然而, 这种行为也有其合理之处:
只有当被调用的 Redis 命令有语法错误时, 这条命令才会执行失败(在将这个命令放入事务队列期间, Redis 能够发现此类问题), 或者对某个键执行不符合其数据类型的操作: 实际上, 这就意味着只有程序错误才会导致 Redis 命令执行失败, 这种错误很有可能在程序开发期间发现, 一般很少在生产环境发现
Redis 已经在系统内部进行功能简化, 这样可以确保更快的运行速度, 因为 Redis 不需要事务回滚的能力
对于 Redis 事务的这种行为, 有一个普遍的反对观点, 那就是程序有可能会有缺陷 (bug) 但是, 你应当注意到: 事务回滚并不能解决任何程序错误例如, 如果某个查询会将一个键的值递增 2, 而不是 1, 或者递增错误的键, 那么事务回滚机制是没有办法解决这些程序问题的请注意, 没有人能解决程序员自己的错误, 这种错误可能会导致 Redis 命令执行失败正因为这些程序错误不大可能会进入生产环境, 所以我们在开发 Redis 时选用更加简单和快速的方法, 没有实现错误回滚的功能
五丢弃命令队列
DISCARD 命令可以用来中止事务运行在这种情况下, 不会执行事务中的任何命令, 并且会将 Redis 连接恢复为正常状态示例如下所示:
六通过 CAS 操作实现乐观锁
Redis 使用 WATCH 命令实现事务的检查再设置 (CAS) 行为
作为 WATCH 命令的参数的键会受到 Redis 的监控, Redis 能够检测到它们的变化在执行 EXEC 命令之前, 如果 Redis 检测到至少有一个键被修改了, 那么整个事务便会中止运行, 然后 EXEC 命令会返回一个 Null 值, 提醒用户事务运行失败
例如, 设想我们需要将某个键的值自动递增 1(假设 Redis 没有 INCR 命令)
首次尝试的伪码可能如下所示:
- val = GET mykey
- val = val + 1
- SET mykey $val
如果我们只有一个 Redis 客户端在一段指定的时间之内执行上述伪码的操作, 那么这段伪码将能够可靠的工作如果有多个客户端大约在同一时间尝试递增这个键的值, 那么将会产生竞争状态例如, 客户端 - A 和客户端 - B 都会读取这个键的旧值 (例如: 10) 这两个客户端都会将这个键的值递增至 11, 最后使用 SET 命令将这个键的新值设置为 11 因此, 这个键的最终值是 11, 而不是 12
现在, 我们可以使用 WATCH 命令完美地解决上述的问题, 伪码如下所示:
- WATCH mykey
- val = GET mykey
- val = val + 1
- MULTI
- SET mykey $val
- EXEC
由上述伪码可知, 如果存在竞争状态, 并且有另一个客户端在我们调用 WATCH 命令和 EXEC 命令之间的时间内修改了 val 变量的结果, 那么事务将会运行失败
我们只需要重复执行上述伪码的操作, 希望此次运行不会再出现竞争状态这种形式的锁就被称为乐观锁, 它是一种非常强大的锁在许多用例中, 多个客户端可能会访问不同的键, 因此不太可能发生冲突 也就是说, 通常没有必要重复执行上述伪码的操作
七 WATCH 命令详解
那么 WATCH 命令实际做了些什么呢? 这个命令会使得 EXEC 命令在满足某些条件时才会运行事务: 我们要求 Redis 只有在所有受监控的键都没有被修改时, 才会执行事务 (但是, 相同的客户端可能会在事务内部修改这些键, 此时这个事务不会中止运行) 否则, Redis 根本就不会进入事务(注意, 如果你使用 WATCH 命令监控一个易失性的键, 然后在你监控这个键之后, Redis 再使这个键过期, 那么 EXEC 命令仍然可以正常工作)
WATCH 命令可以被调用多次简单说来, 所有的 WATCH 命令都会在被调用之时立刻对相应的键进行监控, 直到 EXEC 命令被调用之时为止你可以在单条的 WATCH 命令之中, 使用任意数量的键作为命令参数
当调用 EXEC 命令时, 所有的键都会变为未受监控的状态, Redis 不会管事务是否被中止当一个客户单连接被关闭时, 所有的键也都会变为未受监控的状态
你还可以使用 UNWATCH 命令(不需要任何参数), 这样便能清除所有的受监控键当我们对某些键施加乐观锁之后, 这个命令有时会非常有用因为, 我们可能需要运行一个用来修改这些键的事务, 但是在读取这些键的当前内容之后, 我们可能不打算继续进行操作, 此时便可以使用 UNWATCH 命令, 清除所有受监控的键在运行 UNWATCH 命令之后, Redis 连接便可以再次自由地用于运行新事务
如何使用 WATCH 命令实现 ZPOP 操作呢?
本文将通过一个示例, 说明如何使用 WATCH 命令创建一个新的原子化操作(Redis 并不原生支持这个原子化操作), 此处会以实现 ZPOP 操作为例这个命令会以一种原子化的方式, 从一个有序集合中弹出分数最低的元素以下源码是最简单的实现方式:
- WATCH zset
- element = ZRANGE zset 0 0
- MULTI
- ZREM zset element
- EXEC
如果伪码中的 EXEC 命令执行失败(例如, 返回 Null 值), 那么我们只需要重复运行这个操作即可
八 Redis 脚本和事务
根据定义, Redis 脚本也是事务型的因此, 你可以通过 Redis 事务实现的功能, 同样也可以通过 Redis 脚本来实现, 而且通常脚本更简单更快速
由于 Redis 从 2.6 版本才开始引入脚本特性, 而事务特性是很久以前就已经存在的, 所以目前的版本才有两个看起来重复的特性但是, 我们不太可能在短时间内移除对事务特性的支持因为, 即使不用求助于 Redis 脚本, 用户仍然能够规避竞争状态, 这从语义上来看是适宜的还有另一个更重要的原因, Redis 事务特性的实现复杂度是最小的
但是, 在相当长的一段时间之内, 我们不大可能看到整个用户群体都只使用 Redis 脚本如果发生这种情况, 那么我们可能会废弃, 甚至最终移除 Redis 事务
来源: http://www.92to.com/bangong/2018/02-19/33349422.html