Redis 源码初探- 服务初始化和接收请求流程

前言

Redis (RE mote DI ctionary S erver) 是一个开源、key/value、基于内存的数据库。在 github 上 redis 的介绍是:

Redis is an in-memory database that persists on disk. The data model is key-value, but many different kind of values are supported: Strings, Lists, Sets, Sorted Sets, Hashes, Streams, HyperLogLogs, Bitmaps.

Redis 在全球范围广泛使用,具有极高的流行度,可以从 DB-Engines Ranking 上看到 Redis 排名情况。

本文旨在从源码角度介绍 Redis 数据库,包括服务的初始化及请求的处理过程。让读者对 Redis 源码有整体性的了解,帮助读者阅读源码,并且有以下益处

  • Redis 的核心代码都是 C 语言代码,阅读 Redis 有助于 C 语言初学者深入理解 C 语言,对 C 语言开发工程化项目很有帮助。

  • Redis 使用 IO 多路复用并且进行了封装,适合对网络开发感兴趣者。

  • Redis 数据结构的设计对于普通开发者有很大的借鉴意义。

  • 深入理解 Redis 内部设计可以让后端开发者更高效的使用 Redis。

服务初始化

获取源码

源码、编译、启动

shell 复制代码
git clone --depth 1 --branch 7.2 git@github.com:redis/redis.git

cd redis && make

## 如果有必要在 redis.conf 修改端口号
./src/redis-server  redis.conf

Redis internals

在深入源码之前,首先看看 Redis 代码的整体结构和重要文件。Redis 的 Github 网页 Redis internals 对其进行了介绍。

Here we explain the Redis source code layout, what is in each file as a general idea, the most important functions and structures inside the Redis server and so forth. We keep all the discussion at a high level without digging into the details since this document would be huge otherwise and our code base changes continuously, but a general idea should be a good starting point to understand more. Moreover most of the code is heavily commented and easy to follow.

源码目录

在此,只关注 src 源码目录,并且只关注其中重要的几个文件。

server.h

在 c 语言中头文件是结构体定义和函数定义的文件,在 Redis 中 server.h 定义很多常量、非常重要的结构体、Redis 命令对应的函数。其中重要结构体如下;

struct redisServer: 包含 Redis 运行的所有配置和所有的共享状态。其中重要属性:

  • redisDb *db: 包含所有数据库。
  • server.commands: 包含 Redis 所有的命令处理函数。
  • list *clients: 包含所有 Redis 当前客户端对象。

struct client: 客户端连接对象,封装了客户端连接。

struct redisObject: Redis 所有对象的表现形式,包括 字符串、列表等。

server.c

server.c 包含 main 函数,是整个程序的入口。定义了 struct redisServer server 作为全局变量。 并且定义如下重要函数:

  • initServerConfig() 初始化配置。
  • initServer() 初始化 Redis server 需要的组件。
  • aeMain() 启动时间循环处理网络 IO 。

networking.c

定义了与 client 相关的操作函数,包括:

  • createClient() 创建客户端。
  • writeToClient() 写数据到客户端。
  • readQueryFromClient() 读客户端数据。
  • processInputBuffer() 处理读缓冲区数据,调用 processCommand() 处理命令。
  • freeClient() 释放客户端。
  • addReply*() 一组函数是写数据到发送缓冲区。

db.c

主要包含数据的相关操作,包括增删改查等。

其他 C 文件

t_hash.c, t_list.c, t_set.c, t_string.c, t_zset.c 包含 Redis 五种基本对象的实现。

组件初始化

server.cmain 函数是整个文件的入口,重要组件的初始化都在这个阶段。 本小节只会介绍单机版的 Redis 启动逻辑。下面是 main 函数主要逻辑的主要函数。

c 复制代码
# server.c
int main(int argc, char **argv) {
    //初始化配置
    initServerConfig();
    
    //初始化与连接类型相关的属性。
    //比如 连接是 tcp,那么初始化 listen/conn_create等函数
    //在绑定端口和新链接到来时调用
    connTypeInitialize();
    
    //初始化重要组件
    initServer();
    
    //监听端口
    initListeners();
    
    // 最后初始化的组件,其他包含多线程的初始化
    InitServerLast();
    
    //启动事件循环,处理客户端请求
    aeMain(server.el);
}

