命令的处理过程

Redis server 和一个客户端建立连接后,会在事件驱动框架中注册可读事件——客户端的命令请求。命令处理对应 4 个阶段:

  • 命令读取:对应 readQueryFromClient 函数
  • 命令解析:对应 processInputBuffer 函数
  • 命令执行:对应 processCommand 函数
  • 结果返回:对应 addReply 函数

命令读取

readQueryFromClient 函数在之前的文章中分析过,主要流程就是:

  1. 调用 connRead 函数读取命令
  2. 将命令追加到同步缓冲区,修改同步偏移量
  3. 调用 processInputBuffer 函数进行命令解析
void readQueryFromClient(connection *conn) {
// 从 connection 结构中获取客户端
client *c = connGetPrivateData(conn);
……
nread = connRead(c->conn, c->querybuf+qblen, readlen);
…… /* There is more data in the client input buffer, continue parsing it
* in case to check if there is a full command to execute. */
processInputBuffer(c);
}

命令解析

processInputBuffer 函数会调用 processCommandAndResetClient 函数,其中又会调用 processCommand 函数。

void processInputBuffer(client *c) {

    while(c->qb_pos < sdslen(c->querybuf)) {
…… // 根据客户端输入缓冲区的命令开头字符判断命令类型
if (!c->reqtype) {
// 符合 RESP 协议的命令
if (c->querybuf[c->qb_pos] == '*') {
c->reqtype = PROTO_REQ_MULTIBULK;
} else {
// 管道类型命令
c->reqtype = PROTO_REQ_INLINE;
}
} // 对于管道类型命令,调用 processInlineBuffer 函数解析
if (c->reqtype == PROTO_REQ_INLINE) {
if (processInlineBuffer(c) != C_OK) break;
……
// 对于 RESP 协议命令,调用 processMultibulkBuffer 函数解析
} else if (c->reqtype == PROTO_REQ_MULTIBULK) {
if (processMultibulkBuffer(c) != C_OK) break;
}
…… if (c->argc == 0) {
resetClient(c);
} else {
…… // 可以开始执行命令了
if (processCommandAndResetClient(c) == C_ERR) {
return;
}
}
}
……
}
int processCommandAndResetClient(client *c) {
int deadclient = 0;
client *old_client = server.current_client;
server.current_client = c;
if (processCommand(c) == C_OK) {
commandProcessed(c);
}
if (server.current_client == NULL) deadclient = 1;
/*
* Restore the old client, this is needed because when a script
* times out, we will get into this code from processEventsWhileBlocked.
* Which will cause to set the server.current_client. If not restored
* we will return 1 to our caller which will falsely indicate the client
* is dead and will stop reading from its buffer.
*/
server.current_client = old_client;
/* performEvictions may flush slave output buffers. This may
* result in a slave, that may be the active client, to be
* freed. */
return deadclient ? C_ERR : C_OK;
}

命令执行

processCommand 函数是在 server.c 文件中实现的:

  • 调用 moduleCallCommandFilters 函数,将 Redis 命令替换成 module 想要替换的命令
  • 当前命令是否为 quit 命令,并进行相应处理
  • 调用 lookupCommand 函数,在全局变量 server 的 commands 成员变量中查找相关命令

commands 是一个哈希表:

struct redisServer {
...
dict *commands;
...
}

其是在 initServerConfig 函数中初始化的:

void initServerConfig(void) {
...
server.commands = dictCreate(&commandTableDictType,NULL);
...
populateCommandTable();
...
}

populateCommandTable 函数中使用了 redisCommandTable 数组:

void populateCommandTable(void) {
int j;
int numcommands = sizeof(redisCommandTable)/sizeof(struct redisCommand); for (j = 0; j < numcommands; j++) {
struct redisCommand *c = redisCommandTable+j;
int retval1, retval2; /* Translate the command string flags description into an actual
* set of flags. */
if (populateCommandTableParseFlags(c,c->sflags) == C_ERR)
serverPanic("Unsupported command flag"); c->id = ACLGetCommandID(c->name); /* Assign the ID used for ACL. */
retval1 = dictAdd(server.commands, sdsnew(c->name), c);
/* Populate an additional dictionary that will be unaffected
* by rename-command statements in redis.conf. */
retval2 = dictAdd(server.orig_commands, sdsnew(c->name), c);
serverAssert(retval1 == DICT_OK && retval2 == DICT_OK);
}
}

