- Redis作为一个数据库使用时, 它本身也提供了事务机制的支持。
- 事务执行期间, Redis服务器不会去中断事务而执行其他客户端的命令请求, 它会将事务中所有的命令都执行完毕之后, 才去处理其他客户端的命令请求。
- Redis事务的实现主要通过
MULTI、EXEC和WATCH三个命令实现, 其中MULTI用于开启事务,EXEC用于提交事务、WATCH用于监视任意数量的key。
1. Redis事务实现原理
-
Redis 事务的本质是一组命令的集合。事务支持一次执行多个命令, 一个事务中所有命令都会被序列化。在事务执行过程, 会按照顺序串行化执行队列中的命令, 其他客户端提交的命令请求不会插入到事务执行命令序列中。 总结说: redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。
-
Redis事务实现的一个核心结构是事务队列, 当服务器以事务状态运行时, 针对于接收到的不同命令会有不同的操作:
- 如果是
MULTI、EXEC、WATCH和DISCARD其中的任意一个, 服务器立刻执行; - 如果不是上述的四个命令, 那么服务器就会将其放入到一个事务队列中, 然后向服务器返回
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 开启事务后, 后续所有除了 MULTI、EXEC、WATCH 和 DISCARD 之外的命令都会进入到事务队列中。 当执行EXEC时, 服务器会遍历事务队列,执行队列中的所有命令,最后将命令执行的结果回复给客户端。
以事务形式执行上述的命令, 如下所示:
事务取消如下:
事务出现错误的处理
- 命令语法错误, 会导致事务提交失败, 整个事务不生效。
- Redis类型错误(运行时错误) 在运行时会检测类型错误, 最终导致运行错误, 最终导致事务提交失败, 此时事务并不会回滚, 而是跳过错误命令继续执行。
2. watch命令参与事务控制
- CAS操作实现乐观锁。
WATCH 命令可以为 Redis 事务提供 check-and-set (CAS)行为。
- 被 WATCH 的键会被监视, 并会发觉这些键是否被改动过了。 如果有至少一个被监视的键在 EXEC 执行之前被修改了, 那么整个事务都会被取消, EXEC 返回nil-reply来表示事务已经失败。
- 使用 WATCH 解决两个事务并发问题:
- 事务 1 在事务2执行之前, 修改了incr数值;
- 事务 2 watch了incr的变化, 监听到事务 1 对 incr的修改后, 事务 2失效。
- 如果事务 2 在执行了WATCH命令后, EXEC之前, 其他事务修改了incr的值, 那么当前的事务2将会失败。
WATCH怎么实现的?
-
Redis使用
WATCH命令来决定事务是继续执行还是回滚, 那就需要在MULTI之前使用WATCH来监控某些键值对, 然后使用MULTI命令来开启事务, 执行对数据结构操作的各种命令, 此时这些命令入队列。 -
当使用EXEC执行事务时, 首先会比对
WATCH所监控的键值对, 如果没发生改变, 它会执行事务队列中的命令, 提交事务; 如果发生变化, 将不会执行事务中的任何命令, 同时事务回滚。当然无论是否回滚, Redis都会取消执行事务前的WATCH命令。
3. Redis事务的执行步骤
- Redis事务执行是三个阶段:
- 开启: 以
MULTI开启一个事务 - 入队: 将多个命令入队到事务中, 接到这些命令并不会立即执行, 而是放到等待执行的事务队列里面
- 执行: 由
EXEC命令触发事务
- 开启: 以
- 当一个客户端切换到事务状态之后, 服务器会根据这个客户端发来的不同命令执行不同的操作:
- 如果客户端发送的命令为 EXEC 、 DISCARD 、 WATCH 、 MULTI 四个命令的其中一个, 那么服务器立即执行这个命令。
- 与此相反, 如果客户端发送的命令是 EXEC 、 DISCARD 、 WATCH 、 MULTI 四个命令以外的其他命令, 那么服务器并不立即执行这个命令, 而是将这个命令放入一个事务队列里面, 然后向客户端返回 QUEUED 回复。
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, 回滚是没有办法处理这些情况的。