【Redis学习】Transaction事务管理

1、相关命令:

(1)MULTI

标记一个事务块的开始。事务块内的多条命令会按照先后顺序被放进一个队列当中,最后由 EXEC 命令原子性(atomic)地执行。

返回值:总是返回 OK 。

redis> MULTI            # 标记事务开始
OK

redis> INCR user_id     # 多条命令按顺序入队
QUEUED

redis> INCR user_id
QUEUED

redis> INCR user_id
QUEUED

redis> PING
QUEUED

redis> EXEC             # 执行
1) (integer) 1
2) (integer) 2
3) (integer) 3
4) PONG
(2)EXEC

执行所有事务块内的命令。假如某个(或某些) key 正处于 WATCH 命令的监视之下,且事务块中有和这个(或这些) key 相关的命令,那么 EXEC 命令只在这个(或这些) key 没有被其他命令所改动的情况下执行并生效,否则该事务被打断(abort)。

返回值:事务块内所有命令的返回值,按命令执行的先后顺序排列。当操作被打断时,返回空值 nil 。

(3)DISCARD

取消事务,放弃执行事务块内的所有命令。如果正在使用 WATCH 命令监视某个(或某些) key,那么取消所有监视,等同于执行命令 UNWATCH 。

返回值:总是返回 OK 。

redis> MULTI
OK

redis> PING
QUEUED

redis> SET greeting "hello"
QUEUED

redis> DISCARD
OK

(4)WATCH

WATCH key [key …]

监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。

返回值:总是返回 OK 。

redis> WATCH lock lock_times
OK
(5)UNWATCH

取消 WATCH 命令对所有 key 的监视。如果在执行 WATCH 命令之后, EXEC 命令或 DISCARD 命令先被执行了的话,那么就不需要再执行 UNWATCH 了。因为 EXEC 命令会执行事务,因此 WATCH 命令的效果已经产生了;而 DISCARD 命令在取消事务的同时也会取消所有对 key 的监视,因此这两个命令执行之后,就没有必要执行 UNWATCH 了。

返回值:总是 OK 。

redis> WATCH lock lock_times
OK

redis> UNWATCH
OK

使用实例

# 事务被成功执行

redis> MULTI
OK

redis> INCR user_id
QUEUED

redis> INCR user_id
QUEUED

redis> INCR user_id
QUEUED

redis> PING
QUEUED

redis> EXEC
1) (integer) 1
2) (integer) 2
3) (integer) 3
4) PONG


# 监视 key ,且事务成功执行

redis> WATCH lock lock_times
OK

redis> MULTI
OK

redis> SET lock "huangz"
QUEUED

redis> INCR lock_times
QUEUED

redis> EXEC
1) OK
2) (integer) 1


# 监视 key ,且事务被打断

redis> WATCH lock lock_times
OK

redis> MULTI
OK

redis> SET lock "joe"        # 就在这时,另一个客户端修改了 lock_times 的值
QUEUED

redis> INCR lock_times
QUEUED

redis> EXEC                  # 因为 lock_times 被修改, joe 的事务执行失败
(nil)

2、事务的执行过程

一个事务的执行过程分四步:开始事务→命令入队→命令执行→取消事务。

(1)开始事务

MULTI命令的执行标志着事务的开始。当客户端处于非事务状态下时, 所有发送给服务器端的命令都会立即被服务器执行。
Redis 的事务不可嵌套, 当客户端已经处于事务状态, 而客户端又再向服务器发送 MULTI 时, 服务器只是简单地向客户端发送一个错误, 然后继续等待其他命令的入队。MULTI 命令的发送不会造成整个事务失败, 也不会修改事务队列中已有的数据。

(2)命令入队

当客户端进入事务状态之后, 服务器在收到来自客户端的命令时,不会立即执行命令, 而是将这些命令全部放进一个事务队列里, 然后返回 QUEUED , 表示命令已入队:

事务队列是一个数组,每个数组项都包含三个属性:

a.要执行的命令(cmd)
b. 命令的参数(argv)
c. 参数的个数(argc)

(3)命令执行

如果客户端正处于事务状态, 那么当 EXEC 命令执行时, 服务器根据客户端所保存的事务队列, 以先进先出(FIFO)的方式执行事务队列中的命令:最先入队的命令最先执行, 而最后入队的命令最后执行。
当事务队列中的所有命令被执行完之后,EXEC命令会将回复队列作为自己的执行结果返回给客户端,客户端从事务状态返回到非事务状态。至此,事务执行完毕。

