深入浅出 Redis 事务


深入浅出 Redis 事务

事务大家都熟悉吧,要么全部执行,要么全部不执行,今天我们就来讲讲 Redis 的事务🏃🏃🏃

一、事务基本使用

在讲解事务的原理之前,我们先来看看 Redis 事务是如何使用的吧

事务在其他语言中,一般分为以下三个阶段:

  • 开启事务——Begin Transaction
  • 执行业务代码,提交事务——Commit Transaction
  • 业务处理中出现异常,回滚事务——Rollback Transaction

但是,Redis 中的事务是不是也是如此呢?🤭🤭🤭

Redis 中的事务从开始到结束也是要经历三个阶段:

  • 开启事务
  • 命令入列
  • 执行事务/放弃事务

其中,开启事务使用 multi 命令,事务执行使用 exec 命令,放弃事务使用 discard 命令。

1、开启事务

multi 命令用于开启事务,实现代码如下:

> multi
OK

multi 命令可以让客户端从非事务模式状态,变为事务模式状态,如下图所示:

image-20220327120853267>

小贴士:multi 命令不能嵌套使用,如果已经开启了事务的情况下,再执行 multi 命令,会提示如下错误:

(error) ERR MULTI calls can not be nested

当客户端是非事务状态时,使用 multi 命令,客户端会返回结果 OK,如果客户端已经是事务状态,再执行 multi 命令会发生不能嵌套的错误,但不会终止客户端为事务的状态,如下图所示:

image-20220327120912865

2、命令入队

客户端进入事务状态之后,执行的所有常规 Redis 操作命令(非触发事务执行或放弃和导致入列异常的命令)会依次入列,命令入列成功后会返回 QUEUED,此时命令并不会立即执行,如下代码所示:

> multi
OK
> set k v
QUEUED
> get k
QUEUED

执行流程如下图所示:

image-20220327120918226

小贴士:命令会按照先进先出(FIFO)的顺序出入列,也就是说事务会按照命令的入列顺序,从前往后依次执行。

3、执行事务/放弃事务

执行事务的命令是 exec,放弃事务的命令是 discard

执行事务示例代码如下:

> multi
OK
> set k v2
QUEUED
> exec
1) OK
> get k
"v2"

放弃事务示例代码如下:

> multi
OK
> set k v3
QUEUED
> discard
OK
> get k
"v2"

执行流程如下图所示:

image-20220327120923456

二、事务错误&回滚

事务执行中的错误分为以下三类:

  • 执行时才会出现的错误(简称:执行时错误);
  • 入列时错误,不会终止整个事务;
  • 入列时错误,会终止整个事务。

执行时错误

示例代码如下:

> get k  # 查询 key 原值
"v"
> multi  # 开启事务
OK
> set k v2  # 设置新值 v2
QUEUED
> expire k 10s  # 设置错误命令,因为 expire 只支持数字
QUEUED
> exec  # 执行事务
1) OK
2) (error) ERR value is not an integer or out of range
> get k  # 查询事务中修改的值,部分已成功执行
"v2"

从以上结果可以看出,即使事务队列中某个命令在执行期间发生了错误,事务也会继续执行,直到事务队列中所有命令执行完成。

入列错误不会导致事务结束

示例代码如下:

> get k # 查询 key 原值
"v"
> multi # 开启事务
OK
> set k v2   # 设置新值 v2
QUEUED
> multi  # 发生错误
(error) ERR MULTI calls can not be nested
> exec  # 执行事务
1) OK
> get k
"v2"  # 事务执行成功

可以看出,重复执行 multi 会导致入列错误,但不会终止事务,最终查询的结果是事务执行成功了。除了重复执行 multi 命令,还有在事务状态下执行 watch 也是同样的效果

入列错误导致事务结束

示例代码如下:

> get k # 查询 key 原值
"v2"
> multi # 开启事务
OK
> set k v3  # 设置新值 v3
QUEUED
> set k  # 入队命令错误,没有 value 值
(error) ERR wrong number of arguments for 'set' command
> exec
(error) EXECABORT Transaction discarded because of previous errors.
> get k  # 查询缓存,事务未正常执行
"v2"

为什么不支持事务回滚?

