浅析Redis①:命令处理核心源码分析(上)

写在前面

Redis作为我们日常工作中最常使用的缓存数据库,其重要性不言而喻,作为普调开发者,我们在日常开发中使用Redis,主要聚焦于Redis的基层数据结构的命令使用,很少会有人对Redis的内部实现机制进行了解,对于我而言,也是如此,但一直以来,我对于Redis的内部实现都很好奇,它为什么会如此高效,本系列文章是旨在对Redis源代码分析拆解,通过阅读Redis源代码,了解Redis基础数据结构的实现机制。

关于Redis的源码分析,已经有非常多的大佬写过相关的内容,最为著名的是《Redis设计与实现》,对于Redis源码的分析已经非常出色,本系列文章对于源码拆解时,并不会那么详细,相信大部分读者应该不是从事Redis的二次开发工作,对于源码细节过于深入,会陷入细节的泥潭,这是我在阅读源码时尽量避免的,我尽量做到对大体的脉络进行梳理,讲清楚主干逻辑,细节部分,如果读者有兴趣,可以自行参阅源码或相关资料。

本系列源代码,基于Redis 3.2.6

前言

毫无疑问,Redis已经成为我们日常开发中最长使用的缓存数据库,Redis如此高效的原因,是因为采用了非阻塞I/O模型来处理命令请求,这是我们耳熟能详的事情了,那么Redis具体是如何实现非阻塞I/O的呢?Redis是如何接收命令请求,并执行命令,再返回给客户端的呢?我们来一起探究。

本篇是Redis源码分析系列的第一篇,我们来一起看一下Redis处理命令的核心实现机制。

Redis处理命令请求实现

我们可以思考一下,如果使用Java实现Redis,应该会怎么样?

我们需要编写main函数,在main中初始化Redis的配置,然后实现一些Servlet的接口,处理命令请求,然后使用一个NIO框架,处理请求命令,最后将结果返回给客户端。事实上,Redis的整体结构上,的确是这样实现的,首先第一步,Redis需要一个main函数,作为Redis的启动入口,在main中,需要做一系列的事情。

那由此为引,我们来看一下Redis的main函数实现。

阅读Redis的源码,一切的起点在server.c中,在该文件中,定义了main函数,作为整个工程的入口:

int main(int argc, char **argv) {struct timeval tv;int j;// 省略,各种初始化操作检查
......// 核心1:初始化Server配置initServerConfig();// 从配置文件中加载配置信息loadServerConfig(configfile, options);// 省略,各种初始化操作检查
......// 核心2:初始化Server// 重点如: 绑定监听端口号,设置 acceptTcpHandler 回调函数initServer();// 省略,各种初始化操作检查
......// 从硬盘恢复数据,RBD/AOFloadDataFromDisk();// 核心3:设置核心函数beforeSleep,用于Redis进入事件驱动库的主循环之前被调用// 后面再讲aeSetBeforeSleepProc(server.el,beforeSleep);// 核心4:核中核,主函数循环,处理命令请求的核心函数aeMain(server.el);// 核心5:关闭服务,收尾工作aeDeleteEventLoop(server.el);return 0;
}

上面就是server.c中的main函数实现,这里我删除了很多非核心的检查方法,可以更清晰的聚焦mian函数的核心步骤,简单归纳一下main函数中都做了哪些事情:

1、Redis 会设置一些回调函数,当前时间,随机数的种子。回调函数实际上什么?举个例子,比如要给 Redis 发送一个关闭的命令,让它去做一些优雅的关闭,做一些扫尾清楚的工作,这个工作如果不设计回调函数,它其实什么都不会干。其实 C 语言的程序跑在操作系统之上,Linux 操作系统本身就是提供给我们事件机制的回调注册功能,所以它会设计这个回调函数,让你注册上,关闭的时候优雅的关闭,然后它在后面可以做一些业务逻辑。

2、不管任何软件,肯定有一份配置文件需要配置。首先在服务器端会把它默认的一份配置做一个初始化。

3、解析启动的参数。其实不管什么软件,它在初始化的过程当中,配置都是由两部分组成的。

第一部分,静态的配置文件;第二部分,动态启动的时候,main,就是参数给它的时候进去配置。

4、把服务端的东西拿过来,装载 Config 配置文件,loadServerConfig。

5、初始化服务器,initServer。

6、从磁盘装载数据。

7、有一个主循环程序开始干活,用来处理客户端的请求,并且把这个请求转到后端的业务逻辑,帮你完成命令执行,然后吐数据。

就这么一个过程。

