事务
- 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
1) OK
2) "Practical Common Lisp"
3) OK
4) "Peter Seibel"
事务的三个阶段
事务开始
- MULTI命令的执行标志着事务的开始
- MULTI命令可以将执行该命令的客户端从非事务状态切换至事务状态
命令入队
- 当一个客户端处于非事务状态时,这个客户端发送的命令会立即被服务器执行
- 与此不同的是,当一个客户端切换到事务状态之后,服务器会根据这个客户端发来的不同命令执行不同的操作
事务队列
事务队列以先进先出(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
1) OK
2) "Practical Common Lisp"
3) OK
4) "Peter Seibel"
WATCH命令的实现
watch命令是一个乐观锁,它可以在exec命令执行之前监视多个键,并且在exec命令执行时,检查被监视的键,是否有被修改过,是的话,服务器就会拒绝执行事务。
例如:
redis> WATCH "name"
OK
redis> MULTI
OK
redis> SET "name" "peter"
QUEUED
redis> EXEC
(nil)
在时间T4,客户端B修改了"name"键的值,当客户端A在T5执行EXEC命令时,服务器会发现WATCH监视的键"name"已经被修改,因此服务器拒绝执行客户端A的事务,并向客户端A返回空回复。
使用WATCH命令监视数据库键
每个Redis数据库都保存着一个watched_keys字典,字典的值是一个链表,通过watched_keys字典,服务器可以清楚地知道哪些数据库键正在被监视,以及哪些客户端正在监视这些数据库键。
当客户端c10086执行以下命令后,字典中的数据如图所示
redis> WATCH "name" "age"
OK
监视机制的触发
所有对数据库进行修改的命令,比如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服务器在执行事务的过程中停机,那么根据服务器所使用的持久化模式,可能有以下情况出现:
- 如果redis没有开启持久化机制,那么重启后数据本来也就是空的,所以也就不存在一致性问题
- 如果是用的RDB文件恢复的数据,那么事务执行时的数据也会被记录到RDB文件中,一样也可以通过rdb文件恢复
- 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
1) OK
2) OK