Redis事务与Watch机制相关知识点整理

Redis 事务

Redis事务提供了一种 “将多个命令打包, 然后一次性、按顺序地执行” 的机制,事务在执行的期间不会主动中断,服务器在执行完事务中的所有命令之后, 才会继续处理其他客户端的其他命令。

Redis 通过 MULTIDISCARDEXECWATCH 四个命令来实现事务功能。

MULTI: 标记着事务的开始,客户端从非事务状态切换到事务状态

EXEC: 执行事务队列中的命令

DISCARD: 取消一个事务,客户端从事务状态调整回非事务状态

WATCH: 在事务开始之前监视任意数量的键,当调用 EXEC 命令执行事务时, 如果任意一个被监视的键已经被其他客户端修改了,那么整个事务不再执行直接返回失败。

  • 基本事务
redis > multi
OK
redis > incr foo
QUEUED
redis > set t1 1
QUEUED
redis > exec
  • Watch与事务结合
redis> WATCH name
OK
redis> MULTI
OK
redis> SET name peter
QUEUED
redis> EXEC

Redis 事务实现原理

事务从开始到执行三个阶段:

  1. 开始事务
  2. 命令入队
  3. 执行事务

MULTI

MULTI命令会将客户端的 REDIS_MULTI 选项打开, 让客户端从非事务状态切换到事务状态。Redis的事务是不可嵌套的,所以当客户端已经处于事务状态再向服务器发送 MULTI命令,服务器会向客户端发送一个错误后继续等待其他命令的入队。

image

OFFER(COMMAND) TO QUEUE

当客户端进入事务状态之后,服务器在接收该客户端命令时,不会立即执行,而是将这些命令全部放进一个事务队列里,然后返回 QUEUED ,表示命令已入队。事务队列是一个数组, 每一项是都包含三个属性:

  • 要执行的命令(cmd)
  • 命令的参数(argv)
  • 参数的个数(argc)
Index cmd argv argc
0 SET [“name”,“Python”] 2
1 GET [“name”] 1
2 N

EXEC

EXEC命令执行时,如果当前客户端处于事务状态,服务器根据客户端所保存的事务队列, 以先进先出(FIFO)的方式执行事务队列中的命令,并将所得的结果以 FIFO 的顺序保存到一个回复队列中。当事务队列里的所有命令被执行完之后,会将回复队列作为自己的执行结果返回给客户端,此时客户端从事务状态返回到非事务状态。

Index reply-type reply-content
0 status code reply OK
1 bulk reply “Python”
2

执行流程如下所示:

image

DISCARD

DISCARD 命令用于取消一个事务,它清空客户端的整个事务队列,然后将客户端从事务状态调整回非事务状态, 最后返回字符串OK给到客户端说明事务已被取消。

WATCH

WATCH 命令用于在事务开始之前监视任意数量的键: 当调用 EXEC 命令执行事务时, 如果任意一个被监视的键已经被其他客户端修改了, 那么整个事务不再执行, 直接返回失败。

  • watched_keys字典

在每个代表数据库的 redis.h/redisDb 结构类型中, 都保存了一个 watched_keys 字典, 字典的键是这个数据库被监视的键, 而字典的值则是一个链表, 链表中保存了所有监视这个键的客户端。

image

WATCH命令的作用, 就是将当前客户端和要监视的键在 watched_keys 中进行关联。

通过 watched_keys 字典, 如果程序想检查某个键是否被监视, 那么它只要检查字典中是否存在这个键即可; 如果程序要获取监视某个键的所有客户端, 那么只要取出键的值(一个链表), 然后对链表进行遍历即可。

  • watch触发

在任何对数据库键空间(key space)进行修改的命令成功执行之后 (比如 FLUSHDBSETDELLPUSHSADDZREM…), multi.c/touchWatchedKey 函数都会被调用,它检查数据库的 watched_keys 字典, 检查是否有客户端在监视已经被命令修改的键, 如果有的话, 程序将所有监视这些被修改键的客户端的 REDIS_DIRTY_CAS 选项打开:

image

之后当客户端发送 EXEC 命令触发事务执行时, 服务器会对客户端的状态进行检查:

如果客户端的 REDIS_DIRTY_CAS 选项已经被打开,那么说明被客户端监视的键至少有一个已经被修改了,事务的安全性已经被破坏。服务器会放弃执行这个事务,直接向客户端返回空回复。 如果 REDIS_DIRTY_CAS 选项没有被打开,那么说明所有监视键都安全,服务器正式执行事务。

