序言
对于数据库事务而言,通常包含了一个序列的对数据库的读/写操作,其主要作用有两个:
- ① 为数据库操作序列提供了一个从失败中恢复到正常状态的方法,同时提供了数据库即使在异常状态下仍能保持一致性的方法。
- ② 当多个应用程序在并发访问数据库时,可以在这些应用程序之间提供一个隔离方法,以防止彼此的操作互相干扰。
当事务被提交给了数据库管理系统(DBMS),则 DBMS 需要确保该事务中的所有操作都成功完成且其结果被永久保存在数据库中,若事务中有的操作没有成功完成,则事务中的所有操作都需要回滚),回到事务执行前的状态;同时,该事务对数据库或者其他事务的执行无影响,所有的事务都好像在独立的运行。
数据库的事务是通过 DBMS 完成处理的,那么,对于 Redis 而言,又是如何保证事务的呢?
Redis 事务
Redis 通过multi、exex、watch等命令来实现事务功能。
事务提供了一种将多个命令请求打包,然后一次性、按书讯地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而改去执行其他客户端的命令请求,它会将事务中的所有命令都执行完毕,然后才去处理其他客户端的命令请求。
以下是一个事务的例子, 它先以 MULTI 命令开始一个事务, 之后输入的命令都会依次进入命令队列,但不会执行, 直到输入EXEC 命令触发事务, 才一并依次执行事务中的所有命令(组队的过程中可以用DISCARD来放弃组队取消事务):
1 | 127.0.0.1:6379> multi |
事务命令说明如下:
multi:标记一个事务块的开始exec:执行所有事务块内的命令discard:取消事务,放弃执行事务块内的所有命令watch <key1> <key2>...:监视一个(或多个) key ,若在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断unwatch:取消watch命令对所有 key 的监视(若执行watch命令后exec或discard命令先被执行,则unwatch自动被执行)
事务的实现
官网的事务解释:Redis 事务可以一次执行多个命令, 并且带有以下两个特性:
①事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
②事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。
但是第二个特性有争论,很多人说 Redis 事务不保证原子性:虽然 Redis 的单个命令是原子性的,但同一个事务中若有一条命令执行失败,其后的命令还是会执行,没有回滚
再补充一个特性:没有隔离级别的概念:队列中的命令没有提交之前都不会实际的被执行,因为事务提交前任何指令都不会被实际执行,也就不存在“事务内的查询要看到事务里的更新,在事务外查询不能看到”这种问题
一个事务从开始到执行会经历以下三个阶段:
- 开始事务。
- 命令入队。
- 执行事务。
事务的开始
multi命令的执行标识着事务的开始(即将执行该命令的客户端从非事务状态切换至事务状态):
1 | 127.0.0.1:6380> multi |
命令入队
当一个客户端处于非事务状态时,其命令会被服务器立即执行:
1 | 127.0.0.1:6380> set age 18 |
与此不同,当我们使用multi命令开启事务,切换到事务状态后,服务器会根据客户端发来的不同命令执行不同的操作,具体流程如下图:

每个 Redis 客户端都有自己的事务状态,其被保存在客户端状态的属性里面(了解),事务状态包含一个,以及一个已入队命令的计数器(可以说是事务队列的长度)。是一个数组,以先进先出的方式保存了相关已入队命令的信息。例如若我们执行下面的命令:
1 | 127.0.0.1:6379> multi |
- 则最先入队的
set命令被放在事务队列的索引 0 位置上; - 第二入队的
get命令被放在事务队列的索引 1 位置上; - 第三入队的
sadd命令被放在事务队列的索引 2 位置上; - 最后入队的
smembers命令被放在事务队列的索引 3 位置上;
执行事务
当一个处于事务状态的客户端向服务器发送exec命令时,这个exec命令立即被服务器执行。服务器会便利这个客户端的事务队列,执行其中保存的所有命令,最后将执行命令所得的结果全部返回给客户端,对上面的例子执行exec命令则返回:
1 | 127.0.0.1:6379> exec |
watch命令介绍
1 | 127.0.0.1:6379> set age 16 |
当我们为一个键开启事务时,若想要对键age执行增量操作,执行 2 次incr age命令后 age 的值应为 18 (此时未exec))但是若在这个过程中另一个客户端也执行了增量操作,最后的结果就是 19 ,这显然不是我们所希望看到的。为了解决这种问题,我们可以使用watch命令:
1 | 127.0.0.1:6379> set age 16 |
事务的 ACID 特性
在传统的关系型数据库中,常常用ACID特性来检验事务功能的可靠性和安全性。
在 Redis 中,事务总是具有原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation),并且当 Redis 运行在某种特定的持久化模式下时,事务也具有持久性(Durability)
原子性
事务具有原子性是指,数据库将事务中的多个操作当作一个整体来执行,服务器要么就执行事务中的所有操作,要么就一个操作也不执行。
对于 Redis 来说,事务队列中的命令要么就全部都执行,要么就一个都不执行,因此,Redis事务是具有原子性的。(有争论,其实不能完全保证原子性,等等讨论)
举个例子,以下是一个成功执行的事务:
1 | 127.0.0.1:6380> multi |
再举个执行事务失败的例子,这个事务因为命令入队出错而被服务器拒绝执行,事务中的所有命令都不会被执行:
1 | 127.0.0.1:6380> multi |
Redis 的事务和传统的关系型数据库事务的最大区别在于,Redis 不支持事务回滚机制,即使事务队列中的某个命令在执行期间出现语法错误,整个事务也会继续执行下去,直到将事务中的所有命令都执行完毕。
在下面的例子中,即使set在执行期间出现了语法错误,事务的后续命令也会继续执行下去,并且之后执行的命令也不会有任何影响:
1 | 127.0.0.1:6380> multi |
Redis 的作者在事务功能的文档中解释说,不支持事务回滚是因为这种复杂的功能和 Redis 追求简单高效的设计主旨不符合,并且它认为, Redis 事务的执行时错误通常都是变成错误产生的(确实如此),这种错误只会出现在开发环境中,而很少会在实际的生产环境中出现,所以他认为没有必要为 Redis 开发事务回滚功能。
可这是否违反了原子性的定义呢?即要么全部发生,要么全部不发生。
注意哦!!! 要么全部不发生就是说明出错时事务可以回滚,可 Redis 都不支持事务回滚功能,又怎么能支持原子性呢?只能在某种程度上说是原子性吧,即执行事务时正确时是有原子性的,执行失败则是没有原子性的。就像作者说的那样,Redis 执行事务就肯定成功的,除非你写错了我规定的格式,不然才会失败。。。
这也解释了为什么可以在很多分析 Redis 的文章中看到别人说 Redis 事务是不支持原子性的,确实如此啊!
一致性
事务的一致性指的是,若数据库在执行事务之前是一致的,那么在事务执行之后,无论事务是否执行成功,数据库也应该是一致的。
Redis 通过谨慎的错误检测和简单的设计来保证事务的一致性。
隔离性
事务的隔离性指的是,即使数据库中有多个事务并发地执行,各个事务之间也不会互相影响,并且在并发状态下执行的事务和串行执行的事务产生的结果完全相同。
可以因为刚好 Redis 是使用单线程的方式来执行事务(以及事务队列中的命令),并且服务器保证,在执行事务期间不会对事务进行中断,因此,Redis 的事务总是以串行的方式运行的,所以其也是具有隔离性的。
持久性
事务的持久性指的是,当一个事务执行完毕时,执行这个事务所得的结果已经被保存到永久性存储介质(如硬盘)里面了,即使服务器在事务执行完毕之后停机,执行事务所得的结果也不会丢失。
因为 Redis 的事务不过是简单地用队列包裹了一组 Redis 命令,并没有为事务提供任何额外的持久化功能,所以 Redis 事务的持久性由 Redis 的持久化模式决定,即在 RDB 或 AOF 模式下,才可能具有持久性,因此还得看这些模式的具体配置情况。
参考
- Redis 官方文档
- 黄健宏 Redis 设计与实现 [M]. 机械工业出版社,2014
文章信息
| 时间 | 说明 |
|---|---|
| 2019-04-12 | 初稿 |