网络组件初始化

这部分主要包括 io 函数的绑定和绑定端口

绑定 io 函数

c 复制代码
//调用链
connTypeInitialize() 
    -> RedisRegisterConnectionTypeSocket (tcp连接相关)
        //将连接类型注册到数组中以供后续使用
    -> connTypeRegister(&CT_Socket)

下面重点看看 CT_Socket 结构

c 复制代码
static ConnectionType CT_Socket = {
    //类型 tcp
    .get_type = connSocketGetType,

    /* ae & accept & listen & error & address handler */
    .ae_handler = connSocketEventHandler,
    .accept_handler = connSocketAcceptHandler,
    .addr = connSocketAddr,
    .is_local = connSocketIsLocal,
    .listen = connSocketListen,

    /* create/shutdown/close connection */
    .conn_create = connCreateSocket,
    .conn_create_accepted = connCreateAcceptedSocket,
    .shutdown = connSocketShutdown,
    .close = connSocketClose,

    /* connect & accept */
    .connect = connSocketConnect,
    .blocking_connect = connSocketBlockingConnect,
    .accept = connSocketAccept,

    /* IO */
    .write = connSocketWrite,
    .writev = connSocketWritev,
    .read = connSocketRead,
    .set_write_handler = connSocketSetWriteHandler,
    .set_read_handler = connSocketSetReadHandler,
    .get_last_error = connSocketGetLastError,
    .sync_write = connSocketSyncWrite,
    .sync_read = connSocketSyncRead,
    .sync_readline = connSocketSyncReadLine,
}

Redis 在源码中有解释,一共有四组函数。

  • 与服务端 socket(server socket) 相关的操作。
  • 与服务端 socket连接相关的操作。
  • 与客户端 socket连接相关的操作
  • io 读写相关的函数

函数具体如何被使用,在下文对部分函数有重点论述。

绑定端口

绑定端口是在 initListeners() 函数总完成的。

c 复制代码
//调用链,这里我们仅考虑 tcp socket
 initListeners 
 //1, 绑定端口
 -> connListen(listener)
     //这里会调用前面 CT_Socket 注册的 connSocketListen 函数
     -> listener->ct->listen(listener)  
         -> connSocketListen(connListener *listener)
             -> listenToPort(connListener *sfd)
                 -> anetTcpServer(...)
                     -> _anetTcpServer(....) 

//2. 绑定新链接到来的处理函数
-> createSocketAcceptHandler(....)
      //accept_handler 是前面 CT_Socket 注册的 connSocketAcceptHandler
    -> createSocketAcceptHandler(connListener *sfd, aeFileProc *accept_handler)
        -> aeCreateFileEvent(server.el, sfd->fd[j], AE_READABLE, accept_handler,sfd)
            //调用底层 api 注册
            -> aeApiAddEvent(....)

下面重点讲解 aeCreateFileEvent,方法申明如下。它的含义是为 fdmask 类型的事件绑定 *proc 函数,并且将 clientData 也绑定上。最后将此事件绑定到事件循环。

c 复制代码
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
        aeFileProc *proc, void *clientData)

在此次调用中

  • *eventLoop 是 server.el
  • fd 是监听端口对应的文件描述符
  • mask 是可读性事件,即新链接到来事件。
  • *proc 是 accept_handler ,即 CT_Socket.connSocketAcceptHandler,后面讲到新连接处理的时候会着重讲解这个函数。
  • *clientData 是 connListener

总结:这个阶段绑定了端口,注册了事件,绑定了处理函数。注意:创建事件循环是在此之前。

其他组件初始化

服务初始化工作是在 initServer(void) 函数中完成的。

