事务
一个完整的WATCH事务执行过程
假设当前服务端为c10086,而数据库watched_keys字典的当前状态如图所示,那么当c10086执行以下WATCH命令之后
c10086> WATCH "name"
OK
watched_keys字典将更新如图所示的状态。接下来客户端c10086继续向服务器发送MULTI命令,并将一个SET命令放入
事务队列:
c10086> MULTI
OK
c10086> SET "name" "peter"
QUEUED
就在这时,另一个客户端c999向服务器发送了一条SET命令,将"name"键的值设置成了"john":
c999>SET "name" "john"
OK
c999执行的这个SET命令会导致正在监视"name"键的所有客户端的REDIS_DIRTY_CAS标识被打开,其中包括客户端c10086.
之后,当c10086向服务器发送EXEC命令的时候,因为c10086的REDIS_DIRTY_CAS标志已经被打开,所以服务器将拒绝执行
它提交的事务:
c10086>EXEC
(nil)
事务的ACID性质。
在传统的关系式数据库中,常常用ACID性质来检验事务功能的可靠性和安全性。在Redis中,事务总是具有原子性(Atomicity)、一致性(Consistency)和隔离性(Isolation),并且当Redis运行在某种特定的持久化模式下,事务也具有耐久性(Durability)
原子性
事务具有原子性指的是,数据库将事务中的多个操作当作一个整体来执行,服务器要么就执行事务中的所有操作,要么就一个操作也不执行。对于Redis的事务功能来说,事务队列中的命令要么就全部都执行,要么就一个都不执行,因此,Redis的事务是具有原子性的。
Redis的事务和传统的关系型数据库事务的最大区别在于,Redis不支持事务回滚机制(rollback),即使事务队列中的某个命令在执行期间出现了错误,整个事务也会继续执行下去,直到将事务队列中的所有命令都执行完毕为止
例子
- 举个例子。以下展示了一个成功执行的事务,事务中的所有命令都会被执行
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"
- 举个例子。与此相反,以下展示了一个执行失败的事务,这个事务因为命令入队出错而被服务器拒绝执行,事务中的所有命令都不会被执行:
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> GET msg
QUEUED
127.0.0.1:6379> EXEC
(error) EXECABORT Transaction discarded because of previous errors.
- 举个例子。在下面的例子中,即使RPUSH命令在执行期间出现了错误,事务的后续命令也会继续执行下去,并且之前执行的命令也不会有任何影响
127.0.0.1:6379> SET msg "hello"
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SADD fruit "apple" "banana" "cherry"
QUEUED
127.0.0.1:6379> RPUSH msg "good bye" " bye bye"
QUEUED
127.0.0.1:6379> SADD alphabet "a" "b" "c"
QUEUED
127.0.0.1:6379> EXEC
1) (integer) 3
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
3) (integer) 3
127.0.0.1:6379> SCARD fruit
(integer) 3
Redis的作者在事务功能的文档中解释说,不支持事务回滚是因为这种复杂的功能和Redis追求简单高效的设计主旨不相符,并且他认为,Redis的事务的执行时错误通常都是编程错误产生的,这种错误通常只会出现在开发环境中,而很少会在实际的生产环境中出现,所以他认为没有必要为Redis开发事务回滚功能
一致性
事务具有一致性指的是,如果数据库在执行事务之前是一致的,那么在事务执行之后,无论事务是否执行成功,数据库也仍然是一致的。"一致"指的是数据符合数据库本身的定义和要求,没有包含非法或者无效的错误数据。Redis通过谨慎的错误检测和简单的设计来保证事务的一致性
入队错误
如果一个事务在入队命令的过程中,出现了命令不存在,或者命令的格式不正确等情况,那么Redis将拒绝执行这个事务。在以下的示例中,因为客户端尝试向事务入队一个不存在的命令YAHOOOO,所以客户端提交的事务会被服务器拒绝执行:
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET msg "hello"
QUEUED
127.0.0.1:6379> YAHOOOO
(error) ERR unknown command 'YAHOOOO'
127.0.0.1:6379> GET msg
QUEUED
127.0.0.1:6379> EXEC
(error) EXECABORT Transaction discarded because of previous errors.
因为服务器会拒绝执行入队过程中出现错误的事务,所以Redis事务的一致性不会被带有入队错误的事务影响。
Redis2.6.5以前的入队错误处理
在Redis2.6.5以前的版本,即使有命令在入队过程中发生了错误,事务一样可以执行,不过被执行的命令只包括那些正确入队的命令。以下代码是在Redis2.6.4版本上测试的,可以看到,事务可以正常执行,但只有成功入队的SET命令和GET命令被执行了,而错误的YAHOOOO则被忽略了
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET msg "hello"
QUEUED
127.0.0.1:6379> YAHOOOO
(error) ERR unknown command 'YAHOOOO'
127.0.0.1:6379> GET msg
QUEUED
127.0.0.1:6379> EXEC
1).OK
2)."hello"
因为错误的命令不会被入队,所以Redis不会尝试去执行错误的命令,因此,即使在2.6.5以前的版本中,Redis事务的一致性也不会被入队错误影响
执行错误
除了入队时可能发生错误以外,事务还可能在执行的过程中发生错误。关于这种错误有两个需要说明的地方:
- 1.执行过程中发生的错误都是一些不能入队时被服务器发现的错误,这些错误只会在命令实际执行时被触发
- 2.即使在事务的执行过程中发生了错误,服务器也不会中断事务的执行,它会继续执行事务中余下的其他命令,并且已执行的命令(包括执行命令所产生的结果)不会被出错的命令影响对数据库键执行了错误类型的操作是事务执行期间最常见的错误之一。
在下面的示例中,首先用SET命令将键"msg"设置成了一个字符串键,然后在事务里面尝试对"msg"键执行只能用于列表键的RPUSH命令,这将引发一个错误,并且这种错误只能在事务执行(也即是命令执行)期间被发现:
127.0.0.1:6379> SET msg "hello"
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SADD fruit "apple" "banana" "cherry"
QUEUED
127.0.0.1:6379> RPUSH msg "good bye" " bye bye"
QUEUED
127.0.0.1:6379> SADD alphabet "a" "b" "c"
QUEUED
127.0.0.1:6379> EXEC
1) (integer) 3
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
3) (integer) 3
因为在事务执行的过程中,出错的命令会被服务器识别出来,并进行相应的错误处理,所以这些出错命令不会对数据库做任何修改,也不会对事务的已执行产生影响
服务器停机
如果Redis服务器在执行事务的过程中停机,那么根据服务器所使用的持久化模式,可能有以下情况出现:
- 1.如果服务器运行在无持久化的内存模式下,那么重启之后的数据库将是空白的,因此数据总是一致的。
- 2.如果服务器运行在RDB模式下,那么在事务中途停机不会导致不一致性,因为服务器可以根据现有的RDB文件来恢复数据,从而将数据库还原到一个一致的状态。如果找不到可供使用的RDB文件,那么重启之后的
数据库将是空白的,而空白数据库总是一致的 - 3.如果服务器运行在AOF模式下,那么在事务中途停机不会导致不一致性,因为服务器可以根据现有的AOF文件,从而将数据库还原到一个一致的状态。如果找不到可供使用的AOF文件,那么重启之后的数据库将是
空白的,而空白数据库总是一致的。综上所述,无论Redis服务器运行在哪种持久化模式下,事务执行中途发生的停机都不会影响数据库的一致性
隔离性
事务的隔离性指的是,即使数据库中有多个事务并发地执行,各个事务之间也不会互相影响,并且在并发状态下执行的事务和串行执行的事务产生的结果完全相同。因为Redis使用单线程的方式来执行事务(以及事务
队列中的命令),并且服务器保证在执行事务期间不会对事务进行中断,因此,Redis的事务总是以串行的方式运行的。并且事务也总是具有隔离性
耐久性
事务的耐久性指的是,当一个事务执行完毕时,执行这个事务所得的结果已经被保存到永久性存储介质(比如硬盘)里面了,即使服务器在事务执行完毕之后停机,执行事务所得的结果也不会丢失。因为Redis的事务不过是简单地使用队列包裹起了一组Redis命令,Redis并没有为事务提供任何额外的持久化功能,所以Redis事务的耐久性由Redis所使用的持久化模式决定:
- 1.当服务器在无持久化的内存模式下运作时,事务不具有耐久性:一旦服务器停机,包括事务数据在内的所有服务器数据都将丢失
- 2.当服务器在RDB持久化模式下运作时,服务器只会在特定的保存条件被满足时才会执行BGSAVE命令,对数据库进行保存操作,并且异步执行的BGSAVE不能保证事务数据第一时间保存到硬盘里面,因此RDB持久化模式下的事务也不具有耐久性
- 3.当服务器运行在AOF持久化模式下,并且appendfsync选项的值为everysec时,程序会每秒同步一次命令数据到硬盘,因为停机可能恰好发生在等待同步的那一秒钟之内,这可能会造成事务数据丢失,所以这种配置下的事务不具有耐久性
- 4.当服务器运行在AOF持久化模式下,并且apendfsync选项的值为no时,程序会交由操作系统来决定何时将命令数据同步到硬盘。因为事务数据可能在等待同步的过程中丢失,所以这种配置下的事务不具有耐久性
不论Redis在什么模式下运作,在一个事务的最后加上SAVE命令总可以保证事务的耐久性:
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET msg "hello"
QUEUED
127.0.0.1:6379> SAVE
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) OK
不过这种做法的效率太低,所以不具有实用性