Redis---事务

标签:Redis首次发布:2023-11-12最近修改:2023-11-12

流水线

在一般情况下,用户每执行一个 Redis 命令,Redis 客户端和 Redis 服务器就需要执行以下步骤:

  1. 客户端向服务器发送命令请求。
  2. 服务器接收命令请求,并执行用户指定的命令调用,然后产生相应的命令执行结果。
  3. 服务器向客户端返回命令的执行结果。
  4. 客户端接收命令的执行结果,并向用户进行展示。

与大多数网络程序一样,执行 Redis 命令所消耗的大部分时间都用在了发送命令请求和接收命令结果上面:Redis 服务器处理一个命令请求通常只需要很短的时间,但客户端将命令请求发送给服务器以及服务器向客户端返回命令结果的过程却需要花费不少时间。通常情况下,程序需要执行的 Redis 命令越多,它需要进行的网络通信操作也会越多,程序的执行速度也会因此而变慢。

为了解决这个问题,可以使用 Redis 提供的流水线特性:这个特性允许客户端把任意多条Redis命令请求打包在一起,然后一次性地将它们全部发送给服务器,而服务器则会在流水线包含的所有命令请求都处理完毕之后,一次性地将它们的执行结果全部返回给客户端。通过使用流水线特性,我们可以将执行多个命令所需的网络通信次数从原来的 N 次降低为 1 次,这可以大幅度地减少程序在网络通信方面耗费的时间,使得程序的执行效率得到显著的提升。

作为例子,图 1 展示了在没有使用流水线的情况下,执行 3 个 Redis 命令产生的网络通信示意图,而图 2 则展示了在使用流水线的情况下,执行相同 Redis 命令产生的网络通信示意图。可以看到,在使用了流水线之后,程序进行网络通信的次数从原来的 3 次降低到了 1 次。

image-20231112153646072 image-20231112153724321

Redis 事务的特性

复习:事务的四个特性

  • Atomicity(原子性):一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被恢复(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
  • Consistency(一致性):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
  • Isolation(隔离性):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交、读提交、可重复读和串行化。
  • Durability(持久性):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

Redis 事务就是一组命令的集合,将多个命令进行打包,然后这些命令会被顺序的添加到队列中,并且按顺序的执行这些命令。

Redis 的事务总是具有 ACID 性质中的 A、C、I 性质:

  • 原子性(Atomic):如果事务成功执行,那么事务中包含的所有命令都会被执行;相反,如果事务执行失败,那么事务中包含的所有命令都不会被执行。
  • 一致性(Consistent):Redis 服务器会对事务及其包含的命令进行检查,确保无论事务是否执行成功,事务本身都不会对数据库造成破坏。
  • 隔离性(Isolate):每个 Redis 客户端都拥有自己独立的事务队列,并且每个 Redis 事务都是独立执行的,不同事务之间不会互相干扰。

除此之外,当 Redis 服务器运行在特定的持久化模式之下时,Redis 的事务也具有 ACID 性质中的 D 性质:

  • 持久性(Durable):当事务执行完毕时,它的结果将被存储在硬盘中,即使服务器在此之后停机,事务对数据库所做的修改也不会丢失。

但是 Redis 的事务没有像 Mysql 那样,执行事务失败会进行回滚操作。

流水线与事务

流水线与事务虽然在概念上有些相似,但是在作用上却并不相同:流水线的作用是将多个命令打包,然后一并发送至服务器,而事务的作用则是将多个命令打包,然后让服务器一并执行它们。 因为 Redis 的事务在 EXEC 命令执行之前并不会产生实际效果,所以很多 Redis 客户端都会使用流水线去包裹事务命令,并将入队的命令缓存在本地,等到用户输入 EXEC 命令之后,再将所有事务命令通过流水线一并发送至服务器,这样客户端在执行事务时就可以达到“打包发送,打包执行”的最优效果。

事务的实现

Redis 事务相关命令:

  • MULTI : 开启事务,redis 会将后续的命令逐个放入队列中,并不会立即执行,然后使用 EXEC 命令来原子化执行这个命令系列。
  • EXEC: 执行事务中的所有操作命令,并返回所有命令的执行结果。
  • DISCARD: 取消事务,放弃执行事务块中的所有命令。
1

事务的异常

1. 语法错误

image-20230425164135613

由于 incrby 命令在放入队列之前就被检测到有缺少参数的语法错误,所以第三条事务命令并不会执行,并且其他事务命令都不能正常执行。

2. 执行异常

image-20230425163509249

由于 incrby 命令并没有缺少参数(没有严格意义上的语法错误),可以将该命令放入队列中。却在执行的过程中出现异常(incrby 的操作数只能是整数),结果就是该异常的事务命令不会被执行,但是其他正常的事务命令可以被执行,不会发生回滚。

总结:

Redis 的事务不支持回滚,可能有人会问上面的第一种情况(事务里面的命令出现语法错误的时候),虽然该情况的结果是:事务中的所有命令并没有执行,但是这并不代表 redis 执行了回滚操作。事实上,Redis 会把要执行的所有事务命令放在一个队列中,在执行这个队列里面的所有命令之前,会去检查是否有语法错误,如果有语法错误,那么 redis 并不会执行队列里面的所有命令,并且还会释放这个队列中的所有命令。所以,对于这种情况,Redis 并没有执行回滚操作。

事务隔离机制

1. 命令

  • WATCH:监视一个或多个 key,如果事务在执行前,这个 key(或多个 key)被其他命令修改,则事务被中断,不会执行事务中的任何命令。
  • UNWATCH:取消 WATCH 对所有 key 的监视。

2. 隔离实现原理

  1. 当某一客户端对 key 执行了 watch 后,系统就会为该 key 添加一个 version 乐观锁,并初始化 version。例如初值为 1.0。
  1. 此后某个客户端 A 将对该 key 的修改语句写入到了事务命令队列中,虽未执行,但其将该 key 的 value 值与 version 进行了读取并保存到了当前客户端缓存。此时读取并保存的是 version 的初值 1.0。

  2. 此后客户端 B 对该 key 的值进行了修改,这个修改不仅修改了 key 的 value 本身,同时也增加了 version 的值,例如使其 version 变为了 2.0,并将该 version 记录到了该 key 信息中。

  3. 此后客户端 A 执行 exec,开始执行事务中的命令。不过,其在执行到对该 key 进行修改的命令时,该命令首先对当前客户端缓存中保存的 version 值与当前 key 信息中的 version 值。如果缓存 version 小于 key 的 version,则说明客户端缓存的 key 的 value 已经过时,该写操作如果执行可能会破坏数据的一致性。所以该写操作不执行。

隔离实现

  1. 客户端 A 得到 source 的数量为 50,于是对 source 进行监控,并开启事务。开启事务后,将 source 的数量减少 40。
image-20230425180137794
  1. 在客户端 A 执行事务之前,客户端 B 将 source 的数量减少 30,并成功返回。
image-20230425180212577
  1. 此时,当客户端 A 执行事务的时候返回结果为 nil,表示事务执行失败。因为在客户端 A 执行事务之前,Redis 检测到 source 的数量已经改变,因此执行失败。需要注意的是:如果客户端 A 在执行事务的时候,还有其他命令,比如 get source、set password "abcd"等命令,那么这些命令会由于 source 的值被客户端 B 改变,导致事务里面的所有命令都不会被执行,这也体现了 redis 事务的原子性。

image-20230425180246907