前言
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.c
中 main
函数是整个文件的入口,重要组件的初始化都在这个阶段。 本小节只会介绍单机版的 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
,方法申明如下。它的含义是为 fd
的 mask
类型的事件绑定 *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->rfileProc
和 fe->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_Socket
。CT_Socket
很重要,它包含很多 io 处理函数,这些函数会在事件就绪的时候调用。在绑定端口时也为 server socket 绑定了CT_Socket
。cconn->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 源码感兴趣的朋友有些许帮助。