浅谈 Redis 事务特性和使用

kenticny

随便一个接触过数据库的人肯定都知道“事务”,虽然各种数据库的事务多多少少都有一些区别,但是大体的概念都是相同的,那么当你在其他的应用中再次听到“事务”的时候,很容易就将你已经了解的概念套用进去了,那么这时候很有可能就会造成问题了,比如在 Redis 中,很容易把 Redis 事务理解成和 ACID 事务一样,如果真的这样理解,那么可能就会有很多问题了,所以我们应该养成一个多看文档的好习惯。

Redis 事务简介

和其他数据库的事务类似,Redis 的事务执行也是分为三个步骤,第一步是启动事务,第二步是将要执行的命令加入队列,(注意,这里是将命令加入到队列,并不是立即执行命令),第三步执行事务。下面展示的就是一个 Redis 事务的执行过程:

1
2
3
4
5
6
7
8
9
> MULTI      // 启动事务
OK
> SET a 1 // 加入命令
QUEUED
> SET b 2
QUEUED
> EXEC
1) OK
2) OK

在这个事务中,通过 MULTI 命令启动事务,然后加入两个 set 命令到队列中,然后通过 EXEC 命令执行事务,我们可以得到两个 OK 表示队列中的两个命令的执行结果。

这是一个最简单的事务的执行过程,Redis 中共提供了五个和事务相关的操作命令,除了这个例子中我们使用到的 MULTI 和 EXEC,还有 DISCARD 取消事务,WATCH 监控key,UNWATCH 取消监控。那么这些命令分别在什么场景下使用呢?

我们先来说一说 WATCH 和 UNWATCH 命令,它的作用为监控一个或者多个 key 的值是否变化,如果在监控之间发生变化,那么事务则无法执行。UNWATCH 则为取消对所有 key 的监控。下面举例看下 WATCH 的使用:

1
2
3
4
5
6
7
8
9
10
> WATCH a
OK
> MULTI
OK
> SET a 1
QUEUED
> ............................. set a 2 // 在另外的线程中操作
> ............................. OK
> EXEC
nil

在这个例子中,在开启事务之前 WATCH 了 a 的值,然后在开启事务后,在另一个线程中设置 a 的值,然后返回事务后执行事务,结果为 nil,说明事务没有被执行,因为 a 的值在 WATCH 之后发生了变化,所以事务被取消了。这里要提一点,这里和开启事务的时间点没有关系,只要是在 WATCH 之后发生了变化,无论事务是否已经开启,执行事务的时候都会取消。而且执行 EXEC 和 DISCARD命令,都会默认执行 UNWATCH。

DISCARD 命令作用为取消事务,即将事务中已经入队列的命令移除,将 Redis 链接状态恢复为开启事务之前。同样通过一个例子来演示:

1
2
3
4
5
6
7
8
> MULTI
OK
> SET a 1
QUEUED
> SET b 2
QUEUED
> DISCARD
OK

在这个例子中,开启事务后,入队两个 SET 命令,然后执行 DISCARD 取消事务,此时,a 和 b 的值都未改变。

Redis 事务和 ACID 事务区别

看了上一节的内容,新接触 Redis 事务的同学肯定就回想,这不是和关系数据库的事务差不多吗?那么这一节就主要说说 Redis 事务的特(另)点(类)。

首先,先把 Redis 文档中介绍事务的两个特点贴出来:

  1. 事务中的所有命令会按照顺序执行,而且在执行过程中,不会有其他的命令插入,保证事务中的命令都是单独的操作。

  2. 事务中的所有命令要么全部执行,要么都不做处理。

看起来好像就是 ACID 事务中原子性,如果真的这么想你就上当了。那么 Redis 事务满足事务的原子性吗?看上面的两个特点,要么全部执行、要么全不执行,而且在执行中不会被其他命令插入,看起来好像是满足的!然而事实并非如此。我们先看一个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
> MULTI
OK
> SET a 1
QUEUED
> SET b aaa
QUEUED
> INCR b
QUEUED
> EXEC
1) OK
2) OK
3) (error) ERR value is not an integer or out of range

我们直接看这个事务的执行结果,三条命令有两条执行成功返回 OK,第三条命令由于我们给一个非数值的对象做增加操作,所以报出了一个错误。那么这时候我们再执行 GET a 或者 GET b,发现这两个值已经被成功设置了。看到这里,大家可能就要问了,说好的原子性呢?不是说要么全部执行,要么全不执行吗?如果我告诉你人家确实都全部执行了,只不过有一条命令执行报错了而已!哈哈哈。

回到本文开头的问题,“Redis 事务里面有一步出错了,怎么才能回滚其他的操作啊!”,很遗憾,Redis 的事务没有回滚操作。但是这里我们谈一谈在特殊情况下类似回滚的操作。

上面事务中报错的例子,错误是在 EXEC 阶段产生的,这种类型的错误会导致报错的命令返回错误,而其他的命令正常执行。在 Redis 事务中还有一种错误,就是在 EXEC 之前产生的错误,比如命令名称错误,命令参数错误等等,这些错误都可以在 EXEC 之前检查出来,所以在发生这些错误的时候,事务会被取消,事务中的所有命令都不会被执行,这样看起来是不是就有点像回滚了。

另一种情况,在事务中执行 DISCARD 命令,也可以取消所有命令的执行。比如在检查业务逻辑的时候发现需要回滚,如果此时还没有执行 EXEC,那么执行 DISCARD 则会取消所有操作,又有点像回滚了。

另外,Redis 是支持 LUA 脚本的,而在执行脚本的时候也是事务性的,所以大家如果真的需要更为完善的事务操作,推荐使用 LUA 脚本去实现。

至于为什么 Redis 的事务不支持回滚操作呢?下面是 Redis 官方的说法:

“Redis 事务中只有在命令语法出现错误,或者执行的操作和数据类型不一致的时候才会导致错误,而这些错误都是在编程中人为产生的,是可以避免的。而且 Redis 简单和高效也导致 Redis 事务不适合支持回滚操作。”

说到事务,还有个不得不提的概念就是事务隔离,在关系数据库中这个概念大家应该都了解,那么在 Redis 事务中,是否有事务隔离的概念呢?答案是,没有。那么,在使用 Redis 事务的时候会有事务隔离的需求吗?当然也会有,只不过不需要和关系数据库同样的复杂的事务隔离等级区分,因为 Redis 在执行命令时是单线程的(即使 Redis 6.0 增加多线程特性,大部分的数据操作的命令还是单线程的),而且事务中的命令都是在提交的时候一次性执行的,所以并不需要考虑在多线程并发的情况下事务隔离的情况。

那么我们就可以使用已有的命令来实现简单的类似事务隔离特性的功能,主要思路就是使用 WATCH 监控事务中需要操作的值,以保证事务操作前后所监控的值不发生变化,或者发生变化以后中断事务操作。

以上就是对于 Redis 事务的使用和特性的一些基础知识,之后我会在针对 Redis 事务从源码的角度进行原理分析。

  • 本文标题:浅谈 Redis 事务特性和使用
  • 本文作者:kenticny
  • 创建时间:2018-03-27 20:46:57
  • 本文链接:https://luyun.io/2018/03/27/redis-transactions-basic/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
 评论
此页目录
浅谈 Redis 事务特性和使用