redisCommandTable 数组是在 server.c 中定义的,记录了当前命令所对应的实现函数。具体见:https://github.com/LjyYano/redis/blob/unstable/src/server.c

struct redisCommand redisCommandTable[] = {
{"module",moduleCommand,-2,
"admin no-script",
0,NULL,0,0,0,0,0,0}, {"get",getCommand,2,
"read-only fast @string",
0,NULL,1,1,1,0,0,0}, {"getex",getexCommand,-2,
"write fast @string",
0,NULL,1,1,1,0,0,0}, ……
};

redisCommand 结构如下:

struct redisCommand {
char *name;
redisCommandProc *proc;
int arity;
char *sflags; /* Flags as string representation, one char per flag. */
uint64_t flags; /* The actual flags, obtained from the 'sflags' field. */
/* Use a function to determine keys arguments in a command line.
* Used for Redis Cluster redirect. */
redisGetKeysProc *getkeys_proc;
/* What keys should be loaded in background when calling this command? */
int firstkey; /* The first argument that's a key (0 = no keys) */
int lastkey; /* The last argument that's a key */
int keystep; /* The step between first and last key */
long long microseconds, calls, rejected_calls, failed_calls;
int id; /* Command ID. This is a progressive ID starting from 0 that
is assigned at runtime, and is used in order to check
ACLs. A connection is able to execute a given command if
the user associated to the connection has this command
bit set in the bitmap of allowed commands. */
};

再回到 processCommand 函数,断当前客户端是否有 CLIENT_MULTI 标记,如果有的话,就表明要处理的是 Redis 事务的相关命令,所以它会按照事务的要求,调用 queueMultiCommand 函数将命令入队保存,等待后续一起处理。而如果没有,processCommand 函数就会调用 call 函数来实际执行命令了。

if (c->flags & CLIENT_MULTI &&
c->cmd->proc != execCommand && c->cmd->proc != discardCommand &&
c->cmd->proc != multiCommand && c->cmd->proc != watchCommand &&
c->cmd->proc != resetCommand)
{
// 将命令入队保存,后续一起处理
queueMultiCommand(c);
addReply(c,shared.queued);
} else {
// 调用 call 函数执行命令
call(c,CMD_CALL_FULL);
……
}

下面以最简单的 get 命令为例:

{"get",getCommand,2,
"read-only fast @string",
0,NULL,1,1,1,0,0,0},

对应的实现函数是 getCommand,其调用了 getGenericCommand 函数:

void getCommand(client *c) {
getGenericCommand(c);
} int getGenericCommand(client *c) {
robj *o; if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.null[c->resp])) == NULL)
return C_OK; if (checkType(c,o,OBJ_STRING)) {
return C_ERR;
} addReplyBulk(c,o);
return C_OK;
}

其最终会调用到 db.c 文件中的 lookupKeyReadWithFlags 函数:

robj *lookupKeyReadWithFlags(redisDb *db, robj *key, int flags) {
robj *val; if (expireIfNeeded(db,key) == 1) {
/* If we are in the context of a master, expireIfNeeded() returns 1
* when the key is no longer valid, so we can return NULL ASAP. */
if (server.masterhost == NULL)
goto keymiss; /* However if we are in the context of a slave, expireIfNeeded() will
* not really try to expire the key, it only returns information
* about the "logical" status of the key: key expiring is up to the
* master in order to have a consistent view of master's data set.
*
* However, if the command caller is not the master, and as additional
* safety measure, the command invoked is a read-only command, we can
* safely return NULL here, and provide a more consistent behavior
* to clients accessing expired values in a read-only fashion, that
* will say the key as non existing.
*
* Notably this covers GETs when slaves are used to scale reads. */
if (server.current_client &&
server.current_client != server.master &&
server.current_client->cmd &&
server.current_client->cmd->flags & CMD_READONLY)
{
goto keymiss;
}
}
val = lookupKey(db,key,flags);
if (val == NULL)
goto keymiss;
server.stat_keyspace_hits++;
return val; keymiss:
if (!(flags & LOOKUP_NONOTIFY)) {
notifyKeyspaceEvent(NOTIFY_KEY_MISS, "keymiss", key, db->id);
}
server.stat_keyspace_misses++;
return NULL;
}

