Redis之事务篇

欢迎阅读大魔王的睡前私语系列,这是Redis第十篇文章

事务

Redis通过MULTI,EXEC,WATCH命令来实现事务(Transaction)功能。事务提供了一种将多个命令请求打包,然后一次性,按照顺序执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而改去执行其他客户端命令请求,它会将事务中的所有命令都执行完毕,然后才去处理客户端的命令请求。

事务首先以一个MULTI命令开始,接着将多个命令放入事务中,最后由EXEC命令将这个事务提交给服务器执行:

redis>MULTI
ok
redis>SET "name" "Practical Common Lisp"
QUEUED
redis >ESEC

事务的实现

一个事务从开始到结束通常会经历以下三个阶段:

    事务开始 命令入队 事务执行

事务开始

MULTI命令的执行标志着事务的开始:

redis>MULTI
ok

MULTI命令可以将执行该命令的客户端从非事务状态切换至事务状态,这一切换是通过在客户端状态的flags属性中打开REDIS_MULTI标识来完成的。

命令入队

当一个客户端处于非事务状态时,这个客户端发送的命令会立刻被服务器执行:

redis >SET "name" "Practical Common Lisp"
OK
redis >GET "name"
"Practical Common Lisp"

与此不同的是,当一个客户端切换到事务状态之后,服务器会根据这个客户端发来的不同命令执行不同的操作:

如果客户端发送的命令为EXEC,DISCARD,WATCH,MULTI四个命令中的其中一个,那么服务器立即执行这个命令。 与此相反,如果客户端发送的命令是以上四个命令之外的其他命令,那么服务器不会立即执行,而是将这个命令放入到一个事务队列里面,然后客户端返回QUEUED回复

服务器判断命令是该入队还是立即执行的过程如下图:
在这里插入图片描述

事务队列

每个Redis客户端都有自己的事务状态,这个事务状态保存在客户端状态的mstate属性里面:

typeedf struct redisClient {
	//事务状态
	multiState mstate ;
}redisClient;

事务状态包含一个事务队列,以及一个已入队命令的计数器(也可以说是十事务队列的长度):

typedef struct multiState{
	//事务队列,FIFO顺序
	multiCmd *command;
	//已入队命令计数器
	int count;
}multiState;

事务队列是一个multiCmd类型的数组,数组中的每个multiCmd结构都保存了一个已入队命令的相关信息,包括指向命令实现函数的指针,命令的参数,以及参数的数量:

typedef struct multiCmd{
	//参数
	robj **argv;
	//参数数量
	int argc;
	//命令指针
	struct redisCommand *cmd;
}multiCmd;

事务队列以先进先出的方式保存入队的命令,较先入队的命令会被放到数组的前面,后入队的命令会被放入数组的后面。

执行事务

当一个处于事务状态的客户端向服务器发送EXEC命令时,这个EXEC命令将立即被服务器执行。服务器会遍历这个客户都安的事务队列,执行队列中保存的所有命令,最后将执行命令所得的结果全部返回客户端。

WATCH命令的实现

WATCH命令是一个乐观锁(optimistic locking)。他可以在EXEC命令执行之前,监视任意数量的数据库键,并在EXEC命令执行时,检查被监视的键是否至少有一个已经被修改了,如果是,服务器将拒绝执行事务,并且向客户端返回代表事务执行失败的空回复。

redis >WATCH "name"
OK
redis >MULTI
OK
redis >EXEC
nil

两个客户端执行命令的过程:

时间 客户端A 客户端B
T1 WATCH "name"
T2 MULTI
T3 SET "name" "peter"
T4 SET "name" "john"
T5 EXEC

在时间T4,客户端B修改了name键的值,当客户端A在T5执行EXEC命令时,服务器会发现WATCH监视的键name已经被修改,因此服务器拒绝执行客户端A的事务,并且向客户端A返回空回复

使用WATCH 命令监视数据库键

每个Redis数据库都保存着一个watched_keys字典,这个字典的键是讴歌被WATCH命令键是的数据库键,而字典的值则是一个链表,链表中记录了所有监视相应数据库键的客户端:

