理解 Redis 事务

314 阅读6分钟

Redis 通过 MULTI EXEC WATCH 等命令来实现事务功能。事务提供了一种将多个命令打包,然后一次性、按顺序地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而改去执行其他客户端的命令请求,它会将事务中的所有命令都执行完毕,然后才去处理其他客户端的命令请求。

如何使用事务

以下是一个事务的执行过程,该事务首先以一个 MULTI 命令为开始,接着将多个命令放到事务当中,最后由 EXEC 命令将这个事务提交给服务器执行:

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET "name" "Practical Common Lisp"
QUEUED
127.0.0.1:6379> GET "name"
QUEUED
127.0.0.1:6379> SET "author" "Peter Seibel"
QUEUED
127.0.0.1:6379> Get "author"
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) "Practical Common Lisp"
3) OK
4) "Peter Seibel"

事务是如何实现的

MULTI 命令标志着一个事务的开启,当开启一个事务后,服务器会根据接收到的不同的命令做出不同的操作:

  • 如果服务端收到的是 EXEC DISCARD WATCH MULTI 命令,那么服务端会立即执行这些命令
  • 如果服务端收到的不是以上四个命令,那么服务端不会立马执行命令,而是将这些命令放入一个命令队列中,然后向客户端返回 "QUEUED"

当一个处于事务上下文的客户端向服务器发送 EXEC 命令时,这个 EXEC 命令将立即被服务器执行,服务器会遍历客户端的事务队列,执行队列中保存的所有命令,最后将全部的执行结果一次返回给客户端。反之,当发送的是 DISCARD 命令时,服务器会立马丢弃队列中的所有命令,并且退出事务上下文状态。

Redis 的事务与关系型数据库的事务的不同点在于,Redis 只有执行了 EXEC 命令才会依次的去执行队列中的命令,而关系型数据库即便没有提交事务也会进行写的操作。以 MySQL 为例,如果将隔离级别设置为 Read Uncommitted 那么在 A 事务中就可以看到 B 事务未提交的修改。

Redis 事务是否满足 ACID

在 Redis 中,事务总是具备 原子性 一致性 隔离性 的,至于是否具备 持久性 需要根据 Redis 选择的持久化模式来判断。下面的命令将逐一展示 Redis 对 ACID 的支持。

Redis 事务的原子性

事务具有原子性指的是,数据库将事务事务中的多个操作当做一个整体来执行,服务器要么就执行事务中的所有操作,要么就一个操作都不执行。

# 事务中没有发生异常,所有的命令都得到执行
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET MSG "HELLO"
QUEUED
127.0.0.1:6379> GET MSG
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) "HELLO"
# 因为错误的使用了 GET 命令,事务中发生了异常,所有的命令都没有执行
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET MSG "HELLO"
QUEUED
127.0.0.1:6379> GET
(error) ERR wrong number of arguments for 'get' command
127.0.0.1:6379> EXEC
(error) EXECABORT Transaction discarded because of previous errors.

Redis 事务的一致性

事务具有一致性指的是,如果数据库在执行事务前是一致的,那么在事务执行之后,无论事务是否执行成功,数据库也应该是一致的。

127.0.0.1:6379> SET MSG "HELLO"
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> GET MSG
QUEUED
127.0.0.1:6379> RPUSH MSG "WORLD"
QUEUED
127.0.0.1:6379> GET MSG
QUEUED
127.0.0.1:6379> EXEC
1) "HELLO"
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
3) "HELLO"

我们先将 MSG 设置成一个字符串键,然后在事务上下文中尝试对 MSG 执行只能用于列表键的 RPUSH 命令,这将会在事务上下文中引发一个错误。错误的命令操作会被服务器识别出来,而未出错的命令仍然会继续执行。出错的命令不会对数据库做出任何修改,也不会对数据库的一致性产生任何影响。

Redis 事务的隔离性

事务的隔离性指的是,即使数据库中有多个事务并发地执行,各个事务之间也不会互相影响,并且在并发状态下执行的事务和串行执行的事务产生的结果完全相同。因为 Redis 使用单线程的方式来执行事务(以及事务队列中的命令),并且服务器保证,在执行事务期间不会对事务进行中断,因此,Redis 的事务总是以串行的方式运行的,并且事务也总是具有隔离性的。

Redis 事务的持久性

事务的耐久性指的是,当一个事务执行完毕时,执行这个事务所得的结果已经被保存到永久性存储介质(比如硬盘)里面了,即使服务器在事务执行完毕之后停机,执行事务所得的结果也不会丢失。因为 Redis 的事务不过是简单地用队列包裹起了一组 Redis 命令,Redis 并没有为事务提供任何额外的持久化功能,所以 Redis 事务的耐久性由 Redis 所使用的持久化模式决定:

  • 当服务器在无持久化的内存模式下运作时,事务不具有耐久性,一旦服务器停机,包括事务数据在内的所有服务器数据都将丢失
  • 当服务器在 RDB 持久化模式下运作时,服务器只会在特定的保存条件被满足时,才会执行 BGSAVE 命令,对数据库进行保存操作,并且异步执行的 BGSAVE 不能保证事务数据被第一时间保存到硬盘里面,因此 RDB 持久化模式下的事务也不具有耐久性
  • 当服务器运行在 AOF 持久化模式下,并且 appendfsync 选项的值为 always 时,程序总会在执行命令之后调用同步(sync)函数,将命令数据真正地保存到硬盘里面,因此这种配置下的事务是具有耐久性的
  • 当服务器运行在 AOF 持久化模式下,并且 appendfsync 选项的值为 everysec 时,程序会每秒同步一次命令数据到硬盘。因为停机可能会恰好发生在等待同步的那一秒钟之内,这可能会造成事务数据丢失,所以这种配置下的事务不具有耐久性
  • 当服务器运行在 AOF 持久化模式下,并且 appendfsync 选项的值为 no 时,程序会交由操作系统来决定何时将命令数据同步到硬盘。因为事务数据可能在等待同步的过程中丢失,所以这种配置下的事务不具有耐久性

WATCH 是如何实现的

WATCH 命令是一个 乐观锁(optimistic locking),它可以在 EXEC 命令执行之前,监视任意数量的数据库键,并在 EXEC 命令执行时,检查被监视的键是否至少有一个已经被修改过了,如果是的话,服务器将拒绝执行事务,并向客户端返回代表事务执行失败的空回复。下面的表格展示了两个客户端执行的过程

时间 客户端 A 客户端 B
T1 WATCH MSG
T2 MULTI
T3 SET MSG "HELLO"
T4 SET MSG "WORD"
T5 EXEC

在时间 T4,客户端 B 修改了 "MSG" 键的值,当客户端 A 在 T5 执行 EXEC 命令时,服务器会发现 WATCH 监视的键 "MSG" 已经被修改,因此服务器拒绝执行客户端 A 的事务,并向客户端 A 返回空回复。