在学习关系型数据库时,有一个非常重要的概念——事务,它扮演着关键的角色。它确保了数据操作的完整性、一致性、隔离性和持久性。

那么你有没有想过,非关系型数据库 Redis 是如何处理并使用事务的呢?是否与关系型数据库一一致?又是否能保证 ACID?

为了解开这些疑问,也为了能更加熟练地掌握 Redis,成为一个老司机。今天,我们就来学习 Redis 事务相关的内容。

什么是Redis事务?

Redis 事务的本质是一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。

总结说:Redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。

事务的基本使用

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

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

以 Java 中的事务执行为例:

1
2
3
4
5
6
7
8
9
10
// 开启事务
begin();
try {
//......
// 提交事务
commit();
} catch(Exception e) {
// 回滚事务
rollback();
}

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

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

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

下面一一介绍关于事务的命令。

MULTI:聚会的开场白

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

1
2
> multi
OK

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

img

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

1
2
3
4
127.0.0.1:6379> multi
OK
127.0.0.1:6379> multi
(error) ERR MULTI calls can not be nested

img

常规命令:聚会人员入场

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

1
2
3
4
5
6
> multi
OK
> set k v
QUEUED
> get k
QUEUED

执行流程如下图所示:

img

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

EXEC:行动的高潮

执行事务的命令是 exec,输入 exec 后会执行开启事务后的所有操作。执行事务示例代码如下:

1
2
3
4
5
6
7
8
> multi
OK
> set k v2
QUEUED
> exec
1) OK
> get k
"v2"

img

DISCARD:优雅的撤退

放弃事务的命令是 discard,有时候,事情并不像你预想的那样发展,你决定取消事务。DISCARD就是你的优雅撤退,所有的准备工作都被抛之脑后,大家都当做什么都没发生过一样。

放弃事务示例代码如下:

1
2
3
4
5
6
7
8
> multi
OK
> set k v3
QUEUED
> discard
OK
> get k
"v2"

具体的执行流程和 exec 一致。

WATCH:秘密特工的侦查

WATCH就是你在事务前的小心侦查,确保一切都在你的掌控之中。如果有人试图偷偷溜进来搞破坏,WATCH会立刻发出警报,保护你的数据不被篡改。

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

watch 基本语法如下:

1
watch key [key ...]

watch 示例代码如下:

1
2
3
4
5
6
7
8
9
10
> watch k
OK
> multi
OK
> set k v2
QUEUED
> exec
(nil)
> get k
"v"

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

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

执行流程如下图所示:

img

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

1
2
3
4
5
6
7
8
9
10
> multi
OK
> set k v3
QUEUED
> watch k
(error) ERR WATCH inside MULTI is not allowed
> exec
1) OK
> get k
"v3"

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

unwatch 示例如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
> 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 命令,整个事务依然会顺利执行。

举个例子

以下是事务在 Java 中的使用,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;

public class TransactionExample {
public static void main(String[] args) {
// 创建 Redis 连接
Jedis jedis = new Jedis("xxx.xxx.xxx.xxx", 6379);
// 设置 Redis 密码
jedis.auth("xxx");
// 设置键值
jedis.set("k", "v");
// 开启监视 watch
jedis.watch("k");
// 开始事务
Transaction tx = jedis.multi();
// 命令入列
tx.set("k", "v2");
// 执行事务
tx.exec();
System.out.println(jedis.get("k"));
jedis.close();
}
}

事务出现错误的处理

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

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

执行时出错

执行命令解释如下图所示:

img

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

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

执行命令解释如下图所示:

img

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

入列错误导致事务结束

执行命令解释如下图所示:

img

当然可以!让我们详细探讨一下Redis事务的局限性,揭开这种“完美的幻觉”,看看有哪些实际中的限制和注意事项。

Redis事务的局限:完美的幻觉?

Redis事务通过MULTIEXECWATCHDISCARD等命令实现了一定程度上的事务处理。然而,与传统关系型数据库的事务机制相比,Redis事务仍然存在一些局限性。

乐观锁的陷阱

概念: Redis通过WATCH命令实现乐观锁。WATCH命令用于监视一个或多个键,当事务执行期间,如果这些键发生变化(例如被其他客户端修改),则事务会失败。