(4)取消事务

DISCARD 命令用于取消一个事务,它清空客户端的整个事务队列,然后将客户端从事务状态调整回非事务状态,最后返回字符串 OK 给客户端,说明事务已被取消。

事务命令与非事务命令:

无论在事务状态下, 还是在非事务状态下, Redis 命令都由同一个函数执行, 所以它们共享很多服务器的一般设置, 比如 AOF 的配置、RDB 的配置,以及内存限制,等等。
事务中的命令和普通命令在执行上还是有一点区别的,其中最重要的两点是:

(1)非事务状态下的命令以单个命令为单位执行,前一个命令和后一个命令的客户端不一定是同一个;而事务状态则是以一个事务为单位,执行事务队列中的所有命令,除非当前事务执行完毕,否则服务器不会中断事务,也不会执行其他客户端的其他命令。

(2)在非事务状态下,执行命令所得的结果会立即被返回给客户端;而事务则是将所有命令的结果集合到回复队列,再作为 EXEC 命令的结果返回给客户端。

3、事务内部的错误

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

(1) 一个命令可能会在被放入队列时失败。因此,事务有可能在调用EXEC命令之前就发生错误。

例如,这个命令可能会有语法错误(参数的数量错误、命令名称错误,等等),或者可能会有某些临界条件(例如:如果使用maxmemory指令,为Redis服务器配置内存限制,那么就可能会有内存溢出条件)。

(2)在调用EXEC命令之后,事务中的某个命令可能会执行失败。

例如,我们对某个键执行了错误类型的操作(例如,对一个字符串(String)类型的键执行列表(List)类型的操作)。

可以使用Redis客户端检测第一种类型的错误,在调用EXEC命令之前,这些客户端可以检查被放入队列的命令的返回值:如果命令的返回值是QUEUE字符串,那么就表示已经正确地将这个命令放入队列;否则,Redis将返回一个错误。如果将某个命令放入队列时发生错误,那么大多数客户端将会中止事务,并且丢弃这个事务。

在Redis 2.6.5版本之前,如果发生了上述的错误,那么在客户端调用了EXEC命令之后,Redis还是会运行这个出错的事务,执行已经成功放入事务队列的命令,而不会关心先前发生的错误。

然而,从Redis 2.6.5版本开始,服务器会记住事务积累命令期间发生的错误。然后,Redis会拒绝执行这个事务,在运行EXEC命令之后,便会返回一个错误消息。最后,Redis会自动丢弃这个事务。这样便能轻松地混合使用事务和管道。在这种情况下,客户端可以一次性地将整个事务发送至Redis服务器,稍后再一次性地读取所有的返回值。

相反,在调用EXEC命令之后发生的事务错误,Redis不会进行任何特殊处理:在事务运行期间,即使某个命令运行失败,所有其他的命令也将会继续执行

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

这里写图片描述

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

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

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

这里写图片描述

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

4、为什么Redis不支持回滚

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

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

只有当被调用的Redis命令有语法错误时,这条命令才会执行失败(在将这个命令放入事务队列期间,Redis能够发现此类问题),或者对某个键执行不符合其数据类型的操作:实际上,这就意味着只有程序错误才会导致Redis命令执行失败,这种错误很有可能在程序开发期间发现,一般很少在生产环境发现。

Redis已经在系统内部进行功能简化,这样可以确保更快的运行速度,因为Redis不需要事务回滚的能力。

对于Redis事务的这种行为,有一个普遍的反对观点,那就是程序有可能会有缺陷(bug)。但是,你应当注意到:事务回滚并不能解决任何程序错误

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

5、通过CAS实现操作乐观锁

在redis事务中,WATCH命令可用于提供CAS(check-nd-set)功能。WATCH命令可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行。假设我们通过WATCH命令在事务执行之前监控了一个key,之后如果这个key的值有任何的变化,后面EXEC命令执行的事务都将被放弃,同时返回null muti-bulk应答来通知调用者事务执行失败。

val = GET mykey
val = val + 1
SET mykey $val

以上实现只有在单连接的时候能保证结果正确。如果同一时刻有多个客户端同时执行该段代码就会出现竞态争用(race condition)的的场景。比如,客户端A和B都在同一时刻读取了mykey的原有值,假设该值为10,此后两个客户端又均将该值加一后set回Redis服务器,这样就会导致mykey的结果为11,而不是我们认为的12。

这时WATCH命令就派上用场了:如果我们能保证在GET获得键值后该键值不被其他客户端修改,直到事务执行完成后才允许其他客户端进行修改,这样可以有效的防止竞态条件。

