事务
multi,exec,discard和watch是Redis事务的基础。他们能够保证一组命令独立的运行:
一个事务中的命令会被序列化的顺序执行。事务过程中的请求不会被其他的请求中断,这种方式保证了命令执行的独立性。
要么所有命令都成功执行,要么一个都不会执行,Redis事务也具有原子性。exec命令触发执行一个事务内的所有命令,因此如果事务过程中一个客户端在调用exec之前断开了与服务端的连接,那么所有命令都不会执行。换言之,如果调用了exec命令,那么所有的操作都会被执行。当使用append-only file备份时,Redis通过一个独立的write(2)进程将事务写入磁盘。当Redis服务宕机或被管理员强行中断,可能会导致只有一个部分操作被记录了,Redis在重启时会检查这些情况,并报错退出。使用redis-check-aof可以修复append-only备份文件,移除不完整事务让服务器可以顺利重启。
使用
通过multi启动一个Redis事务。该命令总是响应OK。此时用户可以设置多条命令,然而Redis并不会立即执行这些命令,仅将这些命令推入队列中。直到调用exec命令,队列中的命令会被一次性执行完成。调用discard命令会清空事务队列并退出事务。以下foo和bar将会保持原子性:
从上图看出,exec的执行结果是一个数组,数组的值和事务队列中的命名一一对应。当redis连接处于multi模式时,所有的命令相应的都是QUEUED。当调用exec时,所有的命令都只做简单的顺序执行。
事务内部错误
在Redis事务过程中可能会产生两种命令错误:
- 一个命令推送到队列中失败,因此在exec调用之前就会产生一个错误。对于Redis实例来说,可能是语法错误(错误的参数个数,错误的命令名称等),或者是诸如内存溢出的错误(Redis到达了内置限制(maxmemory)上限)。
- 命令在执行exec失败,例如在执行命令时传递了错误的值(例如:对字符串执行list操作)
客户端通常能够在执行exec通过命令的返回值感知到第一种错:如果命令执行返回的是QUEUED,表示命令时正确的,否则Redis就会返回一个错误。如果在入队列过程中发生错误,大部分客户端将会中断事务并忽该事务。然而从Redis 2.6.5开始,服务端将会记录该错误,然后拒绝执行该事务,并在exec时返回该错误,并自动忽略该错误。
在Redis2.6.5之前这些行为是在执行exec时仅执行已经加入队列中的部分命令,忽略之前的错误。新的行为使事务流程更加简洁,因此整个事务只用发送一次,并且在响应后一次性读取。exec执行后发生的错误没有做特殊的处理:事务的所有其他的命令都会被执行尽管部分命令会执行出错。就像这样:
exec返回了两个元素,一个OK,一个error。因此客户端需要针对这些信息作出相应的处理。其中最重要的是尽管一个命令失败了,队列中的其他命令都成功执行了,Redis并没有阻止这些正确命令的执行。通过结果我们看到:
另外如果是语法性错误:
该命令会被拒绝添加到事务队列中。
为什么Redis不支持事务回滚
在前面的示例中,尽管在事务中部分命令可能会执行失败,但是Redis仍然执行了事务,并没有进行回滚,如果有关系型数据库的经验,就会发现这不太符合我们的逻辑。但是对于这样的情况我们有更好的选择来处理:
- Redis命令会失败的原因只会是语法错误或者Redis键值的数据类型错误,这意味着这类型错误只会是程序本身的错误,这类型的错误很容易在开发阶段就被发现,而不需要等到生产环境来发觉。
- Redis不需要回滚的能力使Redis内部更加简洁和高效
忽略队列命令
discard命令通常用来中断一个事务,此时所有的命令都不会执行,并且连接的状态也会恢复为常规状态:
乐观锁应用
watch通常用来为Redis事务提供CAS行为检查和设置。被watch的键将会被跟踪变化,一旦被watch的键在exec之前发生改变,那么整个事务都会被中断,并且exec会返回一个null通知表示事务失败了。例如:如果我们需要一个键自动增长1(不用incr命令),那么我们可能会尝试这么实现:
val = GET mykey val = val + 1 SET mykey $val
如果在指定时间内只有单一的客户端来操作,这个操作是比较合理的。但是如果同时有多个客户端尝试增加这个键,就会产生竞争了。例如:A客户端和B客户端都读取了老的值,例如10,这个值将被两个客户端都增加为11,然后最终set命令设置的值也就是11,而不是12。不过幸运的是watch让我们很轻松的解决这类问题:
watch mywatch val = get mywatch val = val + 1 multi set mywatch $val exec
此时如果有其他客户端事务在本次事务从watch到exec的过程中修改了val,那么本次事务将会失败。我们期望在同一时间反复操作的过程中不会出现新的竞争。这个形式的锁称为乐观锁。乐观锁是一种非常强大的锁形式。在很多应用案例中,多客户端通过不同的键进入,因此不太容易发生冲突 - 通常也就不需要反复操作。
watch解释
那么watch到底是什么呢?这是个让exec变成有条件执行的命令:通知Redis只有在被watch的键没有发生改变的情况下才能执行事务。这些改变来源包括:客户端(写命令),Redis本身(键到期或删除)。如果在watch之后,exec之前,一个键被修改了,那么整个事务都会中断。
注意:在Redis 6.0.9之前,过期的键不会引起事务的中断。事务内部的命令不会处罚watch条件,因为他们只是入队直到exec执行。watch可以被多次调用,对键的监视从watch执行后开始生效直到exec执行后结束。您也可以通过一个watch同时监听多个键。当调用exec后,无论事务是否中断,所有的键都被取消监听。当然客户端断开连接也会导致取消监听。通过unwatch可以刷新释放所有被监听的键。有时候我们通过锁锁住了一些键,但是后来发现这些键不满足我们的需求,此时我们可以通过直接调用unwatch来直接释放被锁住的键。
通过watch实现ZPOP
通过watch实现一个Redis不支持的ZPOP命令(ZPOPMIN,ZPOPMAX和他们的变种已经在Redis 5.0中实现了)是个不错的想法。这个命令主要用于有序集合中原子性的出栈一个score较小的元素。以下是简单实现:
watch myzset elements = zrange myzset 0 0 multi zrem myzset elements exec
如果exec失败(返回null)了,我们只需要重复该操作即可。
Redis脚本和事务
一段Redis脚本默认是事务性的,因此你能够用redis完成的,都可以用redis脚本来实现,通常redis脚本会更简单效率更高。存在这两种方式的原因是脚本是Redis2.6才引入的,在此之前事务已经存在了很长一段时间。