19.3 事务的ACID性质
在传统的关系式数据库中,常常用ACID性质来检验事务功能的可靠性和安全性。
在Redis中,事务总是具有原子性(Atomicity)、一致性(Consistency)和隔离性(Isolation),并且当Redis运行在某种特定的持久化模式下时,事务也具有耐久性(Durability)。
以下四个小节将分别对这四个性质进行讨论。
19.3.1 原子性
事务具有原子性指的是,数据库将事务中的多个操作当作一个整体来执行,服务器要么就执行事务中的所有操作,要么就一个操作也不执行。
对于Redis的事务功能来说,事务队列中的命令要么就全部都执行,要么就一个都不执行,因此,Redis的事务是具有原子性的。
举个例子,以下展示的是一个成功执行的事务,事务中的所有命令都会被执行:
redis> MULTI
OK
redis> SET msg "hello"
QUEUED
redis> GET msg
QUEUED
redis> EXEC
1) OK
2) "hello"
与此相反,以下展示了一个执行失败的事务,这个事务因为命令入队出错而被服务器拒绝执行,事务中的所有命令都不会被执行:
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的事务和传统的关系型数据库事务的最大区别在于,Redis不支持事务回滚机制(rollback),即使事务队列中的某个命令在执行期间出现了错误,整个事务也会继续执行下去,直到将事务队列中的所有命令都执行完毕为止。
在下面的这个例子中,即使RPUSH命令在执行期间出现了错误,事务的后续命令也会继续执行下去,并且之前执行的命令也不会有任何影响:
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开发事务回滚功能。
19.3.2 一致性
事务具有一致性指的是,如果数据库在执行事务之前是一致的,那么在事务执行之后,无论事务是否执行成功,数据库也应该仍然是一致的。
“一致”指的是数据符合数据库本身的定义和要求,没有包含非法或者无效的错误数据。
Redis通过谨慎的错误检测和简单的设计来保证事务的一致性,以下三个小节将分别介绍三个Redis事务可能出错的地方,并说明Redis是如何妥善地处理这些错误,从而确保事务的一致性的。
1.入队错误
如果一个事务在入队命令的过程中,出现了命令不存在,或者命令的格式不正确等情况,那么Redis将拒绝执行这个事务。
在以下展示的示例中,因为客户端尝试向事务入队一个不存在的命令YAHOOOO,所以客户端提交的事务会被服务器拒绝执行:
redis> MULTI
OK
redis> SET msg "hello"
QUEUED
redis> YAHOOOO
(error) ERR unknown command 'YAHOOOO'
redis> GET msg
QUEUED
redis> EXEC
(error) EXECABORT Transaction discarded because of previous errors.
因为服务器会拒绝执行入队过程中出现错误的事务,所以Redis事务的一致性不会被带有入队错误的事务影响。
Redis 2.6.5以前的入队错误处理
根据文档记录,在Redis 2.6.5以前的版本,即使有命令在入队过程中发生了错误,事务一样可以执行,不过被执行的命令只包括那些正确入队的命令。以下这段代码是在Redis 2.6.4版本上测试的,可以看到,事务可以正常执行,但只有成功入队的SET命令和GET命令被执行了,而错误的YAHOOOO则被忽略了:
redis> MULTI
OK
redis> SET msg "hello"
QUEUED
redis> YAHOOOO
(error) ERR unknown command 'YAHOOOO'
redis> GET msg
QUEUED
redis> EXEC
1) OK
2) "hello"
因为错误的命令不会被入队,所以Redis不会尝试去执行错误的命令,因此,即使在2.6.5以前的版本中,Redis事务的一致性也不会被入队错误影响。
2.执行错误
除了入队时可能发生错误以外,事务还可能在执行的过程中发生错误。
关于这种错误有两个需要说明的地方:
·执行过程中发生的错误都是一些不能在入队时被服务器发现的错误,这些错误只会在命令实际执行时被触发。
·即使在事务的执行过程中发生了错误,服务器也不会中断事务的执行,它会继续执行事务中余下的其他命令,并且已执行的命令(包括执行命令所产生的结果)不会被出错的命令影响。
对数据库键执行了错误类型的操作是事务执行期间最常见的错误之一。
在下面展示的这个例子中,我们首先用SET命令将键"msg"设置成了一个字符串键,然后在事务里面尝试对"msg"键执行只能用于列表键的RPUSH命令,这将引发一个错误,并且这种错误只能在事务执行(也即是命令执行)期间被发现:
redis> SET msg "hello"
OK
redis> MULTI
OK
redis> SADD fruit "apple" "banana" "cherry"
QUEUED
redis> RPUSH msg "good bye" "bye bye"
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
因为在事务执行的过程中,出错的命令会被服务器识别出来,并进行相应的错误处理,所以这些出错命令不会对数据库做任何修改,也不会对事务的一致性产生任何影响。
3.服务器停机
如果Redis服务器在执行事务的过程中停机,那么根据服务器所使用的持久化模式,可能有以下情况出现:
·如果服务器运行在无持久化的内存模式下,那么重启之后的数据库将是空白的,因此数据总是一致的。
·如果服务器运行在RDB模式下,那么在事务中途停机不会导致不一致性,因为服务器可以根据现有的RDB文件来恢复数据,从而将数据库还原到一个一致的状态。如果找不到可供使用的RDB文件,那么重启之后的数据库将是空白的,而空白数据库总是一致的。
·如果服务器运行在AOF模式下,那么在事务中途停机不会导致不一致性,因为服务器可以根据现有的AOF文件来恢复数据,从而将数据库还原到一个一致的状态。如果找不到可供使用的AOF文件,那么重启之后的数据库将是空白的,而空白数据库总是一致的。
综上所述,无论Redis服务器运行在哪种持久化模式下,事务执行中途发生的停机都不会影响数据库的一致性。
19.3.3 隔离性
事务的隔离性指的是,即使数据库中有多个事务并发地执行,各个事务之间也不会互相影响,并且在并发状态下执行的事务和串行执行的事务产生的结果完全相同。
因为Redis使用单线程的方式来执行事务(以及事务队列中的命令),并且服务器保证,在执行事务期间不会对事务进行中断,因此,Redis的事务总是以串行的方式运行的,并且事务也总是具有隔离性的。
19.3.4 耐久性
事务的耐久性指的是,当一个事务执行完毕时,执行这个事务所得的结果已经被保存到永久性存储介质(比如硬盘)里面了,即使服务器在事务执行完毕之后停机,执行事务所得的结果也不会丢失。
因为Redis的事务不过是简单地用队列包裹起了一组Redis命令,Redis并没有为事务提供任何额外的持久化功能,所以Redis事务的耐久性由Redis所使用的持久化模式决定:
·当服务器在无持久化的内存模式下运作时,事务不具有耐久性:一旦服务器停机,包括事务数据在内的所有服务器数据都将丢失。
·当服务器在RDB持久化模式下运作时,服务器只会在特定的保存条件被满足时,才会执行BGSAVE命令,对数据库进行保存操作,并且异步执行的BGSAVE不能保证事务数据被第一时间保存到硬盘里面,因此RDB持久化模式下的事务也不具有耐久性。
·当服务器运行在AOF持久化模式下,并且appendfsync选项的值为always时,程序总会在执行命令之后调用同步(sync)函数,将命令数据真正地保存到硬盘里面,因此这种配置下的事务是具有耐久性的。
·当服务器运行在AOF持久化模式下,并且appendfsync选项的值为everysec时,程序会每秒同步一次命令数据到硬盘。因为停机可能会恰好发生在等待同步的那一秒钟之内,这可能会造成事务数据丢失,所以这种配置下的事务不具有耐久性。
·当服务器运行在AOF持久化模式下,并且appendfsync选项的值为no时,程序会交由操作系统来决定何时将命令数据同步到硬盘。因为事务数据可能在等待同步的过程中丢失,所以这种配置下的事务不具有耐久性。
no-appendfsync-on-rewrite配置选项对耐久性的影响
配置选项no-appendfsync-on-rewrite可以配合appendfsync选项为always或者everysec的AOF持久化模式使用。当no-appendfsync-on-rewrite选项处于打开状态时,在执行BGSAVE命令或者BGREWRITEAOF命令期间,服务器会暂时停止对AOF文件进行同步,从而尽可能地减少I/O阻塞。但是这样一来,关于“always模式的AOF持久化可以保证事务的耐久性”这一结论将不再成立,因为在服务器停止对AOF文件进行同步期间,事务结果可能会因为停机而丢失。因此,如果服务器打开了no-appendfsync-on-rewrite选项,那么即使服务器运行在always模式的AOF持久化之下,事务也不具有耐久性。在默认配置下,no-appendfsync-on-rewrite处于关闭状态。
不论Redis在什么模式下运作,在一个事务的最后加上SAVE命令总可以保证事务的耐久性:
redis> MULTI
OK
redis> SET msg "hello"
QUEUED
redis> SAVE
QUEUED
redis> EXEC
1) OK
2) OK
不过因为这种做法的效率太低,所以并不具有实用性。