7. Redis事务
7.1 事务定义
Redis 通过MULTI
、 DISCARD
、EXEC
和 WATCH
四个命令来实现事务功能。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事务可以保证以下两点:
- Redis保证一个事务中的所有命令要么都执行,要么都不执行。如果在发送EXEC命令前客户端断线了,则Redis会清空事务队列,事务中的所有命令都不会执行。而一旦客户端发送了EXEC命令,所有的命令就都会被执行,即使此后客户端断线也没关系,因为Redis中已经记录了所有要执行的命令。
- 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 执行事务
前面说到,当客户端进入事务状态之后,客户端发送的命令就会被放进事务队列里。但其实并不是所有的命令都会被放进事务队列, 其中的例外就是 EXEC
、 DISCARD
、MULTI
和 WATCH
这四个命令 —— 当这四个命令从客户端发送到服务器时, 它们会像客户端处于非事务状态一样, 直接被服务器执行。因此,如果客户端正处于事务状态, 那么当 EXEC
命令执行时, 服务器根据客户端所保存的事务队列, 以先进先出(FIFO
)的方式执行事务队列中的命令: 最先入队的命令最先执行, 而最后入队的命令最后执行。
127.0.0.1:6379> EXEC
1) (integer) 5
2) (integer) 5
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)
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 ”模式下,事务的每条命令在执行成功之后,都会立即调用
fsync
或fdatasync
将事务数据写入到 AOF 文件。但是,这种保存是由后台线程进行的,主线程不会阻塞直到保存成功,所以从命令执行成功到数据保存到硬盘之间,还是有一段非常小的间隔,所以这种模式下的事务也是不持久的。
其他 AOF 模式也和“总是 SYNC ”模式类似,所以它们都是不持久的。
写在最后
如果你觉得我写的文章帮到了你,欢迎点赞、评论、分享、赞赏哦,你们的鼓励是我不断创作的动力~