Redis事务实现

8 4 月, 2021 155点热度 0人点赞 0条评论

事务

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持久化可以保证事务的耐久性这一结论不再成立。

专注着

一个奋斗在编程路上的小伙

文章评论