OK,继续主题,我们希望了解是如何处理命令请求的,如果是Java语言,我们需要定义Servlet接口,并监听特定的端口,比如8080端口,以此来接收来自客户端的请求,但是对于C,是没有Servlet的,如果希望接收网络请求调用,需要通过socket进行网络通信,下面我们看一下Redis如何注册socket:

server.c initServer()

void initServer(void) {
// 省略,各种初始化操作检查
......// 核心1:创建epollserver.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);// 省略,各种初始化操作检查
......// 核心2:创建socket监听if (server.unixsocket != NULL) {unlink(server.unixsocket); /* don't care if this fails */server.sofd = anetUnixServer(server.neterr,server.unixsocket,server.unixsocketperm, server.tcp_backlog);if (server.sofd == ANET_ERR) {serverLog(LL_WARNING, "Opening Unix socket: %s", server.neterr);exit(1);}anetNonBlock(NULL,server.sofd);}// 省略,各种初始化操作检查
......// 核心3:创建定时任务if(aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {serverPanic("Can't create the serverCron time event.");exit(1);}// 核心4:重点,核中核,通过aeCreateFileEvent创建epoll监听socket,设置行为为READABLE,// 并注册回调函数,当socket接收到套接字时,会触发执行回调函数for (j = 0; j < server.ipfd_count; j++) {if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,acceptTcpHandler,NULL) == AE_ERR){serverPanic("Unrecoverable error creating server.ipfd file event.");}}if (server.sofd > 0 && aeCreateFileEvent(server.el,server.sofd,AE_READABLE,acceptUnixHandler,NULL) == AE_ERR) serverPanic("Unrecoverable error creating server.sofd file event.");// 省略,各种初始化操作检查
......}

上面就是initServer()函数的实现,老规矩,我删除了非核心部分的实现,简单归纳一下initServer()函数中都做了哪些事情:

1、创建非阻塞I/O,这里就是我们最常说的Redis的非阻塞I/O,这里Redis具体使用哪一种非阻塞I/O框架,取决于操作系统的具体实现,大部分场景下,我们的服务器使用Linux CentOS,其默认实现即为epoll

由于C中并没有Java语言中的多态,这里作者采用了一种精妙的方式实现了“多态”:

#ifdef HAVE_EVPORT
#include "ae_evport.c"
#else#ifdef HAVE_EPOLL#include "ae_epoll.c"#else#ifdef HAVE_KQUEUE#include "ae_kqueue.c"#else#include "ae_select.c"#endif#endif
#endif

2、创建socket监听

3、创建系统定时任务,后台线程执行,例如过期Key扫描淘汰

4、通过aeCreateFileEvent创建epoll监听socket,设置行为为READABLE,并注册回调函数,当socket接收到套接字时,会触发执行回调函数

networking.c acceptTcpHandler()

