Redis的事务

134 阅读8分钟

事务

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

事务执行的过程

事务首先以一个MULTI命令为开始,接着将多个命令放入事务当中,最后由EXEC命令将这个事务提交给服务器执行

redis> MULTI  
OK  
redis> SET "name" "Practical Common Lisp"  
QUEUED  
redis> GET "name"  
QUEUED  
redis> SET "author" "Peter Seibel"  
QUEUED  
redis> GET "author"  
QUEUED
redis> EXEC  
1OK  
2"Practical Common Lisp"  
3OK  
4"Peter Seibel"

事务的三个阶段

事务开始

  • MULTI命令的执行标志着事务的开始
  • MULTI命令可以将执行该命令的客户端从非事务状态切换至事务状态

命令入队

  • 当一个客户端处于非事务状态时,这个客户端发送的命令会立即被服务器执行
  • 与此不同的是,当一个客户端切换到事务状态之后,服务器会根据这个客户端发来的不同命令执行不同的操作

image.png

事务队列

事务队列以先进先出(FIFO)的方式保存入队的命令,较先入队的命令会被放到数组的前面,而较后入队的命令则会被放到数组的后面。

例如:

redis> MULTI  
OK  
redis> SET "name" "Practical Common Lisp"  
QUEUED  
redis> GET "name"  
QUEUED  
redis> SET "author" "Peter Seibel"  
QUEUED  
redis> GET "author"  
QUEUED
  • 最先入队的SET命令被放在了事务队列的索引0位置上。
  • 第二入队的GET命令被放在了事务队列的索引1位置上。
  • 第三入队的另一个SET命令被放在了事务队列的索引2位置上。
  • 最后入队的另一个GET命令被放在了事务队列的索引3位置上。

执行事务

当一个处于事务状态的客户端向服务器发送EXEC命令时,这个EXEC命令将立即被服务器执行。服务器会遍历这个客户端的事务队列,执行队列中保存的所有命令,最后将执行命令所得的结果全部返回给客户端。

例如上面事务返回的结果:

redis> EXEC  
1OK
2"Practical Common Lisp"  
3OK  
4"Peter Seibel"

WATCH命令的实现

watch命令是一个乐观锁,它可以在exec命令执行之前监视多个键,并且在exec命令执行时,检查被监视的键,是否有被修改过,是的话,服务器就会拒绝执行事务。

例如:

redis> WATCH "name"  
OK  
redis> MULTI  
OK  
redis> SET "name" "peter"  
QUEUED  
redis> EXEC  
(nil)

image.png

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

使用WATCH命令监视数据库键

每个Redis数据库都保存着一个watched_keys字典,字典的值是一个链表,通过watched_keys字典,服务器可以清楚地知道哪些数据库键正在被监视,以及哪些客户端正在监视这些数据库键。

当客户端c10086执行以下命令后,字典中的数据如图所示

redis> WATCH "name" "age"  
OK

image.png

监视机制的触发

所有对数据库进行修改的命令,比如SET、LPUSH、SADD、ZREM、DEL、FLUSHDB等等,在执行之后都会对字典进行检查。如果有命令被修改过了,就会打开,被修改键的标识REDIS_DIRTY_CAS,表示事务安全性已经被破坏。

判断事务是否安全

当服务器接收到一个客户端发来的EXEC命令时,服务器会根据这个客户端是否打开了REDIS_DIRTY_CAS标识来决定是否执行事务。

事务的ACID性质

在Redis中,事务总是具有原子性、一致性和隔离性,并且当redis开启了持久化机制之后,也会具有持久性

原子性

如果是提交事务期间(入队阶段)命令就报错了(明显的语法错误,导致事务无法正常提交),那么会因为入队错误导致事务中的所有命令都不被执行 比如:

redis> MULTI  
OK  
redis> SET msg "hello"  
QUEUED  
redis> GET  
(error) ERR wrong number of arguments for 'get' command  
redis> GET msg  
QUEUED  
redis> EXEC
(error) EXECABORT Transaction discarded because of previous errors.

但是,如果是在提交事务后,事务执行期间才报错,那么事务中的其他命令还是会正常执行,这就是和传统的关系型数据库的不同,不支持回滚

redis> SET msg "hello"    #msg键是一个字符串  
OK   
redis> MULTI  
OK  
redis> SADD fruit "apple" "banana" "cherry"  
QUEUED  
redis> RPUSH msg "good bye" "bye bye" #错误地对字符串键msg执行列表键的命令
QUEUED   
redis> SADD alphabet "a" "b" "c"  
QUEUED  
redis> EXEC  
1) (integer) 3  
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value  
3) (integer) 3

Redis的作者在事务功能的文档中解释说,不支持事务回滚是因为这种复杂的功能和Redis追求简单高效的设计主旨不相符,并且他认为,Redis事务的执行时错误通常都是编程错误产生的,这种错误通常只会出现在开发环境中,而很少会在实际的生产环境中出现,所以他认为没有必要为Redis开发事务回滚功能。

一致性

入队错误

  • 如果一个事务在入队命令的过程中,出现了命令不存在,或者命令的格式不正确等情况,那么Redis将拒绝执行这个事务。
  • 因为服务器会拒绝执行入队过程中出现错误的事务,所以Redis事务的一致性不会被带有入队错误的事务影响。

根据文档记录,在Redis 2.6.5以前的版本,即使有命令在入队过程中发生了错误,事务一样可以执行,不过被执行的命令只包括那些正确入队的命令。

执行错误

上面举的例子就是执行错误,在事务提交阶段,也就是入队阶段不能被发现,只有在事务提交执行后才能被发现。

服务器停机

如果Redis服务器在执行事务的过程中停机,那么根据服务器所使用的持久化模式,可能有以下情况出现:

  1. 如果redis没有开启持久化机制,那么重启后数据本来也就是空的,所以也就不存在一致性问题
  2. 如果是用的RDB文件恢复的数据,那么事务执行时的数据也会被记录到RDB文件中,一样也可以通过rdb文件恢复
  3. AOF也一样,也会将事务执行的数据记录在案 所以无论怎么样都能保证一执性。

隔离性

redis的文件事件分派器本来就是单线程的,所以天然的保证了隔离性。

持久性

Redis并没有为事务提供任何额外的持久化功能,所以Redis事务的耐久性由Redis所使用的持久化模式决定

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

不论Redis在什么模式下运作,在一个事务的最后加上SAVE命令总可以保证事务的耐久性,不过因为这种做法的效率太低,所以并不具有实用性

redis> MULTI  
OK  
redis> SET msg "hello"  
QUEUED  
redis> SAVE
QUEUED  
redis> EXEC  
1OK  
2OK