简单来说,redis的事务本质是一个指令集合,在事务提交时服务器会一起执行这些指令;
01
—
redis事务概述
MULTI、EXEC、DISCARD、WATCH是组成redis事务的四个基础指令;
MULTI
开启一个事务;
EXEC
执行事务;
DISCARD
丢弃事务;
WATCH
监视某一键值对,当客户端监视的某键值对在执行exec时已被修改,那么当前的事务会被取消;
02
—
实例
//开启事务multiOKset k pinganQUEUEDset k2 jiankangQUEUEDset k3 xingfuQUEUED//执行事务exec1) OK2) OK3) OKget k"pingan"get k2"jiankang"get k3"xingfu"
可以看到在执行事务(exec)后,组成事务的三个命令全部执行;
//开启事务multiOKset k4 helloQUEUEDset k5 hello2QUEUED//丢弃事务discardOKget k4(nil)
可以看到在执行丢弃事务(discard)后,组成事务的两个命令全部丢弃;
03
—
内部实现
redis事务的内部实现并不难:为客户端保存一个命令队列结构体,在执行事务时依次执行队列里的命令;
事务命令
/** 事务命令*/typedef struct multiCmd {参数robj **argv;参数数量int argc;命令指针struct redisCommand *cmd;} multiCmd;
命令队列结构体
/** 保存事务命令的结构体*/typedef struct multiState {事务队列,FIFO 顺序multiCmd *commands; /* Array of MULTI commands */已入队命令计数int count; * Total number of MULTI commands */int minreplicas; * MINREPLICAS for synchronous replication */time_t minreplicas_timeout; * MINREPLICAS timeout as unixtime. */} multiState;
执行开启事务到执行事务整个过程redis服务器状态的变化如下:

可以在processCommand源码中看见:在multi之后的命令都被保存到队列中
*/int processCommand(redisClient *c) {......* Exec the command */if (c->flags & REDIS_MULTI &&c->cmd->proc != execCommand && c->cmd->proc != discardCommand &&c->cmd->proc != multiCommand && c->cmd->proc != watchCommand) {在事务上下文中除 EXEC 、 DISCARD 、 MULTI 和 WATCH 命令之外其他所有命令都会被入队到事务队列中queueMultiCommand(c);addReply(c, shared.queued);} else {执行命令call(c, REDIS_CALL_FULL);c->woff = server.master_repl_offset;处理那些解除了阻塞的键if (listLength(server.ready_keys))handleClientsBlockedOnLists();}return REDIS_OK;}
04
—
Watch指令
redis的watch命令为了让redis拥有CAS特性,CAS的意思是在A修改之前,检查是否有其他人修改,如果其他人已修改,则A修改失败;
一个没有CAS的例子:

有CAS的例子:

客户端结构体redisClient会记录客户端监视的键
// 被监视的键list *watched_keys;
当某个键被修改的时候,所有监视改键的客户端都会被标记为REDIS_DIRTY_CAS,标识该键值被修改过,源码如下:

当用户发出EXEC的时候,在MULTI之后的所有命令都会被执行,从代码上看,如果客户端监视此时已被修改,那么客户端将会被标记REDIS_DIRTY_CAS;
下面我们实验一下:
//会话1(key的值为110,减去100剩10)decrby key 100(integer) 10
//会话2中监视key私有redis:0>watch keyOK私有redis:0>get key110私有redis:0>watch keyOK私有redis:0>multiOK私有redis:0>incrby key 100QUEUED//此时因为在会话1中已经对key进行了修改,所以会话2的事务将被丢弃私有redis:0>exec私有redis:0>get key10
用户开启一个事务后会提交多个命令,如果命令在入队过程中出现错误,譬如提交的命令本身不存在,参数错误和内存超额等,都会导致客户端被标记REDIS_DIRTY_EXEC,被标记 REDIS_DIRTY_EXEC 会导致事务被取消;
下面我们实验一下:
multiOKset key4 helloQUEUED//下面这个命令本身就错了set key5hello2(error) ERR wrong number of arguments for 'set' commandexec(error) EXECABORT Transaction discarded because of previous errors.
如上两个实验源码中表示为:
void execCommand(redisClient *c) {....../* Check if we need to abort the EXEC because:** 检查是否需要阻止事务执行,因为:** 1) Some WATCHed key was touched.* 有被监视的键已经被修改了** 2) There was a previous error while queueing commands.* 命令在入队时发生错误* (注意这个行为是 2.6.4 以后才修改的,之前是静默处理入队出错命令)** A failed EXEC in the first case returns a multi bulk nil object* (technically it is not an error but a special behavior), while* in the second an EXECABORT error is returned.** 第一种情况返回多个批量回复的空对象* 而第二种情况则返回一个 EXECABORT 错误*/在这里可以看到被标记为REDIS_DIRTY_CAS和REDIS_DIRTY_EXEC的客户端都会取消事务if (c->flags & (REDIS_DIRTY_CAS|REDIS_DIRTY_EXEC)) {addReply(c, c->flags & REDIS_DIRTY_EXEC ? shared.execaborterr :shared.nullmultibulk);// 取消事务discardTransaction(c);goto handle_monitor;}......
注意
单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增加任何维持原子性的机制,所以 Redis 事务的执行并不是原子性的。
官网提示:
It's important to note that even when a command fails, all the other commands in the queue are processed – Redis will not stop the processing of commands
比如有如下操作:
multi
set k aaa
set k2 bbb 如果执行这个指令时出错了,那么set k aaa 照样会执行成功;
exec




