Redis学习笔记系列(九)——Redis事务

7. Redis事务

7.1 事务定义

Redis 通过MULTIDISCARDEXECWATCH四个命令来实现事务功能。Redis中的事务同命令一样都是Redis的最小执行单位,一个事务中的命令要么都执行,要么都不执行。事务的原理是先将属于一个事务的命令发送给Redis,然后再让Redis依次执行这些命令。下面,我们用一个示例来演示Redis事务。
假设A有10元钱,B有0元钱,A向B转5元钱,那么这个过程就需要用事务来实现,以保证A减5元钱和B增加5元钱要么都成功,要么都失败。

127.0.0.1:6379> SET A 10
OK
127.0.0.1:6379> SET B 0
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> INCRBY A -5
QUEUED
127.0.0.1:6379> INCRBY B 5
QUEUED
127.0.0.1:6379> EXEC
1) (integer) 5
2) (integer) 5
127.0.0.1:6379> GET A
"5"
127.0.0.1:6379> GET B
"5"

Redis事务可以保证以下两点:

  1. Redis保证一个事务中的所有命令要么都执行,要么都不执行。如果在发送EXEC命令前客户端断线了,则Redis会清空事务队列,事务中的所有命令都不会执行。而一旦客户端发送了EXEC命令,所有的命令就都会被执行,即使此后客户端断线也没关系,因为Redis中已经记录了所有要执行的命令。
  2. Redis的事务还能保证一个事务内的命令依次执行而不被其他命令插入。试想客户端A需要执行几条命令,同时客户端B发送了一条命令,如果不使用事务,则客户端B的命令可能会插入到客户端A的几条命令中执行。如果不希望发生这种情况,也可以使用事务。

7.2 事务的实现机制

一个事务从开始到执行会经历以下三个阶段:

  • 开始事务。
  • 命令入队。
  • 执行事务。
7.2.1 开始事务

MULTI命令的执行标记着事务的开始,该命令的唯一作用就是将客户端的REDIS_MULTI选项打开, 让客户端从非事务状态切换到事务状态。

127.0.0.1:6379> MULTI
OK
7.2.2 命令入队

当客户端处于非事务状态下时, 所有发送给服务器端的命令都会立即被服务器执行。但是,当客户端进入事务状态之后,服务器在收到来自客户端的命令时,不会立即执行命令,而是将这些命令全部放进一个事务队列里,然后返回 QUEUED ,表示命令已入队。

127.0.0.1:6379> INCRBY A -5
QUEUED
127.0.0.1:6379> INCRBY B 5
QUEUED
7.2.3 执行事务

前面说到,当客户端进入事务状态之后,客户端发送的命令就会被放进事务队列里。但其实并不是所有的命令都会被放进事务队列, 其中的例外就是 EXECDISCARDMULTIWATCH 这四个命令 —— 当这四个命令从客户端发送到服务器时, 它们会像客户端处于非事务状态一样, 直接被服务器执行。因此,如果客户端正处于事务状态, 那么当 EXEC 命令执行时, 服务器根据客户端所保存的事务队列, 以先进先出(FIFO)的方式执行事务队列中的命令: 最先入队的命令最先执行, 而最后入队的命令最后执行。

127.0.0.1:6379> EXEC
1) (integer) 5
2) (integer) 5

事务状态下,除了EXEC 命令会立刻执行外,DISCARDMULTIWATCH 也会立刻执行。
DISCARD 命令用于取消一个事务, 它清空客户端的整个事务队列, 然后将客户端从事务状态调整回非事务状态, 最后返回字符串 OK 给客户端, 说明事务已被取消。
Redis 的事务是不可嵌套的, 当客户端已经处于事务状态, 而客户端又再向服务器发送 MULTI 时, 服务器只是简单地向客户端发送一个错误, 然后继续等待其他命令的入队。 MULTI 命令的发送不会造成整个事务失败, 也不会修改事务队列中已有的数据。
WATCH只能在客户端进入事务状态之前执行, 在事务状态下发送 WATCH命令会引发一个错误, 但它不会造成整个事务失败, 也不会修改事务队列中已有的数据。

7.3 WATCH的作用

WATCH 命令用于在事务开始之前监视任意数量的键: 当调用 EXEC 命令执行事务时, 如果任意一个被监视的键已经被其他客户端修改了, 那么整个事务不再执行, 直接返回失败。示例如下:

# 监控k1
127.0.0.1:6379> WATCH k1
OK
# 开始事务
127.0.0.1:6379> MULTI
OK
# 设置k1的值为v2
127.0.0.1:6379> SET k1 v2
QUEUED
# 设置失败
127.0.0.1:6379> EXEC
(nil)
#  获取k1为v1,显然是别的客户端修改了k1的值
127.0.0.1:6379> GET k1
"v1"

失败原因:

时间 客户端A 客户端B
T1 WATCH k1
T2 MULTI
T3 SET k1 v2
T4 SET k1 v1
T5 EXEC

由于T4时刻,客户端B执行了SET k1 v1,当客户端 A 在 T5 执行 EXEC 时,Redis 会发现 k1 这个被监视的键已经被修改, 因此客户端 A 的事务不会被执行,而是直接返回失败。

7.4 错误处理

7.4.1 语法错误

