Redis 支持事务吗?如何实现?

311 阅读6分钟
  • Redis作为一个数据库使用时, 它本身也提供了事务机制的支持。
  • 事务执行期间, Redis服务器不会去中断事务而执行其他客户端的命令请求, 它会将事务中所有的命令都执行完毕之后, 才去处理其他客户端的命令请求。
  • Redis事务的实现主要通过MULTIEXECWATCH三个命令实现, 其中MULTI用于开启事务, EXEC用于提交事务、WATCH用于监视任意数量的key。

1. Redis事务实现原理

  • Redis 事务的本质是一组命令的集合。事务支持一次执行多个命令, 一个事务中所有命令都会被序列化。在事务执行过程, 会按照顺序串行化执行队列中的命令, 其他客户端提交的命令请求不会插入到事务执行命令序列中。 总结说: redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。

  • Redis事务实现的一个核心结构是事务队列, 当服务器以事务状态运行时, 针对于接收到的不同命令会有不同的操作:

    • 如果是MULTIEXECWATCHDISCARD其中的任意一个, 服务器立刻执行;
    • 如果不是上述的四个命令, 那么服务器就会将其放入到一个事务队列中, 然后向服务器返回QUEUED回复, 表示命令已经入队, 等待执行。

每个RedisClient通过mstate字段来标识自己的事务状态, 而事务状态又包含一个事务队列和一个计数器, 如下所示:

typedef struct redsiClient{
    // ... 
    multiState mstate; // 事务状态;
    // ... 
} 
typedef struct multiState{
    // ... 
    multiCmd *commands; // 事务队列;
    int count; // 入队命令计数器;
    // ... 
} 
typedeef struct multiCmd{
    // ... 
    robj **args; // 参数;
    int argc; // 参数数量;
    struct redisCommand *cmd; // 命令指针;
    // ... 
}

假设执行如下的Redis命令:

MULTI

set "key_1" "value_1" 
set "key_2" "value_2" 
get "key_1" 

EXEC

当服务器使用 MULTI 开启事务后, 后续所有除了 MULTIEXECWATCHDISCARD 之外的命令都会进入到事务队列中。 当执行EXEC时, 服务器会遍历事务队列,执行队列中的所有命令,最后将命令执行的结果回复给客户端。

image.png

以事务形式执行上述的命令, 如下所示:

image.png

事务取消如下:

image.png

事务出现错误的处理

  • 命令语法错误, 会导致事务提交失败, 整个事务不生效。

image.png

  • Redis类型错误(运行时错误) 在运行时会检测类型错误, 最终导致运行错误, 最终导致事务提交失败, 此时事务并不会回滚, 而是跳过错误命令继续执行。

image.png

2. watch命令参与事务控制

  • CAS操作实现乐观锁。
WATCH 命令可以为 Redis 事务提供 check-and-set (CAS)行为。
  • 被 WATCH 的键会被监视, 并会发觉这些键是否被改动过了。 如果有至少一个被监视的键在 EXEC 执行之前被修改了, 那么整个事务都会被取消, EXEC 返回nil-reply来表示事务已经失败。
  • 使用 WATCH 解决两个事务并发问题:
    • 事务 1 在事务2执行之前, 修改了incr数值;
    • 事务 2 watch了incr的变化, 监听到事务 1 对 incr的修改后, 事务 2失效。

image.png

image.png

  • 如果事务 2 在执行了WATCH命令后, EXEC之前, 其他事务修改了incr的值, 那么当前的事务2将会失败。

WATCH怎么实现的?

  • Redis使用 WATCH 命令来决定事务是继续执行还是回滚, 那就需要在 MULTI 之前使用WATCH来监控某些键值对, 然后使用 MULTI 命令来开启事务, 执行对数据结构操作的各种命令, 此时这些命令入队列。

  • 当使用EXEC执行事务时, 首先会比对 WATCH 所监控的键值对, 如果没发生改变, 它会执行事务队列中的命令, 提交事务; 如果发生变化, 将不会执行事务中的任何命令, 同时事务回滚。当然无论是否回滚, Redis都会取消执行事务前的WATCH命令。

image.png

3. Redis事务的执行步骤

  • Redis事务执行是三个阶段:
    • 开启:MULTI 开启一个事务
    • 入队: 将多个命令入队到事务中, 接到这些命令并不会立即执行, 而是放到等待执行的事务队列里面
    • 执行:EXEC 命令触发事务
  • 当一个客户端切换到事务状态之后, 服务器会根据这个客户端发来的不同命令执行不同的操作:
    • 如果客户端发送的命令为 EXEC 、 DISCARD 、 WATCH 、 MULTI 四个命令的其中一个, 那么服务器立即执行这个命令。
    • 与此相反, 如果客户端发送的命令是 EXEC 、 DISCARD 、 WATCH 、 MULTI 四个命令以外的其他命令, 那么服务器并不立即执行这个命令, 而是将这个命令放入一个事务队列里面, 然后向客户端返回 QUEUED 回复。 image.png

4. Redis事务的ACID

Redis所支持的事务同样满足事务的ACID, 具体表现来说:

  • 原子性: 事务中的命令要么全部执行, 要么一个都不执行。但Redis不支持事务的回滚, 即使事务中包含出错的命令, 也不影响其他正确命令的执行。也即Redis的事务是原子性的, 所有的命令,要么全部执行,要么全部不执行。而不是完全成功。
  • 一致性: redis事务可以保证命令失败的情况下得以回滚, 数据能恢复到没有执行之前的样子, 是保证一致性的, 除非redis进程意外终结。
  • 隔离性: redis事务是严格遵守隔离性的, 原因是redis是单进程单线程模式(v6.0之前),可以保证命令执行过程中不会被其他客户端命令打断。 但是, Redis不像其它结构化数据库有隔离级别这种设计。
  • 持久性: redis事务是不保证持久性的, 这是因为redis持久化策略中不管是RDB还是AOF都是异步执行的, 不保证持久性是出于对性能的考虑。

5. 为什么Redis事务不支持回滚

  • 以下是redis不支持回滚的优点和深入理解:
    • Redis 命令只会因为错误的语法而失败(并且这些问题不能在入队时发现), 或是命令用在了错误类型的键上面。这也就是说, 从实用性的角度来说, 失败的命令是由编程错误造成的, 而这些错误应该在开发的过程中被发现, 而不应该出现在生产环境中。
    • 因为不需要对回滚进行支持, 所以 Redis 的内部可以保持简单且快速。
    • 有种观点认为 Redis 处理事务的做法会产生 bug, 然而需要注意的是, 在通常情况下, 回滚并不能解决编程错误带来的问题。 举个例子, 如果你本来想通过 INCR 命令将键的值加上 1, 却不小心加上了 2, 又或者对错误类型的键执行了 INCR, 回滚是没有办法处理这些情况的。