01-Redis 事务

官网保平安:https://redis.io/

Redis 事务

什么是 Redis 事务?

你可以将 Redis 中的事务理解为:Redis 事务提供了一种将多个命令请求打包的功能。然后,再按顺序执行打包的所有命令,并且不会被中途打断。

Redis 事务实际开发中使用的非常少,功能比较鸡肋,不要将其和我们平时理解的关系型数据库的事务混淆了。

除了不满足原子性和持久性之外,事务中的每条命令都会与 Redis 服务器进行网络交互,这是比较浪费资源的行为。明明一次批量执行多个命令就可以了,这种操作实在是看不懂。

因此,Redis 事务是不建议在日常开发中使用的。

如何使用 Redis 事务?

Redis 事务功能是通过 MULTI、EXEC、DISCARD 和 WATCH 四个原语实现的,Redis 会将一个事务中的所有命令序列化,然后按顺序执行。

  • MULTI:用于开启一个事务,它总是返回 OK。MULTI 执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当 EXEC 命令被调用时,所有队列中的命令才会被执行。
  • EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。当操作被打断时,返回空值 nil
  • DISCARD:通过调用 DISCARD,客户端可以清空事务队列,并放弃执行事务,并且客户端会从事务状态中退出。
  • WATCH:可以为 Redis 事务提供 check-and-set(CAS)行为。可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到执行 EXEC 命令。

注意:

  1. Redis 不支持回滚。Redis 在事务失败时不进行回滚,而是继续执行余下的命令,所以 Redis 的内部可以保持简单且快速。
  2. 如果在一个事务中的命令出现错误,那么所有的命令都不会执行。
  3. 如果在一个事务中出现运行错误,那么正确的命令会被执行。
  4. Redis 中,单条命令是原子性执行的,但事务不保证原子性。
1
2
3
4
5
6
7
8
9
> MULTI
OK
> SET PROJECT "JavaGuide"
QUEUED
> GET PROJECT
QUEUED
> EXEC
1) OK
2) "JavaGuide"

MULTI 命令后可以输入多个命令,Redis 不会立即执行这些命令,而是将它们放到队列,当调用了 EXEC 命令后,再执行所有的命令。

这个过程是这样的:

  1. 开始事务(MULTI);
  2. 命令入队(批量操作 Redis 的命令,先进先出(FIFO)的顺序执行);
  3. 执行事务(EXEC)。

你也可以通过 DISCARD 命令取消一个事务,它会清空事务队列中保存的所有命令。

1
2
3
4
5
6
7
8
> MULTI
OK
> SET PROJECT "JavaGuide"
QUEUED
> GET PROJECT
QUEUED
> DISCARD
OK

你可以通过WATCH 命令监听指定的 Key,当调用 EXEC 命令执行事务时,如果一个被 WATCH 命令监视的 Key 被 其他客户端/Session 修改的话,整个事务都不会被执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 客户端 1
> SET PROJECT "RustGuide"
OK
> WATCH PROJECT
OK
> MULTI
OK
> SET PROJECT "JavaGuide"
QUEUED

# 客户端 2
# 在客户端 1 执行 EXEC 命令提交事务之前修改 PROJECT 的值
> SET PROJECT "GoGuide"

# 客户端 1
# 修改失败,因为 PROJECT 的值被客户端2修改了
> EXEC
(nil)
> GET PROJECT
"GoGuide"

不过,如果 WATCH事务 在同一个 Session 里,并且被 WATCH 监视的 Key 被修改的操作发生在事务内部,这个事务是可以被执行成功的(相关 issue:WATCH 命令碰到 MULTI 命令时的不同效果)。

事务内部修改 WATCH 监视的 Key:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
> SET PROJECT "JavaGuide"
OK
> WATCH PROJECT
OK
> MULTI
OK
> SET PROJECT "JavaGuide1"
QUEUED
> SET PROJECT "JavaGuide2"
QUEUED
> SET PROJECT "JavaGuide3"
QUEUED
> EXEC
1) OK
2) OK
3) OK
127.0.0.1:6379> GET PROJECT
"JavaGuide3"

事务外部修改 WATCH 监视的 Key:

1
2
3
4
5
6
7
8
9
10
11
12
> SET PROJECT "JavaGuide"
OK
> WATCH PROJECT
OK
> SET PROJECT "JavaGuide2"
OK
> MULTI
OK
> GET USER
QUEUED
> EXEC
(nil)

Redis 官网相关介绍 https://redis.io/topics/transactions 如下:

Redis 事务

Redis 事务支持原子性吗?

Redis 的事务和我们平时理解的关系型数据库的事务不同。我们知道事务具有四大特性:

  1. 原子性(Atomicity): 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
  2. 隔离性(Isolation): 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
  3. 持久性(Durability): 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
  4. 一致性(Consistency): 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的;

Redis 事务在运行错误的情况下,除了执行过程中出现错误的命令外,其他命令都能正常执行。并且,Redis 事务是不支持回滚(roll back)操作的。因此,Redis 事务其实是不满足原子性的。

Redis 官网也解释了自己为啥不支持回滚。简单来说就是 Redis 开发者们觉得没必要支持回滚,这样更简单便捷并且性能更好。Redis 开发者觉得即使命令执行错误也应该在开发过程中就被发现而不是生产过程中。

Redis 为什么不支持回滚

相关 issue :

Redis 事务支持持久性吗?

Redis 不同于 Memcached 的很重要一点就是,Redis 支持持久化,而且支持 3 种持久化方式:

  • 快照(snapshotting,RDB)
  • 只追加文件(append-only file, AOF)
  • RDB 和 AOF 的混合持久化(Redis 4.0 新增)

与 RDB 持久化相比,AOF 持久化的实时性更好。在 Redis 的配置文件中存在三种不同的 AOF 持久化方式(fsync策略),它们分别是:

1
2
3
appendfsync always    # 每次有数据修改发生时都会调用fsync函数同步AOF文件,fsync完成后线程返回,这样会严重降低Redis的速度
appendfsync everysec # 每秒钟调用fsync函数同步一次AOF文件
appendfsync no # 让操作系统决定何时进行同步,一般为30秒一次

AOF 持久化的 fsync 策略为 no、everysec 时都会存在数据丢失的情况 。always 下可以基本是可以满足持久性要求的,但性能太差,实际开发过程中不会使用。

因此,Redis 事务的持久性也是没办法保证的。

如何解决 Redis 事务的缺陷?

Redis 从 2.6 版本开始支持执行 Lua 脚本,它的功能和事务非常类似。我们可以利用 Lua 脚本来批量执行多条 Redis 命令,这些 Redis 命令会被提交到 Redis 服务器一次性执行完成,大幅减小了网络开销。

一段 Lua 脚本可以视作一条命令执行,一段 Lua 脚本执行过程中不会有其他脚本或 Redis 命令同时执行,保证了操作不会被其他指令插入或打扰。

不过,如果 Lua 脚本运行时出错并中途结束,出错之后的命令是不会被执行的。并且,出错之前执行的命令是无法被撤销的,无法实现类似关系型数据库执行失败可以回滚的那种原子性效果。因此, 严格来说的话,通过 Lua 脚本来批量执行 Redis 命令实际也是不完全满足原子性的。

如果想要让 Lua 脚本中的命令全部执行,必须保证语句语法和命令都是对的。

另外,Redis 7.0 新增了 Redis functions 特性,你可以将 Redis functions 看作是比 Lua 更强大的脚本。

Redis 事务他人的一些看法

有人会说,不对啊 Redis 的事务不能保证所有操作要么都执行要么都不执行,为什么它也叫事务啊?

首先你要知晓一般的中间件都会夸大其效果,人家团队也是想更出名,吸引更多的人来使用他们的产品,所以我们得以辩证的角度来看待。

一般而言他们既然敢说出他们实现了什么什么,要么是真的实现了,要么是在某种特殊、定制或者极端的条件下才能满足功能。

我们来看看 Redis 怎么说的。

It's important to note that even when a command fails, all the other commands in the queue are processed – Redis will not stop the processing of commands.

这句话意思是 事务中的某个命令失败了,之后的命令还是会被处理,Redis 不会停止命令,意味着也不会回滚

你说这不是扯嘛?这都偏离事务最核心的本意了啊。别急,咱们来看看 Redis 怎么解释的。