其实,Redis 中并没有提供回滚机制。虽然 Redis 提供了 DISCARD 命令,但是这个命令只能用来主动放弃事务执行,把暂存的命令队列清空,起不到回滚的效果

  • Redis 作者认为 Redis 事务的执行时,错误通常都是编程错误造成的,这种错误通常只会出现在开发环境中,而很少会在实际的生产环境中出现,所以他认为没有必要为 Redis 开发事务回滚功能;
  • 不支持事务回滚是因为这种复杂的功能和 Redis 追求的简单高效的设计主旨不符合。

这里不支持事务回滚,指的是不支持运行时错误的事务回滚。

三、监控

watch 命令用于客户端并发情况下,为事务提供一个乐观锁(CAS,Check And Set,相信大家学Java并发编程的时候学过了),也就是可以用 watch 命令来监控一个或多个变量,如果在事务的过程中,某个监控项被修改了,那么整个事务就会终止执行

watch 基本语法如下:

watch key [key ...]

watch 示例代码如下:

> watch k
OK
> multi
OK
> set k v2
QUEUED
> exec
(nil)
> get k
"v"

小贴士:以上事务在执行期间,也就是开启事务(multi)之后,执行事务(exec)之前,模拟多客户端并发操作了变量 k 的值,这个时候再去执行事务,才会出现如上结果,exec 执行的结果为 nil

注意:即使把原对象的值重新赋值给了原对象,这个时候 watch 命令也会认为监控对象还是被修改了。

可以看出,当执行 exec 返回的结果是 nil 时,表示 watch 监控的对象在事务执行的过程中被修改了。从 get k 的结果也可以印证,因为事务中设置的值 set k v2 并未正常执行。

执行流程如下图所示:

image-20220327120930745

小贴士: watch 命令只能在客户端开启事务之前执行,在事务中执行 watch 命令会引发错误,但不会造成整个事务失败,如下代码所示:

> multi   # 开启事务
OK
> set k v3
QUEUED
> watch k
(error) ERR WATCH inside MULTI is not allowed  # 引发错误
> exec
1) OK
> get k  # watch 报错并不影响其他语句执行
"v3"

unwatch 命令用于清除所有之前监控的所有对象(键值对)

unwatch 示例如下所示:

> set k v
OK
> watch k
OK
> multi
OK
> unwatch
QUEUED
> set k v2
QUEUED
> exec
1) OK
2) OK
> get k
"v2"

可以看出,即使在事务的执行过程中,k 值被修改了,因为调用了 unwatch 命令,整个事务依然会顺利执行。

四、事务的 ACID 属性

以上都是讲了 Redis 事务的基本使用;事务嘛,肯定是离不开 ACID 属性的,即 原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。现在就让我们开始讲 Redis 中事务的 ACID 属性吧🤣🤣🤣

先简单介绍一下各属性都是啥意思:

原子性: 就是一个事务中的多个操作必须都完成,或者都不完成

一致性:就是指数据库中的事务执行前后都是一致的

隔离性:它要求数据库在执行一个事务时,其他操作无法存取到正在还行事务访问的数据

持久性:数据执行事务后,数据的修改要被持久化保存下来,当数据重启后,数据的值需要是被修改后的值

1、原子性

这里需要分三种情况考虑:

第一种情况是,在执行 EXEC 命令前,客户端发送的操作命令本身就有错误(比如语法错误,使用了不存在的命令)在命令入队时就被 Redis 实例判断出来了

对于这种情况,在命令入队时,Redis 就会报错并且记录下这个错误,此时我们还可以继续提交命令操作。等到执行 EXEC 命令之后, Redis 就会拒绝所有提交的命令操作,返回事务失败的结果。这样,事务中的所有命令都不会在执行了,从而保证了原子性

第二种情况是:事务操作入队时,命令和操作的数据类型不匹配,但是 Redis 实例没有检测出错误,当我们执行完 EXEC 命令以后,Redis 就行执行队列中的所有命令,发现命令有错,就会报错。但是此时 Redis 还是会把正确的命令执行完,所以此时并不会保证事务的原子性

第三种情况是:在执行事务的 EXEC 命令时,Redis 实例发生了故障,导致事务执行失败

假如此时我们的 Redis 开启了 AOF 日志,那么有部分事务被记录到 AOF 日志中,我们可以把已经完成的事务从 AOF 去除,这样,当我们使用 AOF 恢复实例时,事务操作不会再执被执行,从而保证原子性

假如你的 AOF 日志没有开启,实例重启后,数据也都没法恢复了,此时更加谈不上原子性了

