事务
Redis事务的相关命令有MULTI,EXEC,DISCARD,WATCH。它们允许在一个步骤中执行一组命令,并有两个重要的保证:
事务中的所有命令都会被序列化并按顺序执行。在执行Redis事务的过程中,不会发生由另一个客户端发出的请求被服务的情况。这保证命令作为一个单独的隔离的操作被执行。
无论是所有的命令都被处理还是没有命令被处理,Redis事务都是原子的。EXEC命令触发事务中所有命令的执行,因此如果在调用MULTI命令之前,客户端在事务上下文中失去与服务器的连接,则不执行任何操作,相反如果调用EXEC命令, 所有的操作都被执行。在使用仅追加文件时,Redis确保使用单个写入(2)系统调用将事务写入磁盘。但是,如果Redis服务器崩溃或被系统管理员以某种强制的方式杀死,可能只有部分操作被注册。Redis将在重新启动时检测到这种情况,并会在出现错误时退出。使用redis-check-aof工具可以删除部分事务修复仅追加文件,以便服务器可以重新启动。
从版本2.2开始,Redis允许为上述两项提供额外保证,采用与check-and-set(CAS)操作非常相似的乐观锁定形式。这将在本页后面记录。
用法
使用MULTI命令进入Redis事务。该命令总是以OK回应。此时用户可以发出多个命令。Redis不会执行这些命令,而是将它们排队。EXEC被调用后,所有的命令都会被执行。
调用DISCARD而不是刷新事务队列并将退出事务。
以下示例以原子方式递增key foo和bar。
> MULTIOK> INCR fooQUEUED> INCR barQUEUED> EXEC1) (integer) 12) (integer) 1
EXEC返回一个响应数组,其中每个元素都是事务中单个命令的响应,顺序与命令的发出顺序相同。
当Redis连接处于MULTI请求的上下文中时,所有命令将以字符串QUEUED(从Redis协议的角度作为状态回复发送)进行回复。EXEC被调用时,排队的命令被简单地安排执行。
事务中的错误
事务过程中,可能会遇到两种命令错误:
命令可能无法排队,因此在调用EXEC之前可能会出现错误。例如,命令可能在语法上是错误的(参数数量错误,错误的命令名称…),或者可能存在某些关键条件,如内存不足的情况(如果服务器配置为使用maxmemory 指示)。
例如,因为我们针对具有错误值的key执行操作(例如,针对字符串值调用list操作),所以命令可能在调用EXEC后失败。
通过检查排队命令的返回值,客户端用于检测EXEC调用之前发生的第一种错误:如果命令使用QUEUED进行响应,则它已正确排队,否则Redis将返回错误。如果排队命令时发生错误,大多数客户端将中止该事务并放弃它。
然而,从Redis 2.6.5开始,服务器会记住在累积命令期间发生错误,并拒绝执行EXEC期间返回错误的事务,并自动丢弃该事务。
在Redis 2.6.5之前,行为只是在成功排队的命令子集内执行事务,以防客户端调用EXEC而不管以前的错误。新的行为使得将事务与流水线混合变得更加简单,因此整个事务可以一次发送,一次读取所有回复。
EXEC之后发生的错误不是以一种特殊的方式处理的:即使某些命令在事务中失败,也会执行所有其他命令。
这在协议层面更加清晰。 在以下示例中,即使语法正确,一个命令在执行时也会失败:
Trying 127.0.0.1…Connected to localhost.Escape character is ‘^]’.MULTI+OKSET a 3 abc+QUEUEDLPOP a+QUEUEDEXEC*2 +OK-ERR Operation against a key holding the wrong kind of value
EXEC返回two-element批量字符串回复,其中一个是OK代码,另一个是-ERR回复。客户端库需要找到一种明智的方式将错误提供给用户。
需要注意的是,即使命令失败,队列中的所有其他命令也会被处理–Redis不会停止命令的处理。
另一个例子,再次使用telnet协议使用Wire协议,可以显示语法错误是如何报告的:
MULTI+OKINCR a b c-ERR wrong number of arguments for ‘incr’ command
这次由于语法错误,错误的INCR命令根本没有排队。
为什么Redis不支持回滚?
如果您有关系数据库背景,Redis命令在事务处理期间可能会失败,但Redis将执行其余事务而不是回滚事务,这可能对您来说看起来很奇怪。
但是,对于这种行为有很好的意见:
如果使用错误的语法调用Redis命令(并且在命令排队期间无法检测到问题),或者针对保存错误数据类型的key,则Redis命令可能会失败:这意味着实际上,失败的命令是编程错误的结果, 以及在开发过程中很可能检测到的一种错误,而不是在生产中。
Redis内部简化且速度更快,因为它不需要回滚功能。
反对Redis观点的一个观点是错误发生了,但是应该指出的是一般情况下,回滚并不能避免编程错误。例如,如果查询将key增加2而不是1,或增加错误的key,则回滚机制无法提供帮助。鉴于没有人能够挽救程序员的错误,并且Redis命令失败所需的错误类型不太可能进入生产环境,所以我们选择了不支持错误回滚的更简单快捷的方法。
放弃命令队列
DISCARD可用于中止交易。在这种情况下,不执行任何命令并且连接状态恢复正常。
> SET foo 1 OK> MULTIOK> INCR fooQUEUED> DISCARDOK> GET foo“1”
乐观锁定使用check-and-set
WATCH用于为Redis事务提供check-and-set(CAS)行为。
监视key被监视以检测对它们的改变。如果在EXEC命令之前至少修改了一个监视的key,则整个事务中止,并且EXEC返回Null答复以通知事务失败。
例如,假设我们需要将key的值自动递增1(让我们假设Redis没有INCR)。
第一次尝试可能如下:
val = GET mykeyval = val + 1SET mykey $val
只有当我们有一个客户端在给定时间内执行操作时,这才能可靠地工作。如果多个客户端尝试在大约同一时间递增key,则会出现竞争状况。例如,客户端A和B将读取旧值,例如10,这两个客户端的值将递增为11,最后将SET作为key的值。所以最终的值将是11而不是12。
感谢WATCH,我们能够很好地模拟这个问题:
WATCH mykeyval = GET mykeyval = val + 1MULTISET mykey $valEXEC
使用上面的代码,如果存在竞争条件,并且另一个客户端在我们对WATCH的调用和我们对EXEC的调用之间的时间内修改了val的结果,则事务将失败。
我们只需要重复这次的操作,希望这次我们不会得到新的竞争。这种形式的锁定称为乐观锁定,是一种非常强大的锁定形式。在许多用例中,多个客户端将访问不同的key,因此碰撞不太可能发生 - 通常不需要重复该操作。
WATCH说明
那么WATCH真的是什么? 这是一个使EXEC有条件的命令:只有在没有任何WATCHed key被修改的情况下,我们才会要求Redis执行事务。 (但是它们可能会被事务中的同一个客户端改变而不会中止它,更多的是这个)否则,事务根本不会进入。(请注意,如果您WATCH易失性key并且Redis在您WATCH该key后过期了该key,那么EXEC将继续工作。更多的是这个。)
WATCH可以多次调用。简单地说,所有的WATCH调用都将具有WATCH从调用开始发生变化的效果,直到EXEC被调用。您也可以将任意数量的key发送到单个WATCH调用。
当调用EXEC时,无论事务是否中止,所有key都是UNWATCHed。此外,当客户端连接关闭时,所有都会被UNWATCHed。
也可以使用UNWATCH命令(无参数)来刷新所有watch的key。
有时候,我们乐观地锁定了几个key,这很有用,因为可能我们需要执行一个事务来改变这些key,但是在读完了key的当前内容之后我们不想继续。发生这种情况时,我们只需调用UNWATCH,以便连接可以自由用于新事务。
使用WATCH来实现ZPOP
举一个很好的例子来说明如何使用WATCH来创建新的原子操作,否则Redis不支持实现ZPOP,即以原子方式从排序集合中以较低分数弹出元素的命令。这是最简单的实现:
WATCH zsetelement = ZRANGE zset 0 0MULTIZREM zset elementEXEC
如果EXEC失败(即返回空回复),我们只需重复该操作。
Redis脚本和事务
根据定义,Redis脚本是事务性的,因此您可以使用Redis事务执行的所有操作都可以通过脚本完成,通常脚本将更简单快捷。
这种重复是由于在Redis 2.6中引入了脚本,而事务早已存在。然而,我们不可能在短时间内取消对事务的支持,因为即使不采用Redis脚本编写,仍然可以避免竞争状况,尤其是因为Redis事务的实施复杂性最低,这在语义上似乎是恰当的。
然而,在不远的将来,我们将看到整个用户群只是使用脚本,这并非不可能。如果发生这种情况,我们可能会弃用并最终删除事务。
官方API文档:http://www.redis.cn/topics/transactions.html