会调用到 lookupKey 函数:

robj *lookupKey(redisDb *db, robj *key, int flags) {
dictEntry *de = dictFind(db->dict,key->ptr);
if (de) {
robj *val = dictGetVal(de); /* Update the access time for the ageing algorithm.
* Don't do it if we have a saving child, as this will trigger
* a copy on write madness. */
if (!hasActiveChildProcess() && !(flags & LOOKUP_NOTOUCH)){
if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
updateLFU(val);
} else {
val->lru = LRU_CLOCK();
}
}
return val;
} else {
return NULL;
}
}

结果返回

addReply 函数,主要是调用 prepareClientToWrite 函数,进而调用到 clientInstallWriteHandler 函数,将待写回客户端加入到全局变量 server 的 clients_pending_write 列表。最终调用 _addReplyToBuffer 函数,将要返回的结果添加到客户端的输出缓冲区。

/* Add the object 'obj' string representation to the client output buffer. */
void addReply(client *c, robj *obj) {
if (prepareClientToWrite(c) != C_OK) return; if (sdsEncodedObject(obj)) {
if (_addReplyToBuffer(c,obj->ptr,sdslen(obj->ptr)) != C_OK)
_addReplyProtoToList(c,obj->ptr,sdslen(obj->ptr));
} else if (obj->encoding == OBJ_ENCODING_INT) {
/* For integer encoded strings we just convert it into a string
* using our optimized function, and attach the resulting string
* to the output buffer. */
char buf[32];
size_t len = ll2string(buf,sizeof(buf),(long)obj->ptr);
if (_addReplyToBuffer(c,buf,len) != C_OK)
_addReplyProtoToList(c,buf,len);
} else {
serverPanic("Wrong obj->encoding in addReply()");
}
}

参考链接

Redis 源码简洁剖析系列

最简洁的 Redis 源码剖析系列文章

Java 编程思想-最全思维导图-GitHub 下载链接,需要的小伙伴可以自取~

原创不易,希望大家转载时请先联系我,并标注原文链接。

Redis 源码简洁剖析 12 - 一条命令的处理过程的更多相关文章

  1. Redis 源码简洁剖析 05 - ziplist 压缩列表

    ziplist 是什么 Redis 哪些数据结构使用了 ziplist? ziplist 特点 优点 缺点 ziplist 数据结构 ziplist 节点 pre_entry_length encod ...

  2. Redis 源码简洁剖析 11 - 主 IO 线程及 Redis 6.0 多 IO 线程

    Redis 到底是不是单线程的程序? 多 IO 线程的初始化 IO 线程运行函数 IOThreadMain 如何推迟客户端「读」操作? 如何推迟客户端「写」操作? 如何把待「读」客户端分配给 IO 线 ...

  3. Redis 源码简洁剖析 15 - AOF

    AOF 是什么 AOF 持久化的实现 命令追加 AOF 文件的写入和同步 AOF 文件的载入和数据还原 AOF 重写 为什么需要重写 什么是重写 如何重写 AOF 后台重写 为什么需要后台重写 带来的 ...

  4. Redis 源码简洁剖析 02 - SDS 字符串

    C 语言的字符串函数 C 语言 string 函数,在 C 语言中可以使用 char* 字符数组实现字符串,C 语言标准库 string.h 中也定义了多种字符串操作函数. 字符串使用广泛,需要满足: ...

  5. Redis 源码简洁剖析 03 - Dict Hash 基础

    Redis Hash 源码 Redis Hash 数据结构 Redis rehash 原理 为什么要 rehash? Redis dict 数据结构 Redis rehash 过程 什么时候触发 re ...

  6. Redis 源码简洁剖析 04 - Sorted Set 有序集合

    Sorted Set 是什么 Sorted Set 命令及实现方法 Sorted Set 数据结构 跳表(skiplist) 跳表节点的结构定义 跳表的定义 跳表节点查询 层数设置 跳表插入节点 zs ...

  7. Redis 源码简洁剖析 06 - quicklist 和 listpack

    quicklist 为什么要设计 quicklist 特点 数据结构 quicklistCreate quicklistDelIndex quicklistDelEntry quicklistInse ...

  8. Redis 源码简洁剖析 07 - main 函数启动

    前言 问题 阶段 1:基本初始化 阶段 2:检查哨兵模式,执行 RDB 或 AOF 检测 阶段 3:运行参数解析 阶段 4:初始化 server 资源管理 初始化数据库 创建事件驱动框架 阶段 5:执 ...

  9. Redis 源码简洁剖析 09 - Reactor 模型

    Reactor 模型 事件驱动框架 Redis 如何实现 Reactor 模型 事件的数据结构:aeFileEvent 主循环:aeMain 函数 事件捕获与分发:aeProcessEvents 函数 ...