语法错误指命令不存在或者命令参数的个数不对。跟在MULTI命令后执行了3个命令:一个是正确的命令,成功地加入事务队列;其余两个命令都有语法错误。而只要有一个命令有语法错误,执行EXEC命令后Redis就会直接返回错误,连语法正确的命令也不会执行。比如:

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET k1 v1
QUEUED
127.0.0.1:6379> GETT k1
(error) ERR unknown command `GETT`, with args beginning with: `k1`, 
127.0.0.1:6379> SET k2 v2
QUEUED
127.0.0.1:6379> EXEC
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> GET k1
(nil)
127.0.0.1:6379> GET k2
(nil)

Redis 2.6.5之前的版本会忽略有语法错误的命令,然后执行事务中其他语法正确的命令。就此例而言,SET k1 v1会被执行,EXEC命令会返回一个结果:1) OK。

7.4.2 运行错误

运行错误指在命令执行时出现的错误,比如使用散列类型的命令操作集合类型的键,这种错误在实际执行之前Redis是无法发现的,所以在事务里这样的命令是会被Redis接受并执行的。如果事务里的一条命令出现了运行错误,事务里其他的命令依然会继续执行(包括出错命令之后的命令),示例如下:

127.0.0.1:6379> SET k1 v1
QUEUED
127.0.0.1:6379> SADD k1 v2
QUEUED
127.0.0.1:6379> GET k1
QUEUED
127.0.0.1:6379> SADD k2 1 2 3
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
3) "v1"
4) (integer) 3

7.5 Redis事务 V.S. MySQL事务

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

7.5.1 原子性(Atomicity)

单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增加任何维持原子性的机制,所以 Redis 事务的执行并不是原子性的。
如果一个事务队列中的所有命令都被成功地执行,那么称这个事务执行成功。
另一方面,如果 Redis 服务器进程在执行事务的过程中被停止 —— 比如接到 KILL 信号、宿主机器停机,等等,那么事务执行失败。
当事务失败时,Redis 也不会进行任何的重试或者回滚动作。

7.5.2 一致性(Consistency)

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

  • 语法错误:当Redis事务中出现语法错误时,会直接取消事务的执行,因此肯定是一致的。
  • 运行错误:当Redis事务中出现运行错误时,错误的命令并不会影响正常命令的执行,因此是一致的。
  • Redis进行被终结:
    如果 Redis 服务器进程在执行事务的过程中被其他进程终结,或者被管理员强制杀死,那么根据 Redis 所使用的持久化模式,可能有以下情况出现:
    • 内存模式:如果 Redis 没有采取任何持久化机制,那么重启之后的数据库总是空白的,所以数据总是一致的。
    • RDB 模式:在执行事务时,Redis 不会中断事务去执行保存 RDB 的工作,只有在事务执行之后,保存 RDB 的工作才有可能开始。所以当 RDB 模式下的 Redis 服务器进程在事务中途被杀死时,事务内执行的命令,不管成功了多少,都不会被保存到 RDB 文件里。恢复数据库需要使用现有的 RDB 文件,而这个 RDB 文件的数据保存的是最近一次的数据库快照(snapshot),所以它的数据可能不是最新的,但只要 RDB 文件本身没有因为其他问题而出错,那么还原后的数据库就是一致的。
    • AOF 模式:因为保存 AOF 文件的工作在后台线程进行,所以即使是在事务执行的中途,保存 AOF 文件的工作也可以继续进行,因此,根据事务语句是否被写入并保存到 AOF 文件,有以下两种情况发生:
      1)如果事务语句未写入到 AOF 文件,或 AOF 未被 SYNC 调用保存到磁盘,那么当进程被杀死之后,Redis 可以根据最近一次成功保存到磁盘的 AOF 文件来还原数据库,只要 AOF 文件本身没有因为其他问题而出错,那么还原后的数据库总是一致的,但其中的数据不一定是最新的。
      2)如果事务的部分语句被写入到 AOF 文件,并且 AOF 文件被成功保存,那么不完整的事务执行信息就会遗留在 AOF 文件里,当重启 Redis 时,程序会检测到 AOF 文件并不完整,Redis 会退出,并报告错误。需要使用 redis-check-aof 工具将部分成功的事务命令移除之后,才能再次启动服务器。还原之后的数据总是一致的,而且数据也是最新的(直到事务执行之前为止)。
7.5.3 隔离性(Isolation)

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

7.5.4 持久性(Durability)

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

  • 在单纯的内存模式下,事务肯定是不持久的。
  • 在 RDB 模式下,服务器可能在事务执行之后、RDB 文件更新之前的这段时间失败,所以 RDB 模式下的 Redis 事务也是不持久的。
  • 在 AOF 的“总是 SYNC ”模式下,事务的每条命令在执行成功之后,都会立即调用 fsyncfdatasync 将事务数据写入到 AOF 文件。但是,这种保存是由后台线程进行的,主线程不会阻塞直到保存成功,所以从命令执行成功到数据保存到硬盘之间,还是有一段非常小的间隔,所以这种模式下的事务也是不持久的。
    其他 AOF 模式也和“总是 SYNC ”模式类似,所以它们都是不持久的。

写在最后

如果你觉得我写的文章帮到了你,欢迎点赞、评论、分享、赞赏哦,你们的鼓励是我不断创作的动力~

你可能感兴趣的:(Redis学习笔记系列(九)——Redis事务)