void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {// 省略部分代码
......while(max--) {// 获取socket数据cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);if (cfd == ANET_ERR) {if (errno != EWOULDBLOCK)serverLog(LL_WARNING,"Accepting client connection: %s", server.neterr);return;}serverLog(LL_VERBOSE,"Accepted %s:%d", cip, cport);// 处理命令请求acceptCommonHandler(cfd,0,cip);}
}

acceptTcpHandler函数中,处理socket连接,并进行命令请求处理,进入命令处理函数acceptCommonHandler

static void acceptCommonHandler(int fd, int flags, char *ip) {client *c;// 创建redis连接if ((c = createClient(fd)) == NULL) {serverLog(LL_WARNING,"Error registering fd event for the new client: %s (fd=%d)",strerror(errno),fd);close(fd); /* May be already closed, just ignore errors */return;}// 省略部分代码
......server.stat_numconnections++;c->flags |= flags;
}

networking.c createClient()

client *createClient(int fd) {client *c = zmalloc(sizeof(client));/* passing -1 as fd it is possible to create a non connected client.* This is useful since all the commands needs to be executed* in the context of a client. When commands are executed in other* contexts (for instance a Lua script) we need a non connected client. */if (fd != -1) {anetNonBlock(NULL,fd);anetEnableTcpNoDelay(NULL,fd);if (server.tcpkeepalive)anetKeepAlive(NULL,fd,server.tcpkeepalive);// 核心,注册命令处理事件,设置回调函数readQueryFromClientif (aeCreateFileEvent(server.el,fd,AE_READABLE,readQueryFromClient, c) == AE_ERR){close(fd);zfree(c);return NULL;}}// 省略部分代码
......return c;
}

createClient函数中,注册命令处理事件到epoll,并设置回调函数readQueryFromClient,当socket数据读取完成时,将执行回调函数,真正的进行命令执行,我们继续看回调函数readQueryFromClient的实现

networking.c readQueryFromClient()

void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {// 省略部分代码
......// 从socket fd中读取数据nread = read(fd, c->querybuf+qblen, readlen);if (nread == -1) {if (errno == EAGAIN) {return;} else {serverLog(LL_VERBOSE, "Reading from client: %s",strerror(errno));freeClient(c);return;}} else if (nread == 0) {serverLog(LL_VERBOSE, "Client closed connection");freeClient(c);return;}// 省略部分代码
......// 处理命令processInputBuffer(c);
}
void processInputBuffer(client *c) {server.current_client = c;/* Keep processing while there is something in the input buffer */while(sdslen(c->querybuf)) {// 省略部分代码
....../* Multibulk processing could see a <= 0 length. */if (c->argc == 0) {resetClient(c);} else {// 执行命令if (processCommand(c) == C_OK)resetClient(c);/* freeMemoryIfNeeded may flush slave output buffers. This may result* into a slave, that may be the active client, to be freed. */if (server.current_client == NULL) break;}}server.current_client = NULL;
}

server.c processCommand()

int processCommand(client *c) {// 省略部分代码
......    /* Now lookup the command and check ASAP about trivial error conditions* such as wrong arity, bad command name and so forth. */c->cmd = c->lastcmd = lookupCommand(c->argv[0]->ptr);if (!c->cmd) {flagTransaction(c);addReplyErrorFormat(c,"unknown command '%s'",(char*)c->argv[0]->ptr);return C_OK;} else if ((c->cmd->arity > 0 && c->cmd->arity != c->argc) ||(c->argc < -c->cmd->arity)) {flagTransaction(c);addReplyErrorFormat(c,"wrong number of arguments for '%s' command",c->cmd->name);return C_OK;}// 省略部分代码
......    /* Exec the command */if (c->flags & CLIENT_MULTI &&c->cmd->proc != execCommand && c->cmd->proc != discardCommand &&c->cmd->proc != multiCommand && c->cmd->proc != watchCommand){queueMultiCommand(c);addReply(c,shared.queued);} else {// 2528行,真正执行命令call(c,CMD_CALL_FULL);c->woff = server.master_repl_offset;if (listLength(server.ready_keys))handleClientsBlockedOnLists();}return C_OK;
}

server.c call()

void call(client *c, int flags) {// 省略部分代码
......    // 核心,执行命令对应的函数调用c->cmd->proc(c);duration = ustime()-start;dirty = server.dirty-dirty;if (dirty < 0) dirty = 0;// 省略部分代码
......  }

上述是命令处理的几个核心函数,这里我省略了部分非核心逻辑,聚焦命令处理的主流程,在回调函数readQueryFromClient中主要做了几个事情:

1、从socket fd中读取数据

2、解析socket数据,解析命令,查找命令是否存在

3、执行命令对应的函数调用

4、重置Client,已备下一次请求处理

server.c redisCommandTable

struct redisCommand redisCommandTable[] = {{"get",getCommand,2,"rF",0,NULL,1,1,1,0,0},{"set",setCommand,-3,"wm",0,NULL,1,1,1,0,0},{"setnx",setnxCommand,3,"wmF",0,NULL,1,1,1,0,0},{"setex",setexCommand,4,"wm",0,NULL,1,1,1,0,0},{"psetex",psetexCommand,4,"wm",0,NULL,1,1,1,0,0},{"append",appendCommand,3,"wm",0,NULL,1,1,1,0,0},{"strlen",strlenCommand,2,"rF",0,NULL,1,1,1,0,0},{"del",delCommand,-2,"w",0,NULL,1,-1,1,0,0},{"exists",existsCommand,-2,"rF",0,NULL,1,-1,1,0,0},{"setbit",setbitCommand,4,"wm",0,NULL,1,1,1,0,0},{"getbit",getbitCommand,3,"rF",0,NULL,1,1,1,0,0},{"bitfield",bitfieldCommand,-2,"wm",0,NULL,1,1,1,0,0},{"setrange",setrangeCommand,4,"wm",0,NULL,1,1,1,0,0},{"getrange",getrangeCommand,4,"r",0,NULL,1,1,1,0,0},{"substr",getrangeCommand,4,"r",0,NULL,1,1,1,0,0},{"incr",incrCommand,2,"wmF",0,NULL,1,1,1,0,0},{"decr",decrCommand,2,"wmF",0,NULL,1,1,1,0,0},{"mget",mgetCommand,-2,"r",0,NULL,1,-1,1,0,0},{"rpush",rpushCommand,-3,"wmF",0,NULL,1,1,1,0,0},{"lpush",lpushCommand,-3,"wmF",0,NULL,1,1,1,0,0},{"rpushx",rpushxCommand,3,"wmF",0,NULL,1,1,1,0,0},{"lpushx",lpushxCommand,3,"wmF",0,NULL,1,1,1,0,0},{"linsert",linsertCommand,5,"wm",0,NULL,1,1,1,0,0},{"rpop",rpopCommand,2,"wF",0,NULL,1,1,1,0,0},{"lpop",lpopCommand,2,"wF",0,NULL,1,1,1,0,0},// 省略部分代码..........};

上述代码,就是命令请求对应的函数列表,也就是我们最熟悉的Redis命令,就此我们可以串起一条命令请求的执行过程,为了方便理解,我们用一张流程图说明一条命令的执行过程:

redis命令执行

结语

本篇,我们通过Redis源代码,了解了一部分Redis处理命令请求的核心流程,之所以说是一部分,是因为我是按照Redis命令处理的逻辑流程进行的拆解,而并非真正的执行过程,Redis的如此高性能的根本,是基于epoll的非阻塞机制实现,在下一篇中,我们将重点介绍Redis的epoll实现机制,敬请期待。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/634301.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

vscode设置terminal的最大行数

今天跑代码出现一个问题&#xff0c;就是整个程序跑完&#xff0c;整个程序的输出信息过多&#xff0c;最开始输出的信息已经被vscode的缓存冲掉了&#xff0c;只能看到最后的一部分&#xff0c;具体的原因是vscode的terminal默认只能保存1000行的信息&#xff0c;所以如果想保…

《WebKit 技术内幕》之四(1): 资源加载和网络栈

第四章 资源加载和网络栈 使用网络栈来下载网页和网页资源是渲染引擎工作的第一步 1.WebKit 资源加载机制 1.1 资源 网页本身就是一种资源、网页还需要依赖很多其他的资源(图片、视频) &#xff08;1&#xff09;HTML 支持的资源主要包括以下几种类型&#xff1a; HTML 页…

Elasticsearch 数据类型相关总结:快速参考指南【记录】

在Elasticsearch中&#xff0c;有多种数据类型可用于定义字段。 在开始了解数据类型之前&#xff0c;首先要知道&#xff0c;在Elasticsearch中&#xff0c;分词处理主要针对文本字段&#xff0c;而对于其他类型字段&#xff08;如数值、日期、布尔等&#xff09;&#xff0c;通…

周五的胡思乱想

众所周知 csdn 在程序员的心目中是比较逊色的, 因为博客水平的参差不齐, 大部分人也都是用来作为自己的笔记方便未来复制。这样就导致这里的文章都是点到为止&#xff0c;没有去深究问题的根本原因&#xff0c;大家也都是复制一下解决方案就关闭的页面。或许这就是 csdn 的价值…

视频直播新时代,低延时直播交互,Web,Android,WebRtc推流拉流测试

直播现在已经深入了生活&#xff0c;学习&#xff0c;工作和娱乐方方面面&#xff0c;由于前些年的技术所限&#xff0c;传统rtmp,flv,m3u8 技术让直播快速启动项目产品&#xff0c;但也有很多不足&#xff0c;特别的交互式直播&#xff0c;一直是其中的痛点&#xff0c;延时较…

Oracle学习笔记——基础一起学 14

第十四天 DECODE的简单例子 用case实现 --DECODE的简单例子 --用case实现 select id,name, case sex when 1 then 男 when 2 then 女 end 性别 from student; --DECODE取出一行内两列中的较大值 --先建表 create table sales(month char(2),sales_tv number,sales_…

C#MQTT编程07--MQTT服务器和客户端(wpf版)

1、前言 上篇完成了winform版的mqtt服务器和客户端&#xff0c;实现了订阅和发布&#xff0c;效果666&#xff0c;长这样 这节要做的wpf版&#xff0c;长这样&#xff0c;效果也是帅BBBB帅&#xff0c;wpf技术是cs程序软件的福音。 wpf的基础知识和案例项目可以看我的另一个专…

Parade Series - RTSP - Web

Cron Startup Scripts └─ lt-init.cmd├─ lt-server.cmd│ └─ lt-rtsp-proxy-server.cmd│ └─ lt-rtsp-proxy-agent.cmd└─ lt-push.cmd└─ lt-rtsp-cam-daemon.cmd└─ lt-rtsp-cam-worker.cmdlt-init.cmd :: :: PLEASE DO NOT EDIT THIS FILE :: ECHO OFF…

Qt —— 编译Qt5版本QFTP库,并实现连接服务、获取列表、上传、下载、删除文件等操作(附源码、附基于Qt5编译好的QFTP库)

示例效果1 示例效果2 介绍 QFTP是Qt4的库,Qt5改用了QNetworkAccessManager来代替。但是Qt5提供的QNetworkAccessManager仅支持FTP的上传和下载,所以只能将QFTP库编译为Qt5的库来进行调用。 QFTP在Github的下载地址:https://github.com/qt/qtftp 客户端源码生成的release结果…

IDEA项目启动报错之Command too long

使用IDEA最新的版本2023-3月份社区版本&#xff0c;启动之前没问题的项目突然报错如下&#xff1a; Error running VipServiceApplication: Error running // VipServiceApplication.Command line is too long. Shorten the command line via // JAR manifest or via a // clas…

运放【之噪声】

电流噪声和电压噪声 我们一般评估噪声&#xff0c;还看对输出端噪声电压的贡献&#xff0c;因为电流乘以电阻等于电压&#xff0c;因此&#xff0c;最终的噪声大小还跟电路中电阻的取值有很大关系。显然&#xff0c;电阻越大&#xff0c;那么噪声电压就越大。反之电阻越小&…

设计模式——建造者模式(Builder Pattern)

概述 建造者模式是较为复杂的创建型模式&#xff0c;它将客户端与包含多个组成部分&#xff08;或部件&#xff09;的复杂对象的创建过程分离&#xff0c;客户端无须知道复杂对象的内部组成部分与装配方式&#xff0c;只需要知道所需建造者的类型即可。它关注如何一步一步创建一…

09 STM32 - PWM

9.1 PWM简介 脉冲宽度调制(Pulse Width Modulation,简称PWM)&#xff0c;是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术。简单一点&#xff0c;就是对脉冲宽度的控制。 9.2 PWM波原理 如下图所示&#xff0c;使用定时器定时&#xff0c;从0开始&#x…

Ubuntu系统Git的安装配置及使用笔记(更新中)

Ubuntu下Git的下载及配置 (1)、下载git 打开终端命令窗口,输入&#xff1a;sudo apt-get install git 提示&#xff1a;sudo命令是用来以其他身份来执行命令&#xff0c;预设的身份为root,使用sudo时必须先输入密码 (2)、可以使用命令git --version查看git的版本号 (3)、设置…

项目中常用的ahoos

项目中常用的ahoos如下 useMount、useUnmountuseRequestuseBooleanuseInfiniteScrolluseLocalStorageStateuseInViewportuseUpdateEffectuseLayoutEffectuseMapuseThrottleFnuseDebounceFn

Java内置锁:深度解析StampedLock并发类

内容摘要 StampedLock类是一种高性能的读写锁&#xff0c;它通过引入乐观读和写锁的优化机制&#xff0c;提高了多线程环境下的并发性能&#xff0c;他支持三种访问模式&#xff1a;悲观读、写和乐观读&#xff0c;可以根据不同的业务场景选择适合的锁策略&#xff0c;相比传统…

Qt/QML编程之路:OpenGL的示例(39)

Qt编程之后,会发现有版本问题,有时候一个示例不同的版本下可能会跑不同,有些Qt5跑不同Qt6已经完善,可以跑通。 我就看到有个关于OpenGL的示例: 这个示例是演示怎么基于OpenGL编程的,但是调试时却发现glViewXXX等gl打头的函数说找不到reference,或者什么link不上之类的错…

FindMy技术与游戏手柄结合

游戏手柄作为游戏的重要配件&#xff0c;它极大地提升了玩家的游戏体验&#xff0c;推动了游戏市场的不断扩大。游戏手柄的触摸感应技术为游戏开发者提供了更多的创意空间&#xff0c;也为硬件制造商带来了新的商机。游戏手柄的个性化定制也为玩家社区的发展提供了动力&#xf…

红队打靶练习:BOB: 1.0.1

目录 信息收集 1、netdiscover 2、nmap 3、nikto 4、whatweb 目录探测 1、dirb 2、gobuster 3、dirsearch WEB 主页&#xff1a; robots.txt 其他页面 反弹shell 提权 系统信息收集 jc账户 本地提权 信息收集 1、netdiscover ┌──(root㉿ru)-[~/kali] └…

命令执行拼接符实例图解

命令执行常用到的5个拼接符&#xff0c;分别是逻辑与&#xff08;&&&#xff09;、逻辑或&#xff08;||&#xff09;、按位与&#xff08;&&#xff09;、按位或&#xff08;|&#xff09;和linux系统特有的分号&#xff08;;&#xff09; 1、&&&#xf…