本文详细介绍Redis事务的实现,以及涉及到主从复制的情况。由于持久化涉及Redis文件系统RIO,所以也会对RIO进行介绍。
这是Redis源码分析的系列文章的第四篇,前三篇分别是
简介
事务在执行期间不会主动中断,也就是说服务器在执行完事务中的所有命令之后,才会继续处理其他客户端的其他命令。
需要注意的是,Redis并没有原子性。原因之一是Redis不支持事务回滚。有人说Redis不是有DISCARD的么?但这个DISCARD只是在命令入队的阶段,等到真正执行事务的时候,Redis并不支持执行到一般就退出。
multi
总的来说,就是multi开始事务,最后exec提交事务或者discard放弃事务。在发送完multi后,后面发送的命令都不会被立即执行,而是返回QUEUED状态。当收到exec后,事务会将这个队列中的命令按照fifo的顺序执行。并发结果放入一个回复队列中返回给客户端。
watch
在multi前监控一些key,如果这些key被修改,则事务回滚。
主要结构
multiState
1 | typedef struct multiState { |
主要涉及下面一些字段:
commands
这个是multiCmd
数组,表示事务中的FIFO的队列。count
multi命令的数量,表示commands的长度?cmd_flags
multiCmd
1 | typedef struct multiCmd { |
redisCmd
事务开始和结束的实现
普通命令
可以看到,在processCommand
中,如果被设置了MULTI,那么就不会调用call
,而是调用queueMultiCommand
将指令入队。
1 | int processCommand(client *c) { |
开始
multi指令,会给客户端打上CLIENT_MULTI
这个flag。
1 | void multiCommand(client *c) { |
discard
1 | void discardCommand(client *c) { |
discardTransaction
先调用discardTransaction
清空multiCmd
数组。
再调用initClientMultiState
清除multiState
里面的其他内容。
再取消所有事务相关的flag。这边的flag都是什么意思?
CLIENT_MULTI
CLIENT_DIRTY_CAS
表示被watch的字段发生了修改。CLIENT_DIRTY_EXEC
由flagTransaction
产生,表示在先前操作中,出现了错误,所以不能exec。包含下面情况:- rejectCommand
在下面的场景下被调用:- shared.noautherr
- shared.oomerr
- shared.bgsaveerr
- 等等
- rejectCommandFormat
- processCommand中,涉及Redis Cluster相关。
1
2
3
4
5
6void discardTransaction(client *c) {
freeClientMultiState(c);
initClientMultiState(c);
c->flags &= ~(CLIENT_MULTI|CLIENT_DIRTY_CAS|CLIENT_DIRTY_EXEC);
unwatchAllKeys(c);
}
- rejectCommand
exec
1 | void execCommand(client *c) { |
检查flags,discard掉有问题的事务:
- 对于
CLIENT_DIRTY_CAS
返回空数组
因为这个不能算作是错误,只能作为一种特殊的表现。 - 对于
CLIENT_DIRTY_EXEC
返回EXECABORT1
2
3
4
5
6
7
8
9
10/*
* 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. */
if (c->flags & (CLIENT_DIRTY_CAS|CLIENT_DIRTY_EXEC)) {
addReply(c, c->flags & CLIENT_DIRTY_EXEC ? shared.execaborterr :
shared.nullarray[c->resp]);
discardTransaction(c);
goto handle_monitor;
}
因为已经检查了,取消客户端对所有键的监视。
因为事务中的命令在执行时可能会修改命令和命令的参数,所以为了正确地传播命令,需要备份这些命令和参数。
下面开始处理multiState
下面所有的multiCmd
。
1 | /* Exec all the queued commands */ |
当我们遇到第一个不是CMD_READONLY
或者CMD_ADMIN
的请求时,传播multi指令。【Q】这里readonly又出现了,为啥这么特殊呢?我觉得readonly指令一般指的是get之类的不涉及修改的指令。而一旦涉及修改或者增加等的指令,我们一定要把这个变动propagate出去。
这样,我们可以将MULTI/..../EXEC
打包成一整个,并且AOF和Slave都具有相同的一致性和原子性的要求。为啥呢?
1 | if (!must_propagate && |
ACLCheckCommandPerm
主要检查权限,包含:
- 执行命令的权限
- 对某个key的权限
如果检查通过,我们就用call来执行命令。
1 | int acl_keypos; |
因为执行后命令、命令参数可能会被改变,比如SPOP
会被改写为SREM
,所以这里需要更新事务队列中的命令和参数,确保Slave和AOF的数据一致性。
1 | /* Commands may alter argc/argv, restore mstate. */ |
在这个循环结束之后,事务执行完了,我们还原命令,并且清空事务状态。
1 | c->argv = orig_argv; |
下面,如果我们需要propagate的话,就增加dirty
。先前我们知道,在call结束之后,如果有dirty,会去触发检测是否propagate的逻辑。而call也是可以嵌套调用的。
这里有一个特殊情况需要考虑。就是在multi/exec块中,这个实例突然从Master切换成了Slave(可能是来自Sentinel的SLAVEOF
指令)。此时,原来Master收到的multi指令已经被传播到了replication backlog里面,但是后面的可能还没来得及传。对于这种情况,我们需要保证至少通过exec来结束这个backlog。【Q】为啥不用discard呢?
1 | /* Make sure the EXEC command will be propagated as well if MULTI |