typedef struct redisDb{
	//正在被WATCH命令监视的键
	dict *watched_keys;
}redisDb;

通过watched_keys字典,服务器可以清楚的直到哪些数据库键正在被监视,以及哪些客户端u你正在监视这些数据库键

监视机制的触发

所有对数据库进行修改的命令,比如SET,LPUSH,SADD,DEL,FLUSHDB等等,在执行之后都会调用multi.c/touchWatchKey函数对watched_keys字典进行检查,查看是否有客户端正在监视刚刚被命令修改过的数据库键,如果有的话,那么touchWatchKey函数将监视被修改键的客户端REDIS_DIRTY_CAS标识打开,标识该客户端的事务安全性已经被破坏。

判断事务是否安全

当服务器接收到一个客户端发送的EXEC命令时,服务器会根据这个客户端是否打开了REDIS_DIRTY_CAS标识来决定是否执行事务:
如果客户端的REDIS_DIRTY_CAS标识已经被打开,那么说明客户端缩减是的键当中,直到有一个键已经被修改

事务的ACID性质

在Redis中,事务总是有原子性,一致性,隔离性,并且当Redis运行在某种特定的持久化模式下,事务也具有持久性。

原子性

事务具有原子性指的是,数据库将事务中的多个操作当成一个整体来执行,服务器要么全都执行,要么全都不执行。

对于Redis事务来说,事务队列中的命令要么全部执行,要么全都不执行,因此,Redis事务具有原子性。

Redis的事务和传统关系型数据库事务不同,Redis不支持事务回滚机制(rollback),即使事务队列中的某个命令在执行期间出现错误,整个事务也会继续执行下去,并且之前执行的命令不会受任何影响。

一致性

事务具有一致性,如果数据库在执行事务之前是一致的,那么在事务执行之后,数据库也应该是一致的。”一致“指的是数据符合数据库本身的定义和要求,没有包含非法数据或者无效数据。Redis通过错误检测和设计来保证事务的一致性。

入队错误

如果一个事务在入队命令的过程中,出现了命令不存在,或者命令的格式不正确等情况,那么Redis将拒绝执行这个事务。

因为服务器会拒绝执行入队过程出现错误的事务,所以Redis事务的一致性不会被带有入队错误的事务影响。

执行错误

除了入队时可能发生错误以外,事务还可能在执行过程中发生错误。

执行过程中发生的错误都是一些不能在入队时被服务器发现的错误,这些错误只会在命令实际执行时被触发 即使在事务执行过程中发生了错误,服务器也不会中断事务的执行,它会继续执行事务中余下的其他命令,并且已执行的命令不会被出错的命令影响。

对数据库键执行了错误类型的操作是事务执行期间最常见的错误。

例如,通过SET命令将键”msg“设置唯一个字符串键,接着对”msg“键执行列表键的RPUSH命令,这会引发错误,并且只在事务执行期间被发现。

因为在事务执行过程中,出错的命令会被服务器识别出来,并进行相应的错误处理,所以这些出错命令不会对数据库进行任务修改,也不会对事务的一致性产生任何影响。

隔离性

事务的隔离性是指,数据库中有多个事务并发执行,各个事务之间不会互相影响,并且在并发状态下执行的事务和串行执行的事务产生的结果完全相同

因为Redis使用单线程的方式来执行事务(以及事务队列中的命令),并且服务器保证,在执行事务期间不会对事务进行中断,因此,Redis的事务总是以串行的方式运行,并且事务也总是具有隔离性。

持久性

持久性是指,当一个事务执行完毕,执行这个事务所得结果已经被保存到永久性存储介质里面了,即使服务器在事务执行完毕之后停机执行事务所得的结果也不会丢失

因为Redis事务不过是简单地用队列包裹一组Redis命令,所以Redis并没有为事务提供额外的持久化功能,Redis事务的持久化功能由Redis所用的Redis持久化模式决定

只有当服务器运行在AOF持久化模式下,并且appendfsync选项的值为slways时,程序会在执行命令之后调用同步
(sync)函数,将命令数据保存到硬盘中。