局限性:

  • 冲突频繁: 在高并发环境下,如果监视的键频繁被修改,事务成功的概率会降低,需要多次重试。
  • 数据竞争: 乐观锁适用于数据冲突较少的场景,对于高冲突的场景可能并不合适。

缺乏回滚机制

概念: 在传统关系型数据库中,如果事务中的某个操作失败,可以回滚事务,撤销已经执行的操作。但是在Redis中,一旦执行EXEC命令,所有命令都会被依次执行,不支持部分回滚。

局限性:

  • 不可中断: 一旦事务开始执行,无法中途停止或回滚已经成功的操作。
  • 操作不可逆: 如果事务中的某个操作失败,必须手动处理恢复或补救措施,不能依赖自动回滚。

更加深入的理解

为什么 Redis 不支持回滚?

如果你有使用关系式数据库的经验, 那么 “Redis 在事务失败时不进行回滚,而是继续执行余下的命令”这种做法可能会让你觉得有点奇怪。

以下是这种做法的优点:

  • Redis 命令只会因为错误的语法而失败(并且这些问题不能在入队时发现),或是命令用在了错误类型的键上面:这也就是说,从实用性的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中。
  • 因为不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速。

有种观点认为 Redis 处理事务的做法会产生 bug , 然而需要注意的是, 在通常情况下, 回滚并不能解决编程错误带来的问题。 举个例子, 如果你本来想通过 INCR 命令将键的值加上 1 , 却不小心加上了 2 , 又或者对错误类型的键执行了 INCR , 回滚是没有办法处理这些情况的。

如何理解 Redis 与事务的 ACID?

一般来说,事务有四个性质称为ACID,分别是原子性,一致性,隔离性和持久性。这是基础,但是很多文章对Redis 是否支持ACID有一些异议,我觉的有必要梳理下:

  • 原子性 Atomicity

首先通过上文知道 运行期的错误是不会回滚的,很多文章由此说Redis事务违背原子性的;而官方文档认为是遵从原子性的。

Redis官方文档给的理解是,Redis的事务是原子性的:所有的命令,要么全部执行,要么全部不执行。而不是完全成功。

  • 一致性 Consistency

Redis事务可以保证命令失败的情况下得以回滚,数据能恢复到没有执行之前的样子,是保证一致性的,除非Redis进程意外终结。

  • 隔离性 Isolation

Redis事务是严格遵守隔离性的,原因是Redis是单进程单线程模式(v6.0之前),可以保证命令执行过程中不会被其他客户端命令打断。

但是,Redis不像其它结构化数据库有隔离级别这种设计。

  • 持久性 Durability

Redis事务是不保证持久性的,这是因为Redis持久化策略中不管是RDB还是AOF都是异步执行的,不保证持久性是出于对性能的考虑。

关于 Redis 事务能不能保证 ACID这一问题,我在网上也是众说纷纭,但是在 一致性隔离性 上,大家都保持着相同的看法,那就是能保证。

总结

以上就是关于 Redis 事务的全部内容了,内容并不多,没有在学习 MySQL 时的海量内容,也可以看出其实在 Redis 中并不会很频繁的使用到事务,算一个小知识点吧。

我是怎么得出这个结论的?因为小林coding写的八股文中都找不到和事务相关的内容,所以我断言它不重要。

前天晚上死活睡不着,熬了个通宵之后直接来了一个说走就走的旅行,岳阳还是很好玩的。不得不说,晚上确实是一个容易冲动消费的时间,纠结了两周的耳机还是买了,不过不知道为什么没有之前买东西的那种期待感了。

这里就不得不吐槽一下京东了,太呆瓜了,我在晚上冲动消费后,中午觉得这样不行所以决定退了,但是思考两分钟后又觉得得买,所以我就取消了我的取消申请,客服同意后,快递竟然还是被退回来。我联系客服,客服跟我说没办法了。然后我就又申请了退款,结果快递又开始配送了,不得不联系美女客服帮我退款。京东找个真人客服是真难。害的我还得再等一天。

昨天高考开始,竟然没刷到几个丢准考证和身份证的,看来今年的学生学聪明了。

参考

redis 事务能保证ACID吗

Redis 事务

Redis 事务解析

Redis事务深度解析