Redis 笔记
数据结构
Redis 有两级的数据结构, 对外的数据结构由 RedisObject 表示:
简称robj, 是数据的对外表示形式, 其具体实现根据不同的情况有所不同.
// robj对象的构成
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
* LFU data (least significant 8 bits frequency
* and most significant 16 bits access time). */
int refcount;
void *ptr;
} robj;
- type: 该对象的类型, 可以看出Redis 对外支持的类型包括以下5种,注意数字类型也是用String 数据结构进行储存
/* The actual Redis Object */
#define OBJ_STRING 0 /* String object. */
#define OBJ_LIST 1 /* List object. */
#define OBJ_SET 2 /* Set object. */
#define OBJ_ZSET 3 /* Sorted set object. */
#define OBJ_HASH 4 /* Hash object. */
-
encoding : 编码格式,不同数据结构的底层实现方案
-
lru: 对象的touch时间,LRU算法使用
-
refcount: 引用计数,记录该对象的引用次数,当引用归零时释放
-
ptr: 该对象的实体,根据type 以及encoding 进行解析
根据 type 和 encoding 的不同, 该robj 的数据对应了以下某种基础数据结构
基础数据结构
IntSet 数字集
- __ inset.h / inset.c__ , zset的底层实现
intset 是一个有序的数字集,或者说数组,可以通过二分法进行查找,其特点是可以根据添加的数字进行动态调整大小,可以节省内存占用
typedef struct intset {
uint32_t encoding;
uint32_t length;
int8_t contents[];
} intset;
- encoding 是该intset的编码类型,也就是每个数字所占的字节数
其中包括三个类型 int16_t, int32_t 以及 int64_t - length 是当前intset包含的数字个数
- contents 数字集的储存位置
当Add添加的新数字超过原intset的编码类型时,会触发升级过程,例如将原本的uint16_t数组升级成uint32_t数组.
当然即使没有触发升级操作,在数组中间插入一个数字也会产生后半个数组的迁移
注从后先前进行拷贝可以原地进行迁移.
删除数字不会触发降级操作
/* Upgrades the intset to a larger encoding and inserts the given integer. */
static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
uint8_t curenc = intrev32ifbe(is->encoding);
uint8_t newenc = _intsetValueEncoding(value);
int length = intrev32ifbe(is->length);
int prepend = value < 0 ? 1 : 0;
/* First set new encoding and resize */
is->encoding = intrev32ifbe(newenc);
is = intsetResize(is,intrev32ifbe(is->length)+1);
/* Upgrade back-to-front so we don't overwrite values.
* Note that the "prepend" variable is used to make sure we have an empty
* space at either the beginning or the end of the intset. */
while(length--) //从后向前赋值,可以原地完成迁移
_intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));
/* Set the value at the beginning or the end. */
if (prepend)
_intsetSet(is,0,value);
else
_intsetSet(is,intrev32ifbe(is->length),value);
is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
return is;
}
dict 哈希表
- dict.h, dict.c, Hash 和Set的底层实现
使用链表法构建的hash表,当负载率过高时,进行渐进的reshash操作.
typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2];
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
int16_t pauserehash; /* If >0 rehashing is paused (<0 indicates coding error) */
} dict;
dict 数据结构包含了两组桶, ht[2],以及一个 rehashidx 记录当前rehash操作的进度(第几个桶). 当负载率过高时, rehash 操作进行中,每次Get和Set 都会从旧表中移动1个桶到新表,进而分摊复制时间.
zskiplist 跳表
ziplist 压缩链表
- ziplist.h
压缩链表是为了减少内存占用而开发出来的数据结构,因为是链表但本质的上还是使用一段连续的空间进行存储,因此插入和删除至少是O(N)的复杂度,又由于是压缩编码,每个节点的变化都可能会影响整个链表值的调整,因此实际的复杂度可能会接近O(N^2) 优点是极大的减少了内存使用.
与levelDB的前缀压缩还不同,前缀压缩由于有一定的startpoint, 插入复杂度不会太高
/* Each entry in the ziplist is either a string or an integer. */
typedef struct {
/* When string is used, it is provided with the length (slen). */
unsigned char *sval; //ziplist的每一个节点是数字或者字符串,当为数字时sval 为nullptr;
unsigned int slen;
/* When integer is used, 'sval' is NULL, and lval holds the value. */
long long lval;
} ziplistEntry;
压缩链表是一个比较重要的数据结构, 可以作为对外数据结构List 和 Hash的底层实现之一
###emsString
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* used */
uint8_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
需要注意的是结构体内定义的char buf[] 是不占内存的,并不是将buf 作为指针分配4个字节,
这种写法与 char buf* 具有天壤之别
/* Create a string object with encoding OBJ_ENCODING_EMBSTR, that is
* an object where the sds string is actually an unmodifiable string
* allocated in the same chunk as the object itself. */
robj *createEmbeddedStringObject(const char *ptr, size_t len) {
robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr8)+len+1);
struct sdshdr8 *sh = (void*)(o+1);
o->type = OBJ_STRING;
o->encoding = OBJ_ENCODING_EMBSTR;
o->ptr = sh+1;
o->refcount = 1;
if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
o->lru = (LFUGetTimeInMinutes()<<8) | LFU_INIT_VAL;
} else {
o->lru = LRU_CLOCK();
}
sh->len = len;
sh->alloc = len;
sh->flags = SDS_TYPE_8;
if (ptr == SDS_NOINIT)
sh->buf[len] = '\0';
else if (ptr) {
memcpy(sh->buf,ptr,len);
sh->buf[len] = '\0';
} else {
memset(sh->buf,0,len+1);
}
return o;
}
数据库服务
Redis 数据库结构体, 一个Server 会包含多个数据库, 默认情况下会启动16个数据库, 由server.dbnum参数定义, 用户可以通过SELECT命令来切换数据库进行操作
/* Redis database representation. There are multiple databases identified
* by integers from 0 (the default database) up to the max configured
* database. The database number is the 'id' field in the structure. */
typedef struct redisDb {
dict *dict; /* The keyspace for this DB */
dict *expires; /* Timeout of keys with a timeout set */
dict *blocking_keys; /* Keys with clients waiting for data (BLPOP)*/
dict *ready_keys; /* Blocked keys that received a PUSH */
dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */
int id; /* Database ID */
long long avg_ttl; /* Average TTL, just for stats */
unsigned long expires_cursor; /* Cursor of the active expire cycle. */
list *defrag_later; /* List of key names to attempt to defrag one by one, gradually. */
} redisDb;
-
dict 是数据库的键值空间, 也就是用户对数据库操作的主入口.key值是String类型, 而value值是一个robj,根据添加的数据使用不同的数据结构.
-
expires 用于键值对的超时判定, 用户可以设置超时时间,所有设置了超时时间的的键值会被放到这里.
Redis 使用Lasy-free 和定时清理两种方式进行过期键值对的清除, 当操作一个key-value时, 首先要在这里寻找该键值,查看是否超时 -
watched_keys 用户可以对某个键进行监听,当监听的键值对被改变时,会设置dirty标志
#持久化
###RDB持久化
Redis的持久化, 由于Redis 是内存数据库, 为防止系统宕机导致的数据丢失问题,需要将当前的数据库写入文件, 再下次启动时从本地文件中读取数据进行恢复.
Redis可以使用SAVE 或者 BGSAVE 命令进行手动备份, 也就对应着两种持久化方法
同步与异步.
rdb.c 文件中的 rdbSaveRio 方法完成数据库持久化的主要工作,下面先梳理同步写入的步骤.
-
首先创建新文件, 命名格式为 “REDIS%04d”,RDB_VERSION,然后写入一些基本信息,包括内存占用情况, RDB_VERSION, 写入时间等等
-
对于每个数据库,遍历dict, 取出每个key-robj键值对, 依次写入失效时间, LRU:闲置时间,LFU: 最新使用频率等缓存信息, 以及节点本身的key和value
-
如果使用后台持久化,子进程和父进程之间会创建一条pipe管道, 子进程要每秒向父进程同步当前写入了多少个键值对.
-
写入 sha校验码和 crc校验码
为什么使用子进程进行持久化? 优缺点是什么?
1 子进程继承父进程的内存空间是 COW(cpoy-on-write) 的, 因此不需要使用锁结构,父进程在更新数据库时,会触发中断,操作系统会复制一份内存, 父进程只会更新自己的那一份,子进程保持不变, 因此不需要使用任何锁结构.
2 缺点, 大量更新的情况下内存空间要拷贝两份数据库,内存占用较高
相关函数 : rdbSaveBackground, rdbSave,rdbSaveRio, redisfork
恢复数据: loadDataFromDisk, rdbload
AOF (Append Only File)持久化方式
RDB持久化方式 是将整个数据库写入文件,而AOF是仅将操作指令进行持久化,因此 AOF记录方式是可以以追加的形式记录的。
1 相对于RDB 形式, AOF需要额外缓存一个buffer 用来存储用户指令,分布式部署时需要在服务器进程收集并记录命令,定时刷新到文件中,(flushAppendOnlyFile函数),类似LevelDB的LOG文件记录方式
恢复时需要按照建立时的路径再执行一遍,速度较慢, 但是由于增量更新的优点,可以省下极大的文件空间。
当服务器启动并尝试恢复AOF文件数据时, 会创建一个fake client来逐条执行从AOF文件中读取的指令,完成数据的恢复。
Redis 默认的配置下,AOF文件每1秒中刷新一次,当系统宕机时,最多丢失1s的数据。也可以将参数 aof_fsync 设置为 always,这样每执行一条指令,AOF文件就会追加一次。
服务端与客户端
Redis 分为服务端 和 客户端两个模块,支持网络远程访问,也支持TLS加密。因此在服务器初始化的时候会进行网络listen, accept等操作。而每个客户端的命令 以及返回的结果都会通过socket 进行传递。因此,Redis使用epoll方法,实现了一个事件驱动器。
事件驱动器
Redis 中 事件驱动相关的函数实现在 ae.c 文件中,aeMain 是整个Redis事件驱动的入口,它通过循环调用 aeProcessEvents 进行注册事件的处理,包括定时事件和文件事件。
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
aeProcessEvents(eventLoop, AE_ALL_EVENTS|
AE_CALL_BEFORE_SLEEP|
AE_CALL_AFTER_SLEEP);
}
}
aeProcessEvent 中首先遍历定时事件的链表,(该链表没有按照时间顺序进行排列)找到最早的定时时间,作为epoll_wait的超时时间。然后使用aeApiPol进行等待,而aeApiPoll实际上是对epoll_wait的一个简单的封装。等到epoll_wait超时返回或者触发文件事件返回之后,再处理定全部的定时事件。
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
int processed = 0, numevents;
/* Nothing to do? return ASAP */
if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0;
/* Note that we want to call select() even if there are no
* file events to process as long as we want to process time
* events, in order to sleep until the next time event is ready
* to fire. */
if (eventLoop->maxfd != -1 ||
((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {
int j;
struct timeval tv, *tvp;
int64_t usUntilTimer = -1;
// 获取定时任务作为 epoll_wait的超时时间
if (flags & AE_TIME_EVENTS && !(flags & AE_DONT_WAIT))
usUntilTimer = usUntilEarliestTimer(eventLoop);
if (usUntilTimer >= 0) {
tv.tv_sec = usUntilTimer / 1000000;
tv.tv_usec = usUntilTimer % 1000000;
tvp = &tv;
} else {
/* If we have to check for events but need to return
* ASAP because of AE_DONT_WAIT we need to set the timeout
* to zero */
if (flags & AE_DONT_WAIT) {
tv.tv_sec = tv.tv_usec = 0;
tvp = &tv;
} else {
/* Otherwise we can block */
tvp = NULL; /* wait forever */
}
}
if (eventLoop->flags & AE_DONT_WAIT) {
tv.tv_sec = tv.tv_usec = 0;
tvp = &tv;
}
if (eventLoop->beforesleep != NULL && flags & AE_CALL_BEFORE_SLEEP)
eventLoop->beforesleep(eventLoop);
/* Call the multiplexing API, will return only on timeout or when
* some event fires. */
numevents = aeApiPoll(eventLoop, tvp);
/* After sleep callback. */
if (eventLoop->aftersleep != NULL && flags & AE_CALL_AFTER_SLEEP)
eventLoop->aftersleep(eventLoop);
for (j = 0; j < numevents; j++) {
aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];
int mask = eventLoop->fired[j].mask;
int fd = eventLoop->fired[j].fd;
int fired = 0; /* Number of events fired for current fd. */
/* Normally we execute the readable event first, and the writable
* event later. This is useful as sometimes we may be able
* to serve the reply of a query immediately after processing the
* query.
*
* However if AE_BARRIER is set in the mask, our application is
* asking us to do the reverse: never fire the writable event
* after the readable. In such a case, we invert the calls.
* This is useful when, for instance, we want to do things
* in the beforeSleep() hook, like fsyncing a file to disk,
* before replying to a client. */
int invert = fe->mask & AE_BARRIER;
/* Note the "fe->mask & mask & ..." code: maybe an already
* processed event removed an element that fired and we still
* didn't processed, so we check if the event is still valid.
*
* Fire the readable event if the call sequence is not
* inverted. */
// 如果没有屏障, 我们先进行读操作
if (!invert && fe->mask & mask & AE_READABLE) {
fe->rfileProc(eventLoop,fd,fe->clientData,mask);
fired++;
fe = &eventLoop->events[fd]; /* Refresh in case of resize. */
}
// 进行写操作
/* Fire the writable event. */
if (fe->mask & mask & AE_WRITABLE) {
if (!fired || fe->wfileProc != fe->rfileProc) {
fe->wfileProc(eventLoop,fd,fe->clientData,mask);
fired++;
}
}
/* If we have to invert the call, fire the readable event now
* after the writable one. */
// 如果设置了屏障,先进行写操作,然后进行读操作
// fd是一个socket, 其同时有读取缓冲区和写入缓冲区
if (invert) {
fe = &eventLoop->events[fd]; /* Refresh in case of resize. */
if ((fe->mask & mask & AE_READABLE) &&
(!fired || fe->wfileProc != fe->rfileProc))
{
fe->rfileProc(eventLoop,fd,fe->clientData,mask);
fired++;
}
}
processed++;
}
}
/* Check time events */
// 此时由于超时,aeApiPoll 已经返回,下面处理定时任务
if (flags & AE_TIME_EVENTS)
processed += processTimeEvents(eventLoop);
return processed; /* return the number of processed file/time events */
}
//创建并加入一个文件事件,mask标志写入事件,读取时间,是否有屏障
// 如果对一个文件既监听写入,也监听读取,可以分两次添加,底层会利用epoll_ctl进行调整。
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
aeFileProc *proc, void *clientData);
// 创建一个定时事件,使用头插法插入到eventloop的定时时间链表里,finalizerProc是定时事件触发后调用,一般为下一个looper执行,允许进行部分资源的清理操作。
long long aeCreateTimeEvent(aeEventLoop *eventLoop, long long milliseconds,
aeTimeProc *proc, void *clientData,
aeEventFinalizerProc *finalizerProc)
##网络服务
无论是否是单机,Redis 都是以客户端-服务端的形式实现的,因此都是通过网络以及事件驱动来完成服务器与客户端的数据交互。
linux下端口监听分为三步,
第一步:创建一个socket并绑定一个或一组ip地址进行监听, 该socket不会进行数据的传输,它专门进行客户端的连接。
第二步:使用accept函数获取监听端口是否有客户端连接。返回值为合法的fd即为有客户端连接。
第三步:accept返回值即为创建好的链接,后续客户端和服务器在这个fd上进行通信。
Redis在第三步中为每个链接创建一个client 对象,用于保存数据库id, 监听键等一系列的客户端参数。
端口监听
使用listenToPort 创建一个网络端口监听,其中port 是的端口号,sfd作为返回值,是监听端口的fd数组。listenToPort 在initServer 函数中调用,分别创建TCP和TLS的端口监听。
void initServer(void) {
.......
/* Open the TCP listening socket for the user commands. */
if (server.port != 0 &&
listenToPort(server.port,&server.ipfd) == C_ERR) {
serverLog(LL_WARNING, "Failed listening on port %u (TCP), aborting.", server.port);
exit(1);
}
if (server.tls_port != 0 &&
listenToPort(server.tls_port,&server.tlsfd) == C_ERR) {
serverLog(LL_WARNING, "Failed listening on port %u (TLS), aborting.", server.tls_port);
exit(1);
}
.....
监听端口的数量由参数 server.bindaddr_count 以及 bindaddr 定义,默认会使用 {"", "-::"} 分别对IPV4和IPV6进行全地址监听,即sfd数组会返回两个fd。
listenToPort 又由_anetTcpServer实现,后者调用anetListen进行网络监听。
int listenToPort(int port, socketFds *sfd) {
int j;
char **bindaddr = server.bindaddr;
int bindaddr_count = server.bindaddr_count;
char *default_bindaddr[2] = {"*", "-::*"};
/* Force binding of 0.0.0.0 if no bind address is specified. */
if (server.bindaddr_count == 0) {
bindaddr_count = 2;
bindaddr = default_bindaddr;
}
for (j = 0; j < bindaddr_count; j++) {
char* addr = bindaddr[j];
int optional = *addr == '-';
if (optional) addr++;
if (strchr(addr,':')) {
/* Bind IPv6 address. */
sfd->fd[sfd->count] = anetTcp6Server(server.neterr,port,addr,server.tcp_backlog);
} else {
/* Bind IPv4 address. */
sfd->fd[sfd->count] = anetTcpServer(server.neterr,port,addr,server.tcp_backlog);
}
// 设定为非阻塞,这很重要,否则后面调用accept函数时会被阻塞
anetNonBlock(NULL,sfd->fd[sfd->count]);
anetCloexec(sfd->fd[sfd->count]);
sfd->count++;
}
...
}
static int _anetTcpServer(char *err, int port, char *bindaddr, int af, int backlog)
{
.....
for (p = servinfo; p != NULL; p = p->ai_next) {
if ((s = socket(p->ai_family,p->ai_socktype,p->ai_protocol)) == -1)
continue;
if (af == AF_INET6 && anetV6Only(err,s) == ANET_ERR) goto error;
if (anetSetReuseAddr(err,s) == ANET_ERR) goto error;
if (anetListen(err,s,p->ai_addr,p->ai_addrlen,backlog) == ANET_ERR) s = ANET_ERR;
goto end;
}
if (p == NULL) {
anetSetError(err, "unable to bind socket, errno: %d", errno);
goto error;
}
....
}
这样一个端口监听就完成了,但实际上光监听还不能完成一个完整的数据交换,还需要将监听的fd,创建文件读创建并放到event_loop中, initServer函数接下来会调用createSocketAcceptHandler 来创建文件事件。
void initServer(void) {
......
if (createSocketAcceptHandler(&server.ipfd, acceptTcpHandler) != C_OK) {
serverPanic("Unrecoverable error creating TCP socket accept handler.");
}
if (createSocketAcceptHandler(&server.tlsfd, acceptTLSHandler) != C_OK) {
serverPanic("Unrecoverable error creating TLS socket accept handler.");
}
......
}
int createSocketAcceptHandler(socketFds *sfd, aeFileProc *accept_handler) {
int j;
for (j = 0; j < sfd->count; j++) {
if (aeCreateFileEvent(server.el, sfd->fd[j], AE_READABLE, accept_handler,NULL) == AE_ERR) {
/* Rollback */
for (j = j-1; j >= 0; j--) aeDeleteFileEvent(server.el, sfd->fd[j], AE_READABLE);
return C_ERR;
}
}
return C_OK;
}
参数ipfd和tlsfd,就是由 listenToPort 创建的监听,而* acceptTcpHandler* 和 acceptTLSHandler 当监听的端口产生连接时的对应处理函数。由文件事件的创建可以看到,当监听的fd可读时,会调用传入的accept_handler进行处理。
下面我们来看一下acceptTcpHandler的实现:
/* Anti-warning macro... */
#define UNUSED(V) ((void) V)
void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
int cport, cfd, max = MAX_ACCEPTS_PER_CALL;
char cip[NET_IP_STR_LEN];
UNUSED(el);
UNUSED(mask);
UNUSED(privdata);
while(max--) {
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;
}
anetCloexec(cfd);
serverLog(LL_VERBOSE,"Accepted %s:%d", cip, cport);
acceptCommonHandler(connCreateAcceptedSocket(cfd),0,cip);
}
其中UNUSED是一个宏,防止因未使用改变量而导致的编译器警告。
anetTcpAccept 函数通过linux底层的accept函数获客户端的ip,端口等信息。返回的fd是服务器与客户端连接成功的socket,此时已经准备就绪可以进行读取写入。
由于前面已经使用anetNonBlock 将fd设置为非阻塞,因此这里不会进行等待,一旦没有连接了,会直接返回-1失败。一个监听可以同时监听多个客户端,因此这里需要使用while不停的调用 anetTcpAccept 进行连接,直到所有的连接请求被处理,或者达到单次的最大处理数量 MAX_ACCEPTS_PER_CALL. 如果还有剩余的链接,需要等下个looper进行处理。
获取到通信的fd之后,使用acceptCommonHandler, 函数创建一个client, 这里的client 并不是真正的客户端,真正的客户端程序在 redis-cli.c 文件里
static void acceptCommonHandler(connection *conn, int flags, char *ip) {
client *c;
char conninfo[100];
........
/* Create connection and client 创建client对象*/
if ((c = createClient(conn)) == NULL) {
serverLog(LL_WARNING,
"Error registering fd event for the new client: %s (conn: %s)",
connGetLastError(conn),
connGetInfo(conn, conninfo, sizeof(conninfo)));
connClose(conn); /* May be already closed, just ignore errors */
return;
}
/* Last chance to keep flags */
c->flags |= flags;
/* Initiate accept.
*
* Note that connAccept() is free to do two things here:
* 1. Call clientAcceptHandler() immediately;
* 2. Schedule a future call to clientAcceptHandler().
*
* Because of that, we must do nothing else afterwards.
*/
// 执行clientAcceptHandler, 集群使用
if (connAccept(conn, clientAcceptHandler) == C_ERR) {
char conninfo[100];
if (connGetState(conn) == CONN_STATE_ERROR)
serverLog(LL_WARNING,
"Error accepting a client connection: %s (conn: %s)",
connGetLastError(conn), connGetInfo(conn, conninfo, sizeof(conninfo)));
freeClient(connGetPrivateData(conn));
return;
}
}
CreateClient 函数实现
client *createClient(connection *conn) {
client *c = zmalloc(sizeof(client));
/* passing NULL as conn 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 (conn) {
connNonBlock(conn);
connEnableTcpNoDelay(conn);
if (server.tcpkeepalive)
connKeepAlive(conn,server.tcpkeepalive);
//设置读事件的处理函数
connSetReadHandler(conn, readQueryFromClient);
connSetPrivateData(conn, c);
}
selectDb(c,0);
.....
}
createClient 函数中的connSetReadHandler函数创建一个文件事件并放入loop中,由于为了复用函数,这里设计的比较复杂,Connection对象会根据器Type类型调用不同的实现,TCP 类型的函数指针指针定义在 CT_Socket 结构体里, 而TLS 则定义在 CT_TLS里。
// 创建文件事件,放入event_loop中,当fd可读时调用readQueryFromClient进行处理
static int connSocketSetReadHandler(connection *conn, ConnectionCallbackFunc func) {
if (func == conn->read_handler) return C_OK;
conn->read_handler = func;
if (!conn->read_handler)
aeDeleteFileEvent(server.el,conn->fd,AE_READABLE);
else
if (aeCreateFileEvent(server.el,conn->fd,
AE_READABLE,conn->type->ae_handler,conn) == AE_ERR) return C_ERR;
return C_OK;
}
当客户端发来请求时,通过注册的文件事件的处理函数connSocketEventHandler->readQueryFromClient->processInputBuffer 的路径对输入命令进行处理
命令处理
processInputBuffer 负责对输入的文本进行分割,将每个token 储存在client 对象的argv数组中,之后调用 processCommand 函数对输入的命令进行分析和处理,在该函数中有大量的rejectCommand的场景, 很多都是语法错误导致的,此外还有一些集群处理方案。
在单进程模式下,会调用call函数,从RedisCommandTabel中找到匹配的API函数执行命令,在对应Command API中会给出命令的执行结果。