例如:

1. client3对 key1 进行了修改
2. 所有监视 key1 的客户端(client2,client5,client6)的 REDIS_DIRTY_CAS 选项都会被打开
3. 当client2,client5,client6执行 EXEC 的时候, 它们的事务都会失败

Redis事务与ACID

ACID事务定义

ACID 事务是指一组属性,旨在确保数据库事务的可靠性和一致性。术语 “ACID” 代表原子性、一致性、隔离性持久性,这是 ACID 事务的四个关键属性。本质上,ACID 事务保证数据库操作正确执行,如果出现任何类型的故障,数据库可以恢复到以前的状态,而不会丢失任何数据或影响数据的一致性。

  • Atomicity(原子性):保证事务被视为一个单一的、不可分割的工作单元。如果事务的任何部分失败,则必须回滚整个事务,这意味着事务期间所做的任何更改都将被撤消。这可确保数据库保持一致状态,而不管事务期间可能发生的任何故障。
  • Consistency(一致性):一致性确保数据库在事务前后保持有效状态,即数据库模式必须满足所有约束和规则,任何违反这些约束的事务都必须回滚以保持数据库的一致性。确保了数据库保持其完整性并且数据保持准确和可靠。
  • Isolation(隔离性):每个事务独立于其他事务运行,这意味着一个事务的效果只有在提交后才对其他事务可见。防止并发事务之间的干扰和冲突,维护数据库的完整性和一致性。
  • Durability(持久性):确保即使在系统故障时,事务期间对数据库所做的更改也是不可逆的。

Atomicity(原子性)

Redis事务具备了一定的原子性,但不支持事务回滚机制:

  • Redis 入队错误,如客户端向服务器发送了错误的命令,服务器将向客户端返回一个出错信息, 并且将客户端的事务状态设为 REDIS_DIRTY_EXEC拒绝执行事务 ,保证原子性。
  • Redis 队列执行错误,Redis只会将错误包含在事务的结果中,不会引起事务中断或整个失败,也不会影响已执行事务命令的结果。因为Redis 中并没有提供回滚机制,这种情况无法保证事务的原子性。
  • EXEC 命令执行时实例故障
    • AOF 模式:需要使用 redis-check-aof 工具检查 AOF 日志文件,把未完成的事务操作从 AOF 文件中去除,再使用 AOF 恢复实例后,事务操作不会再被执行,从而保证了原子性。
    • RDB 模式:Redis 不会中断事务去执行保存 RDB 的工作,只有在事务执行之后,保存 RDB 的工作才有可能开始,所以事务内执行的命令,不管成功了多少,都不会被保存到 RDB 文件里,从而保证原子性。

Consistency(一致性)

Redis事务具备了一致性。

  • Redis 入队错误的情况, REDIS_DIRTY_EXEC拒绝执行事务,不会影响事务的一致性。
  • Redis 队列执行错误的情况,不会引起事务中断或整个失败,也不会影响已执行事务命令的结果,所以它不会影响事务的一致性。
  • EXEC 命令执行时实例故障的情况
    • AOF 模式:使用 redis-check-aof 移除事务后进行还原,数据总是一致的,而且数据也是最新的(事务执行之前的数据),不会影响数据库一致性。
    • RDB 模式:Redis 不会中断事务去执行保存 RDB 的工作,恢复数据库后的数据可能不是最新的,但只要RDB文件本身没有因为其他问题而出错,那么还原后的数据库就是一致的。

Isolation(隔离性)

Redis事务具备了隔离性。

  • EXEC命令执行时,因为Redis 是单进程程序,并且它保证在执行事务时,不会对事务进行中断,事务运行直到执行完所有事务队列中的命令为止。
  • EXEC命令执行前,通过 WATCH 机制保证隔离性。

Durability(持久性)

Redis事务不保证持久性。Redis事务没有提供任何额外的持久性功能,事务的持久性由 Redis 所使用的持久化模式决定:

  • 如果 Redis 没有使用 RDB 或 AOF模式,事务的持久化无法保证。
  • 如果 Redis 使用了RDB 模式,在一个事务执行后,RDB 快照还未执行前发生了故障,事务修改的数据也是不能保证持久化的。
  • 如果 Redis 使用了AOF 模式,AOF 模式的 no、everysec 和 always 都会存在数据丢失的情况,亦不能保证持久化。

参考

https://redisbook.readthedocs.io/en/latest/feature/transaction.html