Redis的事务功能详解

Redis的事务功能详解


MULTI、EXEC、DISCARD和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的乐观锁进行描述。redis

1、相关命令

1. MULTI

用于标记事务块的开始。Redis会将后续的命令逐个放入队列中,而后才能使用EXEC命令原子化地执行这个命令序列。数据库

这个命令的运行格式以下所示:数组

MULTI

这个命令的返回值是一个简单的字符串,老是OK。服务器

2. EXEC

在一个事务中执行全部先前放入队列的命令,而后恢复正常的链接状态。markdown

当使用WATCH命令时,只有当受监控的键没有被修改时,EXEC命令才会执行事务中的命令,这种方式利用了检查再设置(CAS)的机制。工具

这个命令的运行格式以下所示:code

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)。

2、使用方法

使用MULTI命令即可以进入一个Redis事务。这个命令的返回值老是OK。此时,用户能够发出多个Redis命令。Redis会将这些命令放入队列,而不是执行这些命令。一旦调用EXEC命令,那么Redis就会执行事务中的全部命令。

相反,调用DISCARD命令将会清除事务队列,而后退出事务。

如下示例会原子化地递增foo键和bar键的值:

<img>

正如从上面的会话所看到的同样,EXEC命令的返回值是一个数组,其中的每一个元素都分别是事务中的每一个命令的返回值,返回值的顺序和命令的发出顺序是相同的。

当一个Redis链接正处于MULTI请求的上下文中时,经过这个链接发出的全部命令的返回值都是QUEUE字符串(从Redis协议的角度来看,返回值是做为状态回复(Status Reply)来发送的)。当调用EXEC命令时,Redis会简单地调度执行事务队列中的命令。

3、事务内部的错误

在一个事务的运行期间,可能会遇到两种类型的命令错误:

一个命令可能会在被放入队列时失败。所以,事务有可能在调用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不会进行任何特殊处理:在事务运行期间,即便某个命令运行失败,全部其余的命令也将会继续执行。

这种行为在协议层面上更加清晰。在如下示例中,当事务正在运行时,有一条命令将会执行失败,即便这条命令的语法是正确的:

<img>

上述示例的EXEC命令的返回值是批量的字符串,包含两个元素,一个是OK代码,另外一个是-ERR错误消息。客户端会根据自身的程序库,选择一种合适的方式,将错误信息提供给用户

须要注意的是,即便某个命令执行失败,事务队列中的全部其余命令仍然会执行 —— Redis不会中止执行事务中的命令。

再看另外一个示例,再次使用telnet通讯协议,观察命令的语法错误是如何尽快报告给用户的:

<img>

这一次,因为INCR命令的语法错误,Redis根本就没有将这个命令放入事务队列。

4、为何Redis不支持回滚?

若是你具有关系型数据库的知识背景,你就会发现一个事实:在事务运行期间,虽然Redis命令可能会执行失败,可是Redis仍然会执行事务中余下的其余命令,而不会执行回滚操做,你可能会以为这种行为很奇怪。

然而,这种行为也有其合理之处:

只有当被调用的Redis命令有语法错误时,这条命令才会执行失败(在将这个命令放入事务队列期间,Redis可以发现此类问题),或者对某个键执行不符合其数据类型的操做:实际上,这就意味着只有程序错误才会致使Redis命令执行失败,这种错误颇有可能在程序开发期间发现,通常不多在生产环境发现。
Redis已经在系统内部进行功能简化,这样能够确保更快的运行速度,由于Redis不须要事务回滚的能力。

对于Redis事务的这种行为,有一个广泛的反对观点,那就是程序有可能会有缺陷(bug)。可是,你应当注意到:事务回滚并不能解决任何程序错误。例如,若是某个查询会将一个键的值递增2,而不是1,或者递增错误的键,那么事务回滚机制是没有办法解决这些程序问题的。请注意,没有人能解决程序员本身的错误,这种错误可能会致使Redis命令执行失败。正由于这些程序错误不大可能会进入生产环境,因此咱们在开发Redis时选用更加简单和快速的方法,没有实现错误回滚的功能。

5、丢弃命令队列

DISCARD命令能够用来停止事务运行。在这种状况下,不会执行事务中的任何命令,而且会将Redis链接恢复为正常状态。示例以下所示:

<img>

6、经过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变量的结果,那么事务将会运行失败。

咱们只须要重复执行上述伪码的操做,但愿这次运行不会再出现竞争状态。这种形式的锁就被称为乐观锁,它是一种很是强大的锁。在许多用例中,多个客户端可能会访问不一样的键,所以不太可能发生冲突 —— 也就是说,一般没有必要重复执行上述伪码的操做。

7、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值),那么咱们只须要重复运行这个操做便可。

8、Redis脚本和事务

根据定义,Redis脚本也是事务型的。所以,你能够经过Redis事务实现的功能,一样也能够经过Redis脚原本实现,并且一般脚本更简单、更快速。

因为Redis从2.6版本才开始引入脚本特性,而事务特性是好久之前就已经存在的,因此目前的版本才有两个看起来重复的特性。可是,咱们不太可能在短期内移除对事务特性的支持。由于,即便不用求助于Redis脚本,用户仍然可以规避竞争状态,这从语义上来看是适宜的。还有另外一个更重要的缘由,Redis事务特性的实现复杂度是最小的。

可是,在至关长的一段时间以内,咱们不大可能看到整个用户群体都只使用Redis脚本。若是发生这种状况,那么咱们可能会废弃,甚至最终移除Redis事务。

相关文章
相关标签/搜索