什么是事务
在讲解Redis事务以前,先复习一下事务的定义(来源于维基百科):
数据库事务通常包含了一个序列的对数据库的读/写操作。包含有以下两个目的:
- 为数据库操作序列提供了一个从失败中恢复到正常状态的方法,同时提供了数据库即使在异常状态下仍能保持一致性的方法。
- 当多个应用程序在并发访问数据库时,可以在这些应用程序之间提供一个隔离方法,以防止彼此的操作互相干扰。
当事务被提交给了数据库管理系统(DBMS),则DBMS需要确保该事务中的所有操作都成功完成且其结果被永久保存在数据库中,如果事务中有的操作没有成功完成,则事务中的所有操作都需要回滚,回到事务执行前的状态;同时,该事务对数据库或者其他事务的执行无影响,所有的事务都好像在独立的运行。
Redis事务
Redis通过multi
、exex
、watch
等命令来实现事务功能。事务提供了一种将多个命令请求打包,然后一次性、按书讯地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而改去执行其他客户端的命令请求,它会将事务中的所有命令都执行完毕,然后才去处理其他客户端的命令请求。
以下是一个事务的例子, 它先以 MULTI
命令开始一个事务, 之后输入的命令都会依次进入命令队列,但不会执行, 直到输入EXEC
命令触发事务, 才一并依次执行事务中的所有命令(组队的过程中可以用DISCARD
来放弃组队取消事务):
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set book-name "Thinking in Java"
QUEUED
127.0.0.1:6379> get book-name
QUEUED
127.0.0.1:6379> sadd user jack tom lucy
QUEUED
127.0.0.1:6379> smembers user
QUEUED
127.0.0.1:6379> exec
1) OK
2) "Thinking in Java"
3) (integer) 3
4) 1) "jack"
2) "lucy"
3) "tom"
事务命令说明如下:
-
multi
:标记一个事务块的开始 -
exec
:执行所有事务块内的命令 -
discard
:取消事务,放弃执行事务块内的所有命令 -
watch <key1> <key2>...
:监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断 -
unwatch
:取消watch
命令对所有 key 的监视(如果执行watch
命令后exec
或discard
命令先被执行,则unwatch
自动被执行)
事务的实现
官网的事务解释:Redis 事务可以一次执行多个命令, 并且带有以下两个特性:
①事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
②事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。
但是第二个特性有争论,很多人说Redis事务不保证原子性:虽然Redis的单个命令是原子性的,但同一个事务中如果有一条命令执行失败,其后的命令还是会执行,没有回滚
再补充一个特性:没有隔离级别的概念:队列中的命令没有提交之前都不会实际的被执行,因为事务提交前任何指令都不会被实际执行,也就不存在“事务内的查询要看到事务里的更新,在事务外查询不能看到”这种问题
一个事务从开始到执行会经历以下三个阶段:
- 开始事务。
- 命令入队。
- 执行事务。
事务的开始
multi
命令的执行标识着事务的开始(即将执行该命令的客户端从非事务状态切换至事务状态):
127.0.0.1:6380> multi
OK
命令入队
当一个客户端处于非事务状态时,其命令会被服务器立即执行:
127.0.0.1:6380> set age 18
OK
与此不同,当我们使用multi
命令开启事务,切换到事务状态后,服务器会根据客户端发来的不同命令执行不同的操作,具体流程如下图:
每个Redis客户端都有自己的事务状态,其被保存在客户端状态的
master
属性里面(了解),事务状态包含一个事务队列,以及一个已入队命令的计数器(可以说是事务队列的长度)。事务队列是一个数组,以先进先出(FIFO
)的方式保存了相关已入队命令的信息。例如如果我们执行下面的命令:
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set book-name "Thinking in Java"
QUEUED
127.0.0.1:6379> get book-name
QUEUED
127.0.0.1:6379> sadd user jack tom lucy
QUEUED
127.0.0.1:6379> smembers user
QUEUED
- 则最先入队的
set
命令被放在事务队列的索引0位置上; - 第二入队的
get
命令被放在事务队列的索引1位置上; - 第三入队的
sadd
命令被放在事务队列的索引2位置上; - 最后入队的
smembers
命令被放在事务队列的索引3位置上;
执行事务
当一个处于事务状态的客户端向服务器发送exec
命令时,这个exec
命令立即被服务器执行。服务器会便利这个客户端的事务队列,执行其中保存的所有命令,最后将执行命令所得的结果全部返回给客户端,对上面的例子执行exec
命令则返回:
127.0.0.1:6379> exec
1) OK
2) "Thinking in Java"
3) (integer) 3
4) 1) "jack"
2) "lucy"
3) "tom"
watch命令介绍
127.0.0.1:6379> set age 16
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incr age
QUEUED
127.0.0.1:6379> incr age
QUEUED
127.0.0.1:6379> exec
1) (integer) 18
2) (integer) 19
当我们为一个键开启事务时,若想要对键age
执行增量操作,执行2次incr age
命令后age的值应为18(此时未exec
),但是如果在这个过程中另一个客户端也执行了增量操作,最后的结果就是19,这显然不是我们所希望看到的。为了解决这种问题,我们可以使用watch
命令:
127.0.0.1:6379> set age 16
OK
127.0.0.1:6379> watch age
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incr age
QUEUED
127.0.0.1:6379> incr age
QUEUED
127.0.0.1:6379> exec
1) (integer) 17
2) (integer) 18
事务的ACID性质
在传统的关系型数据库中,常常用ACID
性质来检验事务功能的可靠性和安全性。
在Redis中,事务总是具有原子性(Atomicity
)、一致性(Consistency
)、隔离性(Isolation
),并且当Redis运行在某种特定的持久化模式下时,事务也具有持久性(Durability
)。
原子性
事务具有原子性是指,数据库将事务中的多个操作当作一个整体来执行,服务器要么就执行事务中的所有操作,要么就一个操作也不执行。
对于Redis来说,事务队列中的命令要么就全部都执行,要么就一个都不执行,因此,Redis事务是具有原子性的。(有争论,其实不能完全保证原子性,等等讨论)
举个例子,以下是一个成功执行的事务:
127.0.0.1:6380> multi
OK
127.0.0.1:6380> set msg "hello world"
QUEUED
127.0.0.1:6380> get msg
QUEUED
127.0.0.1:6380> exec
1) OK
2) "hello world"
再举个执行事务失败的例子,这个事务因为命令入队出错而被服务器拒绝执行,事务中的所有命令都不会被执行:
127.0.0.1:6380> multi
OK
127.0.0.1:6380> set msg "hello world"
QUEUED
127.0.0.1:6380> get
(error) ERR wrong number of arguments for 'get' command
127.0.0.1:6380> get msg
QUEUED
127.0.0.1:6380> exec
(error) EXECABORT Transaction discarded because of previous errors.
Redis的事务和传统的关系型数据库事务的最大区别在于,Redis不支持事务回滚机制,即使事务队列中的某个命令在执行期间出现语法错误,整个事务也会继续执行下去,直到将事务中的所有命令都执行完毕。
在下面的例子中,即使set
在执行期间出现了语法错误,事务的后续命令也会继续执行下去,并且之后执行的命令也不会有任何影响:
127.0.0.1:6380> multi
OK
127.0.0.1:6380> mset user1 jack user2 luck user3 jim
QUEUED
127.0.0.1:6380> set age 16 18 //语法错误
QUEUED
127.0.0.1:6380> set user4 william
QUEUED
127.0.0.1:6380> exec
1) OK
2) (error) ERR syntax error
3) OK
Redis的作者在事务功能的文档中解释说,不支持事务回滚是因为这种复杂的功能和Redis追求简单高效的设计主旨不符合,并且它认为,Redis事务的执行时错误通常都是变成错误产生的(确实如此),这种错误只会出现在开发环境中,而很少会在实际的生产环境中出现,所以他认为没有必要为Redis开发事务回滚功能。
可这是否违反了原子性的定义呢?即要么全部发生,要么全部不发生。注意!!!
要么全部不发生就是说明出错时事务可以回滚,可Redis都不支持事务回滚功能,又怎么能支持原子性呢?只能在某种程度上说是原子性吧,即执行事务时正确时是有原子性的,执行失败则是没有原子性的。就像作者说的那样,Redis执行事务就肯定成功的,除非你写错了我规定的格式,不然才会失败,好可爱的作者。。。
这也解释了为什么可以在很多分析Redis的文章中看到别人说Redis事务是不支持原子性的,确实如此啊!
一致性
事务的一致性指的是,如果数据库在执行事务之前是一致的,那么在事务执行之后,无论事务是否执行成功,数据库也应该是一致的。
Redis通过谨慎的错误检测和简单的设计来保证事务的一致性。
隔离性
事务的隔离性指的是,即使数据库中有多个事务并发地执行,各个事务之间也不会互相影响,并且在并发状态下执行的事务和串行执行的事务产生的结果完全相同。
可以因为刚好Redis是使用单线程的方式来执行事务(以及事务队列中的命令),并且服务器保证,在执行事务期间不会对事务进行中断,因此,Redis的事务总是以串行的方式运行的,所以其也是具有隔离性的。
持久性
事务的持久性指的是,当一个事务执行完毕时,执行这个事务所得的结果已经被保存到永久性存储介质(如硬盘)里面了,即使服务器在事务执行完毕之后停机,执行事务所得的结果也不会丢失。
因为Redis的事务不过是简单地用队列包裹了一组Redis命令,并没有为事务提供任何额外的持久化功能,所以Redis事务的持久性由Redis的持久化模式决定,即RDB或AOF模式下,这些模式下才可能具有持久性,还得看这些模式的具体配置情况。
参考资料
《redis设计与实现》(第二版)
redis官方文档
网友评论