c 复制代码
# server.c
void initServer(void) {
    //获取当前 redis 进程 id
    server.pid = getpid();
    
    //创建存储客户端的列表
    server.clients = listCreate();
    
    //创建事件循环结构
    server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);
    
    //创建数据库
    server.db = zmalloc(sizeof(redisDb)*server.dbnum);
    for (j = 0; j < server.dbnum; j++) {
        //初始化数据库
    }
    
    //发布订阅功能组件
    server.pubsub_channels = dictCreate(&keylistDictType);
    server.pubsub_patterns = dictCreate(&keylistDictType);
}

创建事件循环

aeCreateEventLoop(...) 就是创建 aeEventLoop 结构,它是 Redis 对操作系统 io 实现的封装,在 linux 操作系统上是对 epoll 的封装。 redis 使用条件编译指令兼容各个操作系统。

c 复制代码
#ifdef HAVE_EVPORT
#include "ae_evport.c"
#else
    #ifdef HAVE_EPOLL
    #include "ae_epoll.c"   //linux epoll
    #else
        #ifdef HAVE_KQUEUE
        #include "ae_kqueue.c"
        #else
        #include "ae_select.c"
        #endif
    #endif
#endif

aeEventLoop 主要结构有

  • events 存储所有事件
  • fired 存储就绪事件
  • apidata 代表操作系统 io

下面是创建流程

c 复制代码
//aeCreateEventLoop
    -> eventLoop->events = zmalloc(sizeof(aeFileEvent)*setsize);
    -> eventLoop->fired = zmalloc(sizeof(aeFiredEvent)*setsize);
    -> aeApiCreate(eventLoop)
        -> state->events = zmalloc(sizeof(struct epoll_event)*eventLoop->setsize);
        //创建 epoll 
        ->  state->epfd = epoll_create(1024); /* 1024 is just a hint for the kernel */

创建数据库

在 redis 中,每个 db 中真实存储数据的数据库就是一个字典,默认 16 个。下面代码中 server.db[j].dict 真实存储 key/value 数据。server.db[j].expires 存储 key 和过期时间。

c 复制代码
for (j = 0; j < server.dbnum; j++) {
    server.db[j].dict = dictCreate(&dbDictType);
    server.db[j].expires = dictCreate(&dbExpiresDictType);
    //....其他属性
}

dict 在创建时需要指定 dictType,比如 server.db[j].dict 指定的 dictType

c 复制代码
/* Db->dict, keys are sds strings, vals are Redis objects. */
dictType dbDictType = {
    dictSdsHash,                /* hash function */
    dictSdsKeyCompare,          /* key compare */
    dictSdsDestructor,          /* key destructor */
    dictObjectDestructor,       /* val destructor */
    dictExpandAllowed,          /* allow to expand */
    //其他
};
  • hashFunction hash函数,负责将 key 映射到字典中的位置。
  • keyCompare key比较函数
  • keyDestructor key析构函数(回收)
  • valDestructor val析构函数(回收)

上面介绍的函数会在数据库的操作中使用到。

多线程初始化

这里说的多线程是指 redis io 处理的多线程,多线程如何与主线程一起处理 io 会在后面的文章中详细介绍。

c 复制代码
-> InitServerLast(void)
    ->  initThreadedIO();

启动事件循环

在所有基础组件初始化好了以后,就可以启动事件循环,并且处理就绪事件。从此开始 redis 可以对外提供服务了。

c 复制代码
-> aeMain(server.el);
    -> aeProcessEvents(....)
        -> aeApiPoll(eventLoop, tvp);
            -> epoll_wait(.....)
    -> fe->rfileProc(eventLoop,fd,fe->clientData,mask);
    -> fe->wfileProc(eventLoop,fd,fe->clientData,mask);

aeApiPoll 函数会调用底层实现,例如在 linux 平台使用 epoll

