zoukankan      html  css  js  c++  java
  • 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的乐观锁进行描述。

    相关命令

    MULTI

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

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

    MULTI

    这个命令的返回值是一个简单的字符串,总是OK。

    EXEC

    在一个事务中执行所有先前放入队列的命令,然后恢复正常的连接状态。

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

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

    EXEC

    这个命令的返回值是一个数组,其中的每个元素分别是原子化事务中的每个命令的返回值。 当使用WATCH命令时,如果事务执行中止,那么EXEC命令就会返回一个Null值。

    DISCARD

    清除所有先前在一个事务中放入队列的命令,然后恢复正常的连接状态。

    如果使用了WATCH命令,那么DISCARD命令就会将当前连接监控的所有键取消监控。

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

    DISCARD

    这个命令的返回值是一个简单的字符串,总是OK。

    WATCH

    当某个事务需要按条件执行时,就要使用这个命令将给定的键设置为受监控的。

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

    WATCH key [key ...]

    这个命令的返回值是一个简单的字符串,总是OK。

    对于每个键来说,时间复杂度总是O(1)。

    UNWATCH

    清除所有先前为一个事务监控的键。

    如果你调用了EXEC或DISCARD命令,那么就不需要手动调用UNWATCH命令。

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

    UNWATCH

    这个命令的返回值是一个简单的字符串,总是OK。

    时间复杂度总是O(1)。

    使用方法

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

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

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

    01

    正如从上面的会话所看到的一样,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不会进行任何特殊处理:在事务运行期间,即使某个命令运行失败,所有其他的命令也将会继续执行。

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

    02

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

    需要注意的是,即使某个命令执行失败,事务队列中的所有其他命令仍然会执行 —— Redis不会停止执行事务中的命令。

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

    03

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

    为什么Redis不支持回滚?

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

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

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

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

    丢弃命令队列

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

    04

    通过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值),那么我们只需要重复运行这个操作即可。

  • 相关阅读:
    Hihocoder 1275 扫地机器人 计算几何
    CodeForces 771C Bear and Tree Jumps 树形DP
    CodeForces 778D Parquet Re-laying 构造
    CodeForces 785E Anton and Permutation 分块
    CodeForces 785D Anton and School
    CodeForces 785C Anton and Fairy Tale 二分
    Hexo Next 接入 google AdSense 广告
    如何统计 Hexo 网站的访问地区和IP
    Design and Implementation of Global Path Planning System for Unmanned Surface Vehicle among Multiple Task Points
    通过ODBC接口访问人大金仓数据库
  • 原文地址:https://www.cnblogs.com/amunote/p/10463415.html
Copyright © 2011-2022 走看看