WATCH mykey
val = GET mykey
val = val + 1
MULTI
SET mykey $val
EXEC

在获取mykey的值之前,先用WATCH命令对其进行了监控,之后将修改mykey的SET命令放在了事务里面,这样就可以有效的保证如果当前连接获取的mykey被其他连接修改的话,当前连接的EXEC命令将会执行失败。这样调用者根据返回值就能判断val是否被重置成功。

6、WATCH命令详解

WATCH命令使得EXEC命令在满足某些条件时才会运行事务:只有redis在所有受监控的键都没有被修改的时候才会执行事务,否则,redis根本不会进入事务。

WATCH命令可以被调用多次。简单的来说,所有的WATCH命令都会在被调用的时候立刻对自己监控的所有的键进行监控,直到EXEC命令被调用。

当调用EXEC命令时,所有的键都会变成未受监控的状态,redis不会管事务是否被终止。当一个客户端连接被关闭时,所有的键也都会变成未受监控的状态。

你还可以使用UNWATCH命令(不需要任何参数),这样便能清除所有的受监控键。当我们对某些键施加乐观锁之后,这个命令有时会非常有用。因为,我们可能需要运行一个用来修改这些键的事务,但是在读取这些键的当前内容之后,我们可能不打算继续进行操作,此时便可以使用UNWATCH命令,清除所有受监控的键。在运行UNWATCH命令之后,Redis连接便可以再次自由地用于运行新事务。

我们来看下面一个例子:

def hsetxx($key, $field, $value)

    WATCH $key

    $isFieldExists = HEXISTS $key, $field

    if $isFieldExists is 1

          MULTI

          HSET $key, $field, $value

          EXEC

    else

          UNWATCH

    return $isFieldExists

在代码中会判断要赋值的字段是否存在,如果字段不存在的话就不执行事务中的命令,但需要使用UNWATCH命令来保证下一个事务的执行不会受到影响

在每个代表数据库的Redis.h/RedisDb结构类型中,保存了一个 watched_keys 字典,字典的键是这个数据库被监视的键,而字典的值则是一个链表,链表中保存了所有监视这个键的客户端。

7、使用 WATCH 实现 ZPOP

WATCH 可以用于创建 Redis 没有内置的原子操作

举个例子, 以下代码实现了原创的 ZPOP 命令, 它可以原子地弹出有序集合中分值(score)最小的元素:

WATCH zset
element = ZRANGE zset 0 0
MULTI
    ZREM zset element
EXEC

程序只要重复执行这段代码, 直到 EXEC 的返回值不是空多条回复(null multi-bulk reply)即可。

8、ACID性质

在传统的关系式数据库中,常常用 ACID性质来检验事务功能的安全性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)
Redis 事务保证了其中的一致性和隔离性,但并不保证原子性和持久性。

8.1原子性(Atomicity)

单个 Redis 命令的执行是原子性的,但Redis 没有在事务上增加任何维持原子性的机制,所以 Redis 事务的执行并不是原子性的。
如果一个事务队列中的所有命令都被成功地执行,那么称这个事务执行成功。

另一方面,如果 Redis 服务器进程在执行事务的过程中被停止 —— 比如接到 KILL 信号、宿主机器停机,等等,那么事务执行失败。
当事务失败时,Redis 也不会进行任何的重试或者回滚动作。

8.2一致性(Consistency)

Redis 的一致性问题可以分为三部分来讨论:入队错误、执行错误、Redis 进程被终结

(1)入队错误

在命令入队的过程中,如果客户端向服务器发送了错误的命令,比如命令的参数数量不对,等等,服务器将向客户端返回一个出错信息,并且将客户端的事务状态设为REDIS_DIRTY_EXEC 。

当客户端执行 EXEC 命令时,Redis 会拒绝执行状态为 REDIS_DIRTY_EXEC 的事务,并返回失败信息。

因此,带有不正确入队命令的事务不会被执行,也不会影响数据库的一致性。

(2)执行错误

如果命令在事务执行的过程中发生错误,比如说对一个不同类型的 key 执行了错误的操作,那么 Redis 只会将错误包含在事务的结果中,这不会引起事务中断或整个失败,不会影响已执行事务命令的结果,也不会影响后面要执行的事务命令,所以它对事务的一致性也没有影响。

(3)Redis 进程被终结

如果 Redis 服务器进程在执行事务的过程中被其他进程终结,或者被管理员强制杀死,那么根据Redis 所使用的持久化模式,可能有以下情况出现:

