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 [email protected]: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 源码感兴趣的朋友有些许帮助。

参考资料

相关推荐
Asthenia04121 分钟前
理解词法分析与LEX:编译器的守门人
后端
uhakadotcom2 分钟前
视频直播与视频点播:基础知识与应用场景
后端·面试·架构
Asthenia04121 小时前
Spring扩展点与工具类获取容器Bean-基于ApplicationContextAware实现非IOC容器中调用IOC的Bean
后端
bobz9651 小时前
ovs patch port 对比 veth pair
后端
Asthenia04121 小时前
Java受检异常与非受检异常分析
后端
uhakadotcom2 小时前
快速开始使用 n8n
后端·面试·github
JavaGuide2 小时前
公司来的新人用字符串存储日期,被组长怒怼了...
后端·mysql
bobz9652 小时前
qemu 网络使用基础
后端
Asthenia04122 小时前
面试攻略:如何应对 Spring 启动流程的层层追问
后端
Asthenia04123 小时前
Spring 启动流程:比喻表达
后端