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

这篇具有很好参考价值的文章主要介绍了浅析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/AOF
    loadDataFromDisk();
	
    // 核心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:创建epoll
    server.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);
        
        // 核心,注册命令处理事件,设置回调函数readQueryFromClient
        if (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源码,缓存

结语

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

到了这里,关于浅析Redis①:命令处理核心源码分析(上)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【Redis深度专题】「核心技术提升」探究Redis服务启动的过程机制的技术原理和流程分析的指南(集群指令分析—上篇)

    Redis Cluster提供了一套完整的功能技术,使得Redis能够以分布式的方式运行,并具备高可用性、容错性和扩展性。通过自动发现、主从选举、在线分片等机制,Redis Cluster能够自动管理集群中的节点,并保证数据的一致性和可靠性。同时,基于配置文件和转向机制,Redis Cluster能

    2024年02月14日
    浏览(36)
  • CaffeineCache+Redis 接入系统做二层缓存思路实现(借鉴 mybatis 二级缓存、自动装配源码)

    现在手上有个系统写操作比较少,很多接口都是读操作,也就是写多读少,性能上遇到瓶颈了,正所谓前人栽树、后人乘凉,原先系统每次都是查数据库的,性能比较低,如果先查 redis,redis 没数据再查数据库的话,但是还可以更快,那就是使用内存查询,依次按照内存、

    2024年02月09日
    浏览(34)
  • springboot+redis+mysql+quartz-通过Java操作redis的KEYS*命令获取缓存数据定时更新数据库

    代码讲解: 3-点赞功能-定时持久化到数据库(pipeline+lua)-完善过程2_哔哩哔哩_bilibili https://www.bilibili.com/video/BV1w14y1o7BV 本文章代码: blogLike_schedule/like03 · xin麒/XinQiUtilsOrDemo - 码云 - 开源中国 (gitee.com) https://gitee.com/flowers-bloom-is-the-sea/XinQiUtilsOrDemo/tree/master/blogLike_schedule/like03 数据

    2024年02月15日
    浏览(40)
  • Redis从入门到精通(十三)Redis分布式缓存(一)RDB和AOF持久化、Redis主从集群的搭建与原理分析

    单机Redis存在四大问题: 1)数据丢失问题; 2)并发能力问题; 3)故障恢复问题; 4)存储能力问题。 而Redis分布式缓存,即基于Redis集群来解决单机Redis存在的问题: 1)数据丢失问题:实现Redis数据持久化; 2)并发能力问题:搭建主从集群,实现读写分离; 3)故障恢复问

    2024年04月12日
    浏览(29)
  • Redis 执行 RDB 快照期间,主进程可以正常处理命令吗?

    执行了 save 命令,会在主进程生成 RDB 文件,由于和执行操作命令在同一个线程,所以如果写入 RDB 文件的时间太长,会阻塞主进程。 执行 bgsave 过程中,由于是交给子进程来构建 RDB 文件,主进程还是可以继续工作的,此时主进程依然可以继续处理操作命令,也就是数据是能

    2024年02月11日
    浏览(34)
  • Redis 重写 AOF 日志期间,主进程可以正常处理命令吗?

    重写 AOF 日志的过程是怎样的? Redis 的重写 AOF 过程是由后台子进程 bgrewriteaof 来完成的,这么做有以下两个好处。 子进程进行 AOF 重写期间,主进程可以继续处理命令请求,从而避免阻塞主进程 子进程带有主进程的数据副本。这里使用子进程而不是线程,是因为如果使用线

    2024年02月11日
    浏览(31)
  • 从源码分析 Redis 异步删除各个参数的具体作用

    以前对异步删除几个参数的作用比较模糊,包括网上的很多资料都是一笔带过,语焉不详。 所以这次从源码(基于 Redis 7.0.5)的角度来深入分析下这几个参数的具体作用: lazyfree-lazy-user-del lazyfree-lazy-user-flush lazyfree-lazy-server-del lazyfree-lazy-expire lazyfree-lazy-eviction slave-lazy-flush

    2024年02月05日
    浏览(28)
  • 浅析Redis(1)

    一.Redis的含义 Redis可以用来作数据库,缓存,流引擎,消息队列。redis只有在分布式系统中才能充分的发挥作用,如果是单机程序,直接通过变量来存储数据是更优的选择。那我们知道进程之间是有隔离性的,那么redis中的数据是如何共享的呢?reids是基于网络,把自己内存中

    2024年02月11日
    浏览(21)
  • 浅析Redis大Key | 京东云技术团队

    在京东到家购物车系统中,用户基于门店能够对商品进行加车操作。用户与门店商品使用Redis的Hash类型存储,如下代码块所示。不知细心的你有没有发现,如果单门店加车商品过多,或者门店过多时,此Key就会越来越大,从而影响线上业务。 2.1、BigKey的界定 BigKey称为大Key,

    2024年02月06日
    浏览(27)
  • 浅析Redis集群数据倾斜问题及解决方法

    在服务端系统服务开发中,缓存是一种常用的技术,它可以提高系统对请求的处理效率,而redis又是缓存技术栈中的一个佼佼者,广泛的应用于各种服务系统中。在大型互联网服务中,每天需要处理的请求和存储的缓存数据都是海量的,在这些大型系统中,使用单实例的redi

    2024年02月07日
    浏览(28)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包