在关系数据库的事务中,用户首先向数据库服务器发送BEGIN,然后执行各个相互一致的写操作和读操作,最后,用户可以选择发送COMMIT来确认之前所做的修改,或者发送ROLLBACK来放弃那些修改。
Redis事务处理流程
Redis通过MULTI、EXEC、WATCH等命令来实现事务功能。事务提供了一种将多个命令请求打包,然后一次性、按顺序地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而该去执行其他客户端的命令请求,它会将事务中的所有命令都执行完毕,然后才去处理其他客户端的命令请求。
一个事务从开始到结束会经历如下三个阶段:
(1)事务开始
MULTI命令标志着事务的开始,其将执行该命令的客户端从非事务状态切换至事务状态。
(2)命令入队
当一个客户端处于非事务状态时,这个客户端发送的命令会立即被服务器执行,而当其切换到事务状态时,服务器会根据客户端发来的不同命令来执行不同的操作:如果是MULTI、EXEC、DISCARD、WATCH四个命令中的一个,那么服务器立即执行;如果不是,那么服务器不立即执行命令,而是将其放入一个服务队列里面,然后向客户端返回QUEUED回复。
这些命令保存在每个客户端所持有的一个事务队列中,该队列以FIFO的方式保存入队的命令。
(3)执行事务
当处于事务状态的客户端向服务器发送EXEC命令时,EXEC命令将立即被服务器执行。服务器会遍历这个客户端的事务队列,执行队列中保持的所有命令,最后将执行结果全部返回给客户端。
但是,MULTI和EXEC只能保证它们中间的那些入队的命令不会被其他客户端掺杂在一起,却没有锁的逻辑,即不能解决干扰。比如,现有一个key为1的数字,客户端1需要在事务中连续做两次自增操作,但在EXEC之前,客户端2对key进行了修改set key 100
。
可以看出,在事务执行的过程中,读到了脏数据,为了保障不被干扰,需要对数据加锁。
乐观锁WATCH
在访问以写入为目的数据的时候,关系数据库会对被访问的数据加锁,直到事务被提交或者被回滚为止。如果有其他客户端试图对被加锁的数据行进行写入,那么该客户端将被阻塞,直到第一个事务执行完毕为止。这种做法称为悲观锁。
而Redis为了减少客户端的等待时间,并不会在执行WATCH命令时对数据加锁。相反地,Redis只会在数据已经被其他客户端抢先修改了的情况下,通知执行了WATCH的客户端,这种做法称为乐观锁。当被监视的键至少有一个已经被修改过了,服务器将拒绝执行事务,并向客户端返回代表事务执行失败的空回复。
对于上面的例子,使用WATCH监视key。
当客户端执行EXEC时,服务器发现WATCH监视的键“name”已经被修改,因此服务器拒绝执行客户端A的事务,并向客户端A返回空回复。
但是,需要注意的是,WATCH命令的作用只是当被监控的键值被修改后阻止之后一个事务的执行,而不能保证其他客户端不修改这一键值,所以,通常情况下在EXEC执行失败后会重新执行整个函数。
利用WATCH实现原子自增
使用伪代码表示为:
WATCH key
val = GET key
val = val + 1
MULTI
SET key $val
EXEC
事务的ACID性质
(1)原子性
对于Redis的事务功能来说,事务队列中的命令要么就全部都执行,要么就一个都不执行,因此,从这一点来看,Redis的事务是具有原子性的。
下图是事务因为命令入队出错而被服务器拒绝执行,事务中的所有命令都不会被执行。
但是,Redis事务不支持关系数据库的事务回滚机制,即事务队列中的某个命令在执行期间(非语法错误,执行错误)出现了错误,事务的后续命令也会继续执行下去,并且之前执行的命令没有影响。因此,在这方面是不符合原子性的。
(2)一致性
事务的一致性是指,如果数据库在执行事务之前是一致的,那么在事务执行之后,无论事务是否执行成功,数据库也应该是一致的。
如前所述,当出现入队错误或是执行错误的情况,Redis分别通过拒绝执行和进行错误处理的方式来保证事务的一致性。
当Redis服务器在执行事务的过程中停机,那么根据服务器使用的持久化方式,分以下几种情况讨论:
a. 如果当前Redis采用的是内存模式,那么重启后Redis数据库是空的,满足一致性条件;
b. 如果当前Redis采用RDB模式存储,在执行事务时,Redis不会中断事务去执行保存RDB的工作,只有在事务执行之后,保存RDB的工作才有可能开始。所以当RDB模式下的Redis服务器进程在事务中途被杀死时,事务内执行的命令,不管成功了多少,都不会被保存到RDB文件里。恢复数据库需要使用现有的RDB文件,而这个RDB文件的数据保存的是最近一次的数据库快照,所以它的数据可能不是最新的,但只要RDB文件本身没有因为其他问题而出错,那么还原后的数据库就是一致的;
c. 如果当前Redis采用的AOF模式存储,那么可能事务的内容还未写入到AOF文件,那么此时肯定是满足一致性的,如果事务的内容有部分写入到AOF文件中,那么需要用工具把AOF中事务执行部分成功的指令移除,这时,移除之后的AOF文件也是满足一致性的。
(3)隔离性
事务的隔离性是指,即使数据库有多个事务并发地执行,各个事务之间也不会互相影响,并发状态下执行的事务和串行执行的事务产生的结果完全相同。
因为Redis采用单线程的方式来执行事务,并且服务器保证,在执行事务期间不会对事务进行中断,因此,Redis的事务总是以串行的方式运行的,且事务总是具有隔离性。
(4)持久性
事务的持久性是指,当一个事务执行完毕时,执行这个事务所得的结果已经被保存到永久性存储介质里面了,即使服务器在事务执行完毕之后停机,执行事务所得的结果也不会丢失。
Redis事务只有当服务器运行在AOF持久化模式下,并且appendfsync选项为always时,才是具有持久性的。因为程序总会在执行命令之后调用同步函数,将命令数据真正地保存到硬盘里。
当然,不论Redis以什么模式运行,在一个事务的最后加上SAVE命令总可以保证持久性,但这种做法效率太低。
所以,Redis只满足ACID中的一致性和隔离性。