c 复制代码
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
    aeApiState *state = eventLoop->apidata;
    int retval, numevents = 0;

    //调用epoll_wait
    retval = epoll_wait(state->epfd,state->events,eventLoop->setsize,
            tvp ? (tvp->tv_sec*1000 + (tvp->tv_usec + 999)/1000) : -1);
    if (retval > 0) {
        int j;
        numevents = retval;
        for (j = 0; j < numevents; j++) {
            int mask = 0;
            struct epoll_event *e = state->events+j;
            //将就绪事件保存到封装的结构中
            eventLoop->fired[j].fd = e->data.fd;
            eventLoop->fired[j].mask = mask;
        }
    }
    return numevents;
}

根就绪事件的 fd 找到对应注册过的事件对象。

c 复制代码
int fd = eventLoop->fired[j].fd;
aeFileEvent *fe = &eventLoop->events[fd];

fe->rfileProcfe->wfileProc 是在事件初始化的时候创建的。具体来说,对于服务端 socket 是在绑定端口时对可读性事件绑定的 accept_handler 函数,见 端口绑定 小节。客户端连接 socket 见后面 新连接到来

请求处理

新连接到来

如果 FileEvent 对应的是服务端 socket ,那么rfileProc 是绑定端口时注册的 connSocketAcceptHandler 函数。

c 复制代码
-> fe->rfileProc(eventLoop,fd,fe->clientData,mask); 
   -> anetTcpAccept(....)
       -> anetGenericAccept(...)
   -> connCreateAcceptedSocket(....)
       -> connCreateSocket()
   -> acceptCommonHandler(....)
       -> createClient(conn))
  • anetTcpAccept 获取连接的 fd

  • connCreateAcceptedSocket 创建 redis 连接对象,并且为连接绑定了 CT_SocketCT_Socket 很重要,它包含很多 io 处理函数,这些函数会在事件就绪的时候调用。在绑定端口时也为 server socket 绑定了CT_Socket

    c 复制代码
    conn->type = &CT_Socket;
  • acceptCommonHandler 中最重要的是创建连接客户端端对象。

创建客户端连接

c 复制代码
client *createClient(connection *conn){
    //创建客户端
    client *c = zmalloc(sizeof(client));
    if (conn) {
        //设置 tcp 连接的属性
        connEnableTcpNoDelay(conn);
        if (server.tcpkeepalive)
            connKeepAlive(conn,server.tcpkeepalive);
        //为连接绑定读函数,当连接可读时会调 readQueryFromClient 函数,此函数会在后面介绍
        connSetReadHandler(conn, readQueryFromClient);
        //保存客户端对象到连接对象
        connSetPrivateData(conn, c);
    }
    
    //其他重要属性初始化
    //写缓冲区
    c->buf = zmalloc_usable(PROTO_REPLY_CHUNK_BYTES, &c->buf_usable_size);
    
    //默认查询为 0 的db,
    selectDb(c,0);
    
    //保存连接对象到客户端对象
    c->conn = conn;
    
    //客户端名称
    c->name = NULL;
    
    //读缓冲区
    c->querybuf = sdsempty();
    
    //命令包含的参数个数和命令
    c->argc = 0;
    c->argv = NULL;
}

connSetReadHandler(conn, readQueryFromClient) 注册读处理函数。其中 conn->type->ae_handler 是函数connSocketEventHandler 。当处理读事件时,调用顺序是 connSocketEventHandler (fe->rfileProc) 调用 readQueryFromClient

c 复制代码
static int connSocketSetReadHandler(connection *conn, ConnectionCallbackFunc func) {
    conn->read_handler = func;
    aeCreateFileEvent(server.el,conn->fd,AE_READABLE,conn->type->ae_handler,conn) == AE_ERR) return C_ERR;
    return C_OK;
}

SET 命令

经历上面一系列准备之后,服务端可以处理客户端命令了,执行下面的命令。

sql 复制代码
SET FOO 1

当客户端的命令发送到服务端时会触发可读事件。调用函数链是 fe->rfileProc(connSocketEventHandler) -> conn->read_handler(readQueryFromClient),接下来把重点放在 readQueryFromClient 函数上。

要处理一个命令分 3 个步骤 :

  • 读取数据到客户缓冲区
  • 根据 redis 协议解析出 redis 命令
  • 执行 redis 命令