Redis commands can fail only if called with a wrong syntax (and the problem is not detectable during the command queueing), or against keys holding the wrong data type: this means that in practical terms a failing command is the result of a programming errors, and a kind of error that is very likely to be detected during development, and not in production.
Redis is internally simplified and faster because it does not need the ability to roll back.

Redis 官网解释了为什么不支持回滚,他们说首先如果命令出错那都是语法使用错误,是你们自己编程出错,而且这种情况应该在开发的时候就被检测出来,不应在生产环境中出现。

然后 Redis 就是为了快!不需要提供回滚。下面还有一段话,就是说就算提供回滚也没用,你这代码都写错了,回滚并不能使你免于编程错误。而且一般这种错也不可能进入到生产环境,所以选择更加简单、快速的方法,我们不支持回滚。

你看看这说的好像很有道理,我们不提供回滚,因为我们不需要为你的编程错误买单!

但好像哪里不对劲?角度、立场不同,大家自己品。

Redis 事务到底该不该用

不推荐使用 Redis 的事务机制。因为我们的生产环境,基本都是 Redis 集群环境,做了数据分片操作。你一个事务中有涉及到多个 key 操作的时候,这多个 key 不一定都存储在同一个 redis-server 上。因此,Redis 的事务机制,十分鸡肋。

Redis 事务失败场景

有三种类型的失败场景:

  1. 在事务提交之前,客户端执行的命令缓存(队列)失败,比如命令的语法错误(命令参数个数错误,不支持的命令等等)。如果发生这种类型的错误,Redis 将向客户端返回包含错误提示信息的响应,同时 Redis 会清空队列中的命令并取消事务。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    127.0.0.1:6379> set name xiaoming # 事务之前执行
    OK
    127.0.0.1:6379> multi # 开启事务
    OK
    127.0.0.1:6379> set name zhangsan # 事务中执行,命令入队列
    QUEUED
    127.0.0.1:6379> setset name zhangsan2 # 错误的命令,模拟失败场景
    (error) ERR unknown command `setset`, with args beginning with: `name`, `zhangsan2`,
    127.0.0.1:6379> exec # 提交事务,发现由于上条命令的错误导致事务已经自动取消了
    (error) EXECABORT Transaction discarded because of previous errors.
    127.0.0.1:6379> get name # 查询name,发现未被修改
    "xiaoming"
  2. 事务提交后开始顺序执行命令,之前缓存在队列中的命令有可能执行失败。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    127.0.0.1:6379> multi # 开启事务
    OK
    127.0.0.1:6379> set name xiaoming # 设置名字
    QUEUED
    127.0.0.1:6379> set age 18 # 设置年龄
    QUEUED
    127.0.0.1:6379> lpush age 20 # 此处仅检查是否有语法错误,不会真正执行
    QUEUED
    127.0.0.1:6379> exec # 提交事务后开始顺序执行命令,第三条命令执行失败
    1) OK
    2) OK
    3) (error) WRONGTYPE Operation against a key holding the wrong kind of value
    127.0.0.1:6379> get name # 第三条命令失败没有将前两条命令回滚
    "xiaoming"
  3. 由于乐观锁失败,事务提交时将丢弃之前缓存的所有命令序列。通过开启两个redis客户端并结合watch命令模拟这种失败场景。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    # 客户端1
    127.0.0.1:6379> set name xiaoming # 客户端1设置name
    OK
    127.0.0.1:6379> watch name # 客户端1通过watch命令给name加乐观锁
    OK
    # 客户端2
    127.0.0.1:6379> get name # 客户端2查询name
    "xiaoming"
    127.0.0.1:6379> set name zhangsan # 客户端2修改name值
    OK
    # 客户端1
    127.0.0.1:6379> multi # 客户端1开启事务
    OK
    127.0.0.1:6379> set name lisi # 客户端1修改name
    QUEUED
    127.0.0.1:6379> exec # 客户端1提交事务,返回空
    (nil)
    127.0.0.1:6379> get name # 客户端1查询name,发现name没有被修改为lisi
    "zhangsan"

    在事务过程中监控的key被其他客户端改变,则当前客户端的乐观锁失败,事务提交时将丢弃所有命令缓存队列。


01-Redis 事务
https://flepeng.github.io/interview-41-数据库-41-Redis-01-Redis-事务/
作者
Lepeng
发布于
2020年8月8日
许可协议