Redis Server一旦和某客户端建立连接,就会在事件驱动框架中注册可读事件,对应客户端的命令请求。
整个命令处理过程可分阶段:
- 命令解析,processInputBufferAndReplicate
- 命令执行,processCommand
- 结果返回,addReply
1 命令读取:readQueryFromClient
会从客户端连接的socket中,读取最大为readlen长度的数据,readlen大小为宏定义PROTO_IOBUF_LEN,默认16KB。
接着根据读取数据的情况,进行异常处理,如:
-
数据读取失败
-
或客户端连接关闭等
若当前客户端是主从复制中的主节点,readQueryFromClient会把读取的数据,追加到用于主从节点命令同步的缓冲区中。
最后,调用processInputBuffer,进入命令解析阶段。
void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {
...
readlen = PROTO_IOBUF_LEN; // 从客户端socket中读取的数据长度,默认16KB
...
c->querybuf = sdsMakeRoomFor(c->querybuf, readlen); // 给缓冲区分配空间
nread = read(fd, c->querybuf+qblen, readlen); // 调用read从描述符为fd的客户端socket中读取数据
...
processInputBufferAndReplicate(c); // 进一步处理读取内容
}
2 命令解析:processInputBuffer
根据当前客户端是否有CLIENT_MASTER标记,执行如下分支:
-
Case1
客户端无CLIENT_MASTER标记,即当前客户端不属于主从的Master。processInputBufferAndReplicate直接调processInputBuffer,对客户端输入缓冲区中的命令和参数进行解析。所以在这里,实际执行命令解析的函数是processInputBuffer
-
Case2
客户端有CLIENT_MASTER标记。processInputBufferAndReplicate除了会调用processInputBuffer,解析客户端命令,还会调用replicationFeedSlavesFromMasterStream,将主节点接收到的命令同步给从节点
最终命令解析就在processInputBuffer:
-
首先,processInputBuffer函数会执行一个while循环,不断从客户端的输入缓冲区读数据
-
然后,判断读取到的命令格式,是否以“*”开头:
-
命令
*
开头,processInputBuffer会调processMultibulkBuffer解析读取到的命令 -
不是
*
开头,即管道命令,命令和命令间用换行符\r\n
分隔的。如使用Telnet发给Redis的命令就属该类型命令。processInputBuffer会调用processInlineBuffer解析命令。
-
命令解析完成后,processInputBuffer就会调用processCommand,进入命令处理的第三阶段:命令执行。
执行流程图
3 命令执行:processCommand
实际执行命令前的主要逻辑:
-
processCommand调moduleCallCommandFilters,将Redis命令替换成module想替换的命令
-
processCommand判断当前命令是否为quit命令并做相应处理
-
processCommand调lookupCommand,在全局变量server的commands成员变量中查找相关命令
全局变量server的commands成员变量是个哈希表,定义在redisServer结构体:
commands成员变量的初始化是在initServerConfig,调用dictCreate完成哈希表创建,再调用populateCommandTable将Redis提供的命令名称和对应的实现函数,插入哈希表。
而这其中的populateCommandTable使用redisCommand结构体数组redisCommandTable。
redisCommandTable数组在server.c定义,它的每一个元素是redisCommand结构体类型的记录,对应Redis实现的一条命令。即redisCommand结构体记录当前命令所对应的实现函数。
如下代码展示GET、SET等命令信息,实现函数getCommand,setCommand:
所以lookupCommand会根据解析的命令名称,在commands对应的哈希表中查找相应命令。
查到对应命令后,processCommand就会检查,如命令参数是否有效、发送命令的用户是否进行过验证、当前内存的使用情况等。
等processCommand对命令做完各种检查,就开始执行命令,判断当前客户端是否有CLIENT_MULTI标记:
-
有,说明要处理Redis事务相关命令
按事务要求,调queueMultiCommand:将命令入队保存,等待后续再一把梭
-
无,无关事务特性
调call实际执行命令。call通过调用命令本身,即redisCommand结构体中定义的函数指针完成。每个redisCommand结构体中都定义了其对应实现函数,在redisCommandTable数组。
分布式锁的加锁操作就是使用SET命令,就通过SET命令看一个命令实际执行过程。
SET命令对应实现函数setCommand:
- 首先会判断命令参数,如是否带有NX、EX、XX、PX等可选项,若有,就会记录这些标记
- 然后,调用setGenericCommand:根据setCommand记录的命令参数标记,进行相应处理。如命令参数中有NX,则setGenericCommand会调用lookupKeyWrite,查找要执行SET命令的K是否已存在
- 若K已存在,则setGenericCommand会调用addReply,返回NULL,正符合分布式锁语义。
若SET命令可正常执行,即:
-
命令带NX选项,但K不存在
-
或带有XX选项,但K已存在
这样setGenericCommand就会调用setKey完成KV对的实际插入:
setKey(c->db,key,val);
然后,若命令设置了TTL,setGenericCommand还会调用setExpire函数设置过期时间。最后,setGenericCommand调用addReply函数,将结果返给客户端:
addReply(c, ok_reply ? ok_reply : shared.ok);
SET命令执行流程图
无论:
- 在命令执行过程中,发现不符合命令的执行条件
- 或是命令能成功执行
addReply函数都会被调用以返回结果。所以,这就进入命令处理过程的最后一个阶段:结果返回阶段。
4 结果返回:addReply
调用prepareClientToWrite,并在prepareClientToWrite中调用clientInstallWriteHandler,将待写回客户端加入到全局变量server的clients_pending_write列表。
然后,addReply会调用_addReplyToBuffer等函数,将要返回的结果添加到客户端的输出缓冲区。
至此,这就是一条命令如何从读取,经过解析、执行等步骤,最终将结果返给客户端,该过程以及涉及的主要函数:
若在前面命令处理过程中,都由I/O主线程处理,则命令执行的原子性肯定能得到保证,分布式锁的原子性也相应得到保证。
FAQ
但若这个处理过程配合I/O多路复用机制和多IO线程机制,那这俩机制是在这个过程的什么阶段发挥作用?会不会影响命令执行原子性?