Redis通过MULTI
,EXEC
,WATCH
等命令来实现事务功能. 事务提供了一种将多个命令请求打包, 然后一次性,按顺序地执行多个命令的机制. 并且在事务执行期间, 服务器不会中断事务而改去执行其他客户端的命令请求, 它会将事务中的所有命令都执行完毕, 然后才去处理其他客户端的请求命令.
事务的实现
一个事务从开始到结束通常会经历一下三个阶段:
-
事务开始
-
命令入队
-
事务执行
事务开始
MULTI
命令的执行标志着事务的开始:
redis> MULTI
MULTI
命令可以将执行该命令的客户端从非事务状态切换至事务状态, 这一切换是通过在客户端状态的flags
属性中打开REDIS_MULTI
表示来完成的。
命令入队
当一个客户端处于非事务状态时, 这个客户端发送的命令会立即被服务器执行:
与此不同的是, 当一个客户端切换到事务状态之后, 服务器会根据这个客户端发来的不同命令执行不同的操作:
-
如果客户端发送的命令为
EXEC
,DISCARD
,WATCH
,MULTI
四个命令的其中一个, 那么服务器立即执行这个命令 -
与此相反, 如果客户端发送的命令是
EXEC
,DISCARD
,WATCH
,MULTI
四个命令以外的其他命令, 那么服务器并不立即执行这个命令, 而是将这个命令放入一个事务队列
里面, 然后向客户端返回QUEUED
回复。
事务队列
每个Redis客户端都有自己的事务状态, 这个事务事务状态保存在客户端状态的mstate
属性里面:
typedef struct redisClient { // 事务状态 multiState mstate; } redisClient;
事务状态包含一个事务队列, 以及一个已入队列命令的计数器:
typedef struct multiState { // 事务队列, FIFO顺序 multiCmd *commands; // 已入队命令技术 int count; }
事务队列是一个multiCmd
类型的数组, 数组中的每个multiCmd
结构都保存了一个已入队命令的相关信息, 包括指向命令实现函数的指针, 命令的参数, 以及参数的数量:
typedef struct multiCmd { // 参数 robj **argv; // 参数数量 int argc; // 命令指针 struct redisCommand *cmd; } multiCmd;
事务队列以先进先出(FIFO)的方式保存入队的命令, 较先入队的命令会被放到数组的前面, 而较后入队的命令则会被放到数组的后面.
执行事务
当一个处于事务状态的客户端向服务器发送EXEC
命令时, 这个EXEC
命令将立即被服务器执行。服务器会遍历这个客户端的事务队列, 执行队列中保存的所有命令, 最后将执行命令所得的结果全部返回给客户端。
WATCH命令的实现
WATCH
命令是一个乐观锁, 它可以在exec
命令执行之前,监视任意数量的数据库键, 并在EXEC
命令执行时, 检查被监视的键是否至少有一个已经被修改过了, 如果是的话, 服务器将拒绝执行事务, 并向客户端返回代表事务执行失败的空回复.
NOTE: 当监视某一个key时, 如果在执行事务期间, 对应的key被更改, 那么对应的事务执行将会失败.
使用WATCH 命令监视数据库键
每个Redis数据库都保存着一个watched_keys
字典, 这个字典的键是某个被WATCH
命令监视的数据库键, 而字典的值则是一个链表, 链表中记录了所有监视相应数据库键的客户端。
typedef struct redisDb { // 正在被WATCH命令监视的键 dict *watched_keys; }
通过watched_keys
字典, 服务器可以清楚地知道哪些数据库正在被监视, 以及哪些客户端正在监视这些数据库键.
监视机制的触发
所有对数据库进行修改的命令, 比如SET
,LPUSH
,SADD
,ZERM
,DEL
,FLUSHDB
等等, 在执行之后都会调用touchWatchKey
函数对watched_keys
字典进行检查, 查看是否有客户端正在监视刚刚被命令修改过的数据键, 如果有的话, 那么touchWatchKey
函数会将监视被修改键的客户端的REDIS_DIRTY_CAS
表示打开, 表示客户端的事务安全性已经被破坏.
判断事务是否安全
当服务器接收到一个客户端发来的EXEC
命令时, 服务器会根据这个客户端是否打开了REDIS_DIRTY_CAS
标识来决定是否执行事务:
-
如果客户端的
REDIS_DIRTY_CAS
标识已经被打开, 那么说明客户端所监视的键当中, 至少有一个键已经被修改过了, 在这种情况下, 客户端提交的事务已经不再安全, 所以服务器会拒绝执行客户端提交的事务. -
如果客户端的
REDIS_DIRTY_CAS
标识没有被打开, 那么说明客户端监视的所有键都没有被修改过, 事务任然是安全的, 服务器执行客户端提交的这个事务。
事务的ACID性质
在Redis中, 事务总是具有原子性(Atomicity),一致性(Consistency)
,隔离性(Isolation),并且当Redis运行在某种特定的持久化模式下时, 事务也具有耐久性(Durability).
原子性
事务具有原子性指的是, 数据库事务中的多个操作当做一个整体执行, 服务器要么就执行事务中的所有操作, 要么就一个操作也不执行。
对于Redis的事务功能来说, 事务队列中的命令要么就全部执行, 要么就一个不执行, 因此, Redis的事务是具有原子性的。
NOTE: Redis的事务和传统的关系型数据库事务的最大区别在于, Redis不支持事务回滚机制, 即使事务队列中的某个命令在执行期间出现了错误, 整个事务也会继续执行下去, 知道将事务队列中的所有命令执行完毕为止.
一致性
事务具有一致性指的是, 如果数据库在执行事务之前是一致的, 那么在事务执行之后, 无论是否是否执行成功, 数据库也应该仍然是一致的。
入队错误
如果一个事务在入队命令的过程中,出现了命令不存在, 或者命令的格式不正确等情况, 那么Redis将拒绝执行这个事务.
2.6.5 版本之前的错误处理
错误的命令不会被入队, 所以Redis不会尝试去执行错误的命令, 因此, 即使在2.6.5以前版本, Redis事务的一致性也不会被入队错误影响。
执行错误
除了入队时可能发生错误以外, 事务还可能在执行的过程中发生错误. 关于这种错误有两个需要说明的地方:
-
执行过程中发生的错误都是一些不能在入队时被服务器发现的错误, 这些错误只会在命令实际执行时被触发
-
即使在事务的执行过程中发生了错误, 服务器也不会中断事务的执行, 他会继续执行事务中余下的其他命令, 并且一致性的命令不会被出错的命令影响。
NOTE: 因为事务执行的过程中, 出错的命令会被服务器识别出来, 并进行相应的错误处理, 所以这些出错命令不会对数据库做任何修改, 也不会对事务的一致性产生任何影响。
服务器停机
如果Redis服务器在执行事务的过程中停机, 那么根据服务器所使用的持久化模式, 可能有以下情况出现:
-
如果服务器运行在无持久化的内存模式下, 那么重启之后的数据库将是空白的, 因此数据总是一致的
-
如果服务器运行在
RDB
模式下, 那么在事务中途停机不会导致不一致性, 因为服务器可以根据现有的RDB文件来回复数据.从而将数据库还原到一个一致的状态。如果找不到可供使用的RDB文件, 那么重启之后的数据将是空白的, 而空白数据库总是一致的。 -
如果服务器运行在AOF模式下, 那么在事务中途停机不会导致不一致性, 因为服务器可以根据现有的AOF文件来回复数据, 从而将数据库还原到一个一致的状态。如果找不到可供使用的AOF文件, 那么重启之后的数据库将是空白的,而空白数据库总是一致的。
隔离性
事务的隔离性指的是, 即使数据库中有多个事务并发地执行, 哥哥事务之间也不会互相影响, 并且在并发状态下执行事务和穿行执行的事务产生的结果完全相同。
因为Redis使用单线程的方式来执行事务, 并且服务器保证, 在执行期间不会对事务进行中断, 因此, Redis的事务总是以串行的方式运行的, 并且事务也总是具有隔离性的。
耐久性
事务的耐久性指的是, 当一个事务执行完毕时, 执行这个事务所的的结果已经被保存到永久性存储介质里面, 即使服务器在事务执行完毕之后停机, 执行事务所得到的结果也不会丢失
因为Redis的事务不过是简单地用队列包裹起了一组Redis命令, Redis并没有为事务提供任何额外的持久化功能, 所以Redis事务的耐久性由Redis所使用的持久化模式决定:
-
当服务器在无持久化的内存模式下运作时, 事务不具有耐久性: 一旦服务器停机, 包括事务数据在内的所有服务器数据都将丢失
-
当服务器在RDB持久化模式下运作时, 服务器只会在特定的保存条件被满足时, 才会被执行BGSAVE命令, 对数据库进行保存操作, 并且异步执行的
BGSAVE
不能保证事务数据被第一时间保存到硬盘里面. -
当服务器运行在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持久化可以保证事务的耐久性