2、一致性

事务的一致性保证会受到错误命令、实例故障的影响,所以我们按照命令出错和实例故障的发生时机,分情况:

情况一:命令入队时就报错

此时事务本身就会被放弃执行,所以可以保证数据的一致性

情况二:命令入队时没有报错,实际执行时报错

此时有错误的命令不会被执行,正确的命令可以正常执行,也不会改变数据库的一致性

情况三:EXEC 命令执行时实例发生故障

此时实例故障会进行重启,这和恢复数据的方式有关,分为 RDB 和 AOF 两种情况:

如果没有开始 RDB 和 AOF,那么实例故障重启后,数据都没有了,数据库是一致的

如果使用了 RDB 快照,因为 RDB 快照不会在事务执行时执行,所以事务命令操作的结果不会保存到 RDB 快照中,使用 RDB 快照进行恢复是,数据库里的数据也是一致的

如果使用 AOF 日志,加入事务操作还没有记录到 AOF 日志时,实例就发生了故障,那么使用 AOF 日志恢复的数据库数据是一致的。加入有部分数据记录到了 AOF, 我们可以使用 redis-check-aof 清除事务中已经完成的操作,数据库恢复后也是一致的

总的来说,在命令执行错误或 Redis 发送故障时,Redis 事务机制对一致性是由保证的

3、隔离性

隔离性也分两种情况进行说明:

并发操作在 EXEC 命令前执行,此时,隔离性的保证要使用 WATCH 机制实现,否则隔离性无法保证

相信大家对 WATCH 机制不陌生了吧(上文已经讲的很清楚了😅😅😅),当并发操作在 EXEC 命令前执行时,事务的命令操作都是存储在命令队列中的,如果在事务执行之前,开启了 WATCH 机制,假如其他的客户端修改了 key,此时就会放弃事务的执行。当客户端再次执行事务时,假如 key 没有被修改,事务就能正常执行,从而保证隔离性。

假如没有开启 WATCH 机制,被修改的操作还是会执行,此时隔离性不会得到保障

并发操作在 EXEC 命令后执行,此时隔离性可以保证

因为 Redis 是单线程执行命令,在执行 EXEC 命令后,Redis 会保证把命令队列中的所有命令执行完。所以,此时并发操作不会破坏事务的隔离性

4、持久性

因为 Redis 是内存数据库,所以,数据是否持久化保存完全取决于 Redis 的持久化配置模式

如果 Redis 没有使用 RDB 或 AOF,那么事务的持久化属性肯定得不到保证,如果 Redis 使用了 RDB 模式,那么,在一个事务执行后,而下一次的 RDB 快照还为执行前,如果发生了宕机,这种情况下,事务修改的数据也是不能保证持久化的

如果 Redis 使用了 AOF 模式,因为 AOF 模式的三种配置选项 noeverysecalways都会存在数据丢失的情况,所以,事务的持久性属性也还是得不到保证

所以,不管 Redis 采用什么持久化模式,事务的持久性属性是得不到保证的

五、小结

以上讲了这么多,相信大家都看累了,那好,我们现在来个总结吧🥰🥰🥰

事务为多个命令提供一次性按顺序执行的机制,与 Redis 事务相关的命令有以下五个:

  • multi:开启事务
  • exec:执行事务
  • discard:丢弃事务
  • watch:为事务提供乐观锁实现
  • unwatch:取消监控(取消事务中的乐观锁)

正常情况下 Redis 事务分为三个阶段:开启事务、命令入列、执行事务。Redis 事务并不支持运行时错误的事务回滚,但在某些入列错误,如 set key 或者是 watch 监控项被修改时,提供整个事务回滚的功能。

Redis 的 ACID 讲的也很清楚了,如果有其他问题,其实我也不会🤪🤪🤪

巨人的肩膀:

http://learn.lianglianglee.com/%E4%B8%93%E6%A0%8F/Redis%20%E6%A0%B8%E5%BF%83%E5%8E%9F%E7%90%86%E4%B8%8E%E5%AE%9E%E6%88%98/16%20Redis%20%E4%BA%8B%E5%8A%A1%E6%B7%B1%E5%85%A5%E8%A7%A3%E6%9E%90.md

https://time.geekbang.org/column/intro/100056701


文章作者: Gtwff
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Gtwff !
  目录