随机推荐

  1. spring-aop(一)学习笔记

    AOP(Aspect-Oriented Programming) 面向切面编程 将复杂的需求分解出不同方面,将散布在系统中的公共功能集中解决 面向切面编程,是一种通过预编译方式和运行期动态代理实现在不 ...

  2. [ Flask ] myblog_flask问题集(RESTfull风格)

    VUE问题 前端VUE怎么捕获所有404NOT FOUND的路由呢? [ 解决方案 ] vue-router路由守卫,参考文档:动态路由匹配 对于路由.../edit/<id>,自己能编辑 ...

  3. 10个JS技巧

    1.过滤唯一值 Set 对象是es6新引入的,配合扩展运算符[...]一起使用,我们可以用它来过滤数组的唯一值. const array = [1, 1, 2, 3, 5, 5, 1] const u ...

  4. HttpRunner3的HTTP请求是怎么发出去的

    在HttpRunner3的示例代码中,发送HTTP请求的代码是这样写的: from httprunner import HttpRunner, Config, Step, RunRequest, Ru ...

  5. [论文翻译] 分布式训练 Parameter Sharding 之 Google Weight Sharding

    [论文翻译] 分布式训练 Parameter sharding 之 Google Weight Sharding 目录 [论文翻译] 分布式训练 Parameter sharding 之 Google ...

  6. 【Java】泛型

    文章目录 泛型 为什么要有泛型 在集合中使用泛型 如何自定义泛型结构 自定义泛型类.接口 泛型方法 泛型在继承方面的体现 通配符的使用 有限制条件的通配符的使用 泛型 为什么要有泛型 集合容器类在设计 ...

  7. 【机器学习基础】无监督学习(1)——PCA

    前面对半监督学习部分作了简单的介绍,这里开始了解有关无监督学习的部分,无监督学习内容稍微较多,本节主要介绍无监督学习中的PCA降维的基本原理和实现. PCA 0.无监督学习简介 相较于有监督学习和半监 ...

  8. http8种请求方式

    根据HTTP标准,HTTP请求可以使用多种请求方法. HTTP1.0定义了三种请求方法: GET, POST 和 HEAD方法. HTTP1.1新增了五种请求方法:OPTIONS, PUT, DELE ...

  9. python09day

    内容回顾 文件操作初识 三步走: 打开文件open() 文件路径path,编码方式encoding=,mode(默认读) 操作文件(对文件句柄进行操作) 读.写.追加 各四种模式 读:read().r ...

  10. LoadRunner编写socket性能测试脚本

    利用LoadRunner编写socket性能测试脚本 一.概述 Loadrunner拥有极为丰富的工具箱,供予我们制造出各种奇妙魔法的能力.其中就有此次要讨论的socket套接字操作. 二.socke ...