内存模式:如果 Redis 没有采取任何持久化机制,那么重启之后的数据库总是空白的,所以数据总是一致的。

RDB 模式:在执行事务时,Redis 不会中断事务去执行保存 RDB 的工作,只有在事务执行之后,保存 RDB 的工作才有可能开始。所以当 RDB 模式下的 Redis 服务器进程在事务中途被杀死时,事务内执行的命令,不管成功了多少,都不会被保存到 RDB 文件里。恢复数据库需要使用现有的 RDB 文件,而这个 RDB 文件的数据保存的是最近一次的数据库快照,所以它的数据可能不是最新的,但只要RDB 文件本身没有因为其他问题而出错,那么还原后的数据库就是一致的。

AOF 模式:因为保存 AOF 文件的工作在后台线程进行,所以即使是在事务执行的中途,保存 AOF 文件的工作也可以继续进行,因此,根据事务语句是否被写入并保存到 AOF 文件,有以下两种情况发生:

1)如果事务语句未写入到 AOF 文件,或 AOF 未被SYNC 调用保存到磁盘,那么当进程被杀死之后,Redis 可以根据最近一次成功保存到磁盘的 AOF 文件来还原数据库,只要 AOF 文件本身没有因为其他问题而出错,那么还原后的数据库总是一致的,但其中的数据不一定是最新的。

2)如果事务的部分语句被写入到 AOF 文件,并且 AOF 文件被成功保存,那么不完整的事务执行信息就会遗留在 AOF 文件里,当重启 Redis 时,程序会检测到 AOF 文件并不完整,Redis 会退出,并报告错误。需要使用 Redis-check-aof 工具将部分成功的事务命令移除之后,才能再次启动服务器。还原之后的数据总是一致的,而且数据也是最新的(直到事务执行之前为止)。

8.3隔离性(Isolation)

Redis 是单进程程序,并且它保证在执行事务时,不会对事务进行中断,事务可以运行直到执行完所有事务队列中的命令为止。因此,Redis 的事务是总是带有隔离性的。

8.4持久性(Durability)

因为事务不过是用队列包裹起了一组 Redis 命令,并没有提供任何额外的持久性功能,所以事务的持久性由 Redis 所使用的持久化模式决定:
在单纯的内存模式下,事务肯定是不持久的。

在 RDB 模式下,服务器可能在事务执行之后、RDB 文件更新之前的这段时间失败,所以 RDB 模式下的 Redis 事务也是不持久的。

在 AOF 的“总是 SYNC ”模式下,事务的每条命令在执行成功之后,都会立即调用 fsync 或 fdatasync 将事务数据写入到 AOF 文件。但是,这种保存是由后台线程进行的,主线程不会阻塞直到保存成功,所以从命令执行成功到数据保存到硬盘之间,还是有一段非常小的间隔,所以这种模式下的事务也是不持久的。

其他 AOF 模式也和“总是 SYNC ”模式类似,所以它们都是不持久的。

Redis 脚本和事务
从定义上来说, Redis 中的脚本本身就是一种事务, 所以任何在事务里可以完成的事, 在脚本里面也能完成。 并且一般来说, 使用脚本要来得更简单,并且速度更快。

因为脚本功能是 Redis 2.6 才引入的, 而事务功能则更早之前就存在了, 所以 Redis 才会同时存在两种处理事务的方法。

不过我们并不打算在短时间内就移除事务功能, 因为事务提供了一种即使不使用脚本, 也可以避免竞争条件的方法, 而且事务本身的实现并不复杂。

不过在不远的将来, 可能所有用户都会只使用脚本来实现事务也说不定。 如果真的发生这种情况的话, 那么我们将废弃并最终移除事务功能。

9、Redis 脚本和事务

从定义上来说, Redis 中的脚本本身就是一种事务, 所以任何在事务里可以完成的事, 在脚本里面也能完成。 并且一般来说, 使用脚本要来得更简单,并且速度更快。

因为脚本功能是 Redis 2.6 才引入的, 而事务功能则更早之前就存在了, 所以 Redis 才会同时存在两种处理事务的方法。

不过我们并不打算在短时间内就移除事务功能, 因为事务提供了一种即使不使用脚本, 也可以避免竞争条件的方法, 而且事务本身的实现并不复杂。

不过在不远的将来, 可能所有用户都会只使用脚本来实现事务也说不定。 如果真的发生这种情况的话, 那么我们将废弃并最终移除事务功能。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值