Redis 学习笔记

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 进行解析

根据 typeencoding 的不同, 该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中会给出命令的执行结果。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值