下面的代码是读取数据到客户端缓冲区,c->querybuf+qblen 是缓冲区空闲空间的起始位置,readlen 表示缓冲区空闲的大小。

c 复制代码
nread = connRead(c->conn, c->querybuf+qblen, readlen);

命令解析

命令解析就是按照 redis 协议将二进制数据转成 redis 命令的字符串数组形式。

processInlineBuffer 函数中对命令进行解析。调用链为 readQueryFromClient -> processInputBuffer -> processInlineBuffer

相关代码如下

c 复制代码
//读取和解析命令
aux = sdsnewlen(c->querybuf+c->qb_pos,querylen);
argv = sdssplitargs(aux,&argc);

//将解析的参数存储到客户端对象中
for (c->argc = 0, j = 0; j < argc; j++) {
    c->argv[c->argc] = createObject(OBJ_STRING,argv[j]);
    c->argc++;
    c->argv_len_sum += sdslen(argv[j]);
}

命令处理

在 redis 中一个命令对应一个处理函数,比如 SET 命令对应 setCommand 函数。在前面我们已经获取到命令,解析就是查找对应的函数,并且调用函数即可

命令调用

命令调用函数链为

c 复制代码
-> processInputBuffer(....)
    -> processCommandAndResetClient(...)
        -> processCommand(...)
          //根据命令查找函数
            -> c->cmd = c->lastcmd = c->realcmd = lookupCommand(c->argv,c->argc);
                //commands 是包含了所有函数的字典
                -> lookupCommandLogic(server.commands,argv,argc,0);
        -> call(c,flags);
            -> c->cmd->proc(c);

commands 是包含了所有命令的字典,key 是命令的名称。在 src/commands.def 文件的 redisCommandTable[] 定义了所有的命令。在 populateCommandTable 函数中对 commands 进行了初始化。

SET 命令执行

命令在 setCommand 函数中执行,真正执行命令的是 setGenericCommand 函数。

c 复制代码
-> setGenericCommand
    -> setKey(c,c->db,key,val,setkey_flags);
        -> dbAdd(db,key,val);
            -> dbAddInternal(db, key, val, 0);
                -> dictEntry *de = dictAddRaw(db->dict, key->ptr, &existing);
                -> dictSetVal(db->dict, de, val);
    //如果设置了过期时间
    -> setExpire(c,c->db,key,milliseconds);

上面是 SET 命令的主体逻辑,更多的 redis 命令处理源码期待后面的文章。

总结

本文从源码级别介绍 redis 启动的详细步骤和 redis 处理客户端命令的全过程。希望本文对 redis 源码感兴趣的朋友有些许帮助。

参考资料

相关推荐
代码吐槽菌4 分钟前
基于SpringBoot的在线点餐系统【附源码】
java·开发语言·spring boot·后端·mysql·计算机专业
X² 编程说1 小时前
14.面试算法-字符串常见算法题(三)
java·数据结构·后端·算法·面试
请不要叫我菜鸡2 小时前
Go语言基础学习02-命令源码文件;库源码文件;类型推断;变量重声明
linux·后端·golang·类型推断·短变量·变量重声明·库源码文件
AskHarries2 小时前
Spring Boot集成Akka Cluster快速入门Demo
java·spring boot·后端·akka
少喝冰美式2 小时前
【大模型教程】如何在Spring Boot中无缝集成LangChain4j,玩转AI大模型!
人工智能·spring boot·后端·langchain·llm·ai大模型·计算机技术
lucifer3113 小时前
Spring Boot 中的配置属性绑定机制详解
java·后端
cci3 小时前
Rust gRPC---Tonic教程
后端·rust·grpc
谢林v3 小时前
Explain的简单使用
后端
lucifer3113 小时前
深入理解 Spring Boot 自动配置原理
java·后端
岁岁岁平安3 小时前
《飞机大战游戏》实训项目(Java GUI实现)(设计模式)(简易)
后端·游戏·设计模式·飞机大战·java-gui