Redis原理学习:Redis主体流程分析
网上分析Redis源码的文章挺多,如黄健宏的《Redis设计与实现》就很详尽的分析了redis源码,很赞。前不久看到Paul Smith的较早年份的大作《Redis:under the hood》,受益匪浅,如此从整体上对redis原理有个大的把控,不过多纠结于细节,甚好。这里我用的版本是redis2.4.18版本,跟Paul Smith的版本有所不同,不过主体流程没有太多变化,这篇文章基本是对 《Redis:under the hood》的翻译和注解。
1 启动
让我们从 src/redis.c 中的main() 函数开始。
全局服务器状态初始化
首先, initServerConfig() 函数被调用。这部分主要是初始化 server 变量,它是一个 struct redisServer类型,用于保存redis服务器全局状态。
// redis.h:388
struct redisServer {
    pthread_t mainthread;
    int arch_bits;
    int port;
    char *bindaddr;
    char *unixsocket;
    mode_t unixsocketperm;
    int ipfd;
    int sofd;
    redisDb *db;
    list *clients; // 客户端列表
    dict *commands; // 命令字典,key为命令名如get,值为redisCommand类型。
    unsigned lruclock:22;        /* clock incrementing every minute, for LRU */
    unsigned lruclock_padding:10;
    ...
}
// redis.c:71
struct redisServer server;
redisServer结构体有很多成员,主要可以分为下面几种类型:
- 全局的服务器状态
- 统计信息
- 配置信息
- 复制信息
- 排序参数
- 虚拟内存配置,状态,I/O线程以及统计
- zip结构体(默认ziplist,zipmap大小等)
- event loop helpers
- pub/sub
initServerConfig()的作用是设置redis server的默认配置。
- 如设置redis的run_id,设置端口为6379。
- 设置db名为dump.rdb,pid文件目录为/var/run/redis.pid,关闭aof。
- 关闭daemon模式,过期数据淘汰策略为LRU。
- 设置保存策略 saveparams为 1小时有1个值变化就保存,5分钟有100个值变化保存以及1分钟有1万个值变化保存。
- 设置 double R_PosInf = 1.0/0.0,好吧,用python的时候除0.0会抛异常,C里面不会,这里会变成无穷大。同理,还可以设置无穷小等。
- 创建commandTableDictType类型的字典commands,然后调用populateCommandTable()函数来初始化redis命令集。redis的字典dict的实现采用的也是经典的哈希表实现,冲突的键通过哈希表串联。
- 设置慢日志记录条件,默认是REDIS_SLOWLOG_LOG_SLOWER_THAN即命令执行时间超过10000毫秒(10秒)才记录慢日志。慢日志记录条数默认为128。
设置redis命令表
上一节提到redis的命令存储在server.commands这个字典中,其中key为命令名字,value为 redisCommand 结构体,其定义如下:
// redis.c:73
struct redisCommand readonlyCommandTable[] = {
    {"get",getCommand,2,0,NULL,1,1,1},
    {"set",setCommand,3,REDIS_CMD_DENYOOM,NULL,0,0,0},
    {"setnx",setnxCommand,3,REDIS_CMD_DENYOOM,NULL,0,0,0},
    {"setex",setexCommand,4,REDIS_CMD_DENYOOM,NULL,0,0,0},
    {"append",appendCommand,3,REDIS_CMD_DENYOOM,NULL,1,1,1},
    {"strlen",strlenCommand,2,0,NULL,1,1,1},
    {"del",delCommand,-2,0,NULL,0,0,0},
    ...
}
// redis.c:561
typedef void redisCommandProc(redisClient *c);
// redis.c:563
struct redisCommand {
    char *name;  // 命令名,如get
    redisCommandProc *proc; // 命令对应函数,如getCommand
    int arity; // 参数个数,如2
    int flags; // 标记,如set命令为REDIS_CMD_DENYOOM,表示在内存不够时不再处理set命令。
    redisVmPreloadProc *vm_preload_proc;
    int vm_firstkey; /* The first argument that's a key (0 = no keys) */
    int vm_lastkey;  /* THe last argument that's a key */
    int vm_keystep;  /* The step between first and last key */
};
其中 readonlyCommandTable数组就是命令集合,redisCommand各字段分别是命名名,命令函数,参数个数,oom标记以及vm相关参数。
解析配置文件并更新配置
接下来会判断启动参数个数,如果参数个数为2:
- 第二个参数是-v/--version则显示版本信息,若是--help显示帮助信息。如果是其他,则标识是配置文件,则解析配置文件并更新server的配置。
- 如果超过2个参数,则会判断是否是测试内存的命令,如果是,则测试内存,否则显示帮助信息。
解析配置文件是loadServerConfig()函数完成的,通过fgets一行行读取redis配置文件并更新服务器的配置。在这个函数里面可以看到若干的if else语句,一些所谓的编程书籍不提倡这样,包括goto使用等,然而大师级的程序员并不在意这些细节,所谓编程无定法,境界高就是可以为所欲为的。
从代码中可以发现,redis配置也可以不指定配置文件,而是在标准输入指定,运行src/redis-server -,然后在命令行输入配置即可。
开启daemon
如果配置了以守护进程运行,则会调用daemonize()函数通过fork()创建子进程然后子进程调用setsid()创建一个新的会话,并将标准输入输出,错误输出冲形象到/dev/null。
有些书上会写运行守护进程要fork()两次,其实通过setsid()创建了新的会话的话,没有必要fork()两次,redis就是这么做的。
如果以守护进程运行,后面还需调用createPidFile()创建pid文件,默认路径是/var/run/redis.pid。
初始化服务器
在上面步骤完成后,就调用initServer()函数来初始化服务器了。
信号设置
先是信号设置。忽略SIGHUP, SIGPIPE信号,然后通过setupSignalHandlers()设置TERM信号等处理函数为 sigtermHandler()等。
成员初始化
接着是初始化server成员变量,如客户端链表clients,slave链表slaves,这些列表都是双向链表struct list。
创建事件循环对象
接着是调用aeCreateEventLoop()创建Event Loop 并赋值给 server.el变量。它的类型是 aeEventLoop,定义如下:
// ae.h:89
typedef struct aeEventLoop {
    int maxfd;
    long long timeEventNextId;
    aeFileEvent events[AE_SETSIZE]; /* Registered events */
    aeFiredEvent fired[AE_SETSIZE]; /* Fired events */
    aeTimeEvent *timeEventHead;
    int stop;
    void *apidata; /* This is used for polling API specific data */
    aeBeforeSleepProc *beforesleep;
} aeEventLoop;
/* File event structure */
typedef struct aeFileEvent {
    int mask; /* one of AE_(READABLE|WRITABLE) */
    aeFileProc *rfileProc;
    aeFileProc *wfileProc;
    void *clientData;
} aeFileEvent;
/* Time event structure */
typedef struct aeTimeEvent {
    long long id; /* time event identifier. */
    long when_sec; /* seconds */
    long when_ms; /* milliseconds */
    aeTimeProc *timeProc;
    aeEventFinalizerProc *finalizerProc;
    void *clientData;
    struct aeTimeEvent *next;
} aeTimeEvent;
/* A fired event */
typedef struct aeFiredEvent {
    int fd;
    int mask;
} aeFiredEvent;
aeEventLoop 包括时间事件链表头 timeEventHead ,文件事件数组 aeFileEvent数组,以及待处理的文件事件数组 aeFiredEvent(AE_SETSIZE为10240)。
可以看到文件事件结构体aeFileEvent中字段mask是标记,表示事件类型读/写。而rfileProc则是读取事件处理函数,而wfileProc是写事件处理函数。而待处理的文件事件 aeFiredEvent 则只包含了需要处理的文件描述符fd和它的读写标记mask。
而时间事件则是 aeTimeEvent 类型,存储的包括时间事件ID,时间事件执行时间(秒 when_sec 和 毫秒 when_ms),此外还有时间事件的处理函数 timeProc 等。这是一个单向链表结构,next指向下一个时间事件,时间事件和文件事件最后都是在redis服务器的大循环中处理的。
服务器监听
如果指定了端口,则会启动anetTcpServer并开始监听。监听端口默认为6379,配置文件可以指定绑定的ip和端口。对应文件描述符为ipfd。如果是设置的unixsocket,则启动anetUnixServer,对应文件描述符为sofd。
这里跟我们平时写WEB服务器程序基本一致,只是稍作了封装,流程也是通用的socket(),bind(),listen()。
int anetTcpServer(char *err, int port, char *bindaddr)
{
    int s;
    struct sockaddr_in sa;
    if ((s = anetCreateSocket(err,AF_INET)) == ANET_ERR)
        return ANET_ERR;
    memset(&sa,0,sizeof(sa));
    sa.sin_family = AF_INET;
    sa.sin_port = htons(port);
    sa.sin_addr.s_addr = htonl(INADDR_ANY);
    if (bindaddr && inet_aton(bindaddr, &sa.sin_addr) == 0) {
        anetSetError(err, "invalid bind address");
        close(s);
        return ANET_ERR;
    }
    if (anetListen(err,s,(struct sockaddr*)&sa,sizeof(sa)) == ANET_ERR)
        return ANET_ERR;
    return s;
}
数据库
initServer() 中还完成了数据库初始化。默认是16个db,对应类型为dbDictType,id为0-15。此外还要初始化过期键字典expires,阻塞键字典 blocking_keys,观察键字典 watched_keys等。
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 *io_keys;              /* Keys with clients waiting for VM I/O */
    dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */
    int id;
} redisDb;
redis的持久化分为两种方式,rdb和aof。其中rdb是使用二进制格式存储数据,包括键值类型和数据,过期时间等,saveparams里面指定存储条件,详细格式说明见 Redis-RDB-Format。aof则是按redis命令存储,重启后可以按照aof中的命令重放恢复数据。
注册时间事件
接着在server.el中注册时间事件。这个就是1毫秒后要开始执行的事件 serverCron(),主要是为了在服务器启动后马上运行它,后面该函数100毫秒执行一次。这个函数要做很多事情,主要包括:
- 缓存当前时间,因为在LRU和VM访问对象时都要记录访问时间,每次调用 time(NULL)开销太大,而缓存这个时间并不会有很大影响。
- 更新LRUClock值,这个用于LRU策略。redisServer有一个全局的lruclock,该时钟每100ms更新一次。虽然lru用了22位,但是因为它最大为REDIS_LRU_CLOCK_MAX((1<<21)-1),其实是只用到21位,精度为10秒,所以它每242天会重新开始计时(跟redis源码注释中说的略有不同,注释说22位的话wrap时间为1.5年左右,但其实最大是用了21位)。而每个redisObject也有一个自己的 lruclock,这样在使用内存超过maxmemory之后就可以根据全局时钟和每个redisObject的时钟进行比较,确定是否淘汰。这里有个问题是,因为LRUClock每隔242天会重置,所以可能会导致一些很久没有访问的键它的lru更大,不过这个没有太大问题,一个键这么久没有访问,说明不太活跃。
- 如果达到了条件,执行BGSAVE(根据save配置来决定)和AOF文件重写。BGSAVE和AOF重写都是在子进程中执行的,这样不会影响redis主进程继续处理请求,见rdbSaveBackground()。注意,aof文件定期刷磁盘主要在beforeSleep中通过后台IO线程执行,serverCron只是在对aof刷磁盘操作推迟时做些处理。
- 打印统计信息。如key的数目,设置了过期时间的key的数目,连接的client数目,slave数目以及内存使用情况等,统计信息每50个循环(50*100ms=5秒)打印一次。
- 还有resize 哈希表,关闭超时客户端连接,后台的AOF重写,BGSAVE(如多少秒内有多少个键发生了变化执行的保存操作)。
- 计算LRU信息并删除一部分过期的键,如果开启了vm的话还要swap一些键值到磁盘上。
- 如果是slave,还需要从master同步数据。
注册文件事件
那之前我们创建了一个TcpServer而且已经开始监听,需要对客户端连接事件进行注册和处理。这在Linux上面是通过 epoll 来实现的。
aeCreateFileEvent()函数主要是设置aeFileEvent结构体的值,包括指定该文件事件是读还是写,根据读写事件指定对应的处理函数 rfileProc和 wfileProc。这里对tcp服务器指定的函数是 acceptTcpHandler()。最终都是通过 aeApiAddEvent() 函数使用 epoll_ctl() 将 tcp socket的fd注册到epoll中,客户端连接的命令处理都是在 acceptTcpHandler()中完成,这个函数后面分析。
// redis.c:980
aeCreateFileEvent(server.el,server.ipfd,AE_READABLE, acceptTcpHandler,NULL);
// ae.c:88
typedef struct aeFileEvent {
    int mask; /* one of AE_(READABLE|WRITABLE) */
    aeFileProc *rfileProc;
    aeFileProc *wfileProc;
    void *clientData;
} aeFileEvent;
// ae_epoll.c:29
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
    aeApiState *state = eventLoop->apidata;
    struct epoll_event ee;
    /* If the fd was already monitored for some event, we need a MOD
     * operation. Otherwise we need an ADD operation. */
    int op = eventLoop->events[fd].mask == AE_NONE ?
            EPOLL_CTL_ADD : EPOLL_CTL_MOD;
    ee.events = 0;
    mask |= eventLoop->events[fd].mask; /* Merge old events */
    if (mask & AE_READABLE) ee.events |= EPOLLIN;
    if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
    ee.data.u64 = 0; /* avoid valgrind warning */
    ee.data.fd = fd;
    if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1;
    return 0;
}
其他
- 如果配置了AOF的话,接着会打开AOF文件。
- 如果是32位机器,且没有配置maxmemory的话,会将maxmemory设置为3.5G,数据淘汰策略为 REDIS_MAXMEMORY_NO_EVICTION,这个选项的意思是禁止删除数据,永远不过期,只会对写操作返回一个错误,这也是默认的数据淘汰数据。
- 如果开启了vm,则会调用vmInit()初始化vm模块。
- 调用slowlogInit()初始化slowlog。
- 调用bioInit()初始化后台IO线程,一共两个后台线程,其中一个用于关闭文件,一个用于定期将AOF文件刷到磁盘。redis的AOF刷新策略有ALWAYS,EVERYSEC,NO,默认是EVERYSEC。ALWAYS是同步写入,会在进入每个事件循环后通过aof_fsync()(在linux下面就是fdatasync)同步到磁盘。而EVERYSEC则是通过aof_background_fsync()将刷磁盘操作加入到一个作业列表中(每秒执行一次),由bioInit创建的后台IO线程执行刷磁盘操作。设置为NO则不刷新磁盘,同步到磁盘的操作由操作系统决定。
- 调用 srand(time(NULL)^getpid())初始化随机数发生器。
恢复数据
如果开启了AOF,则从AOF文件重放命令来恢复redis数据。否则,则是从RDB文件恢复数据。每次进行RDB持久化时,redis都是将内存中的数据库的数据全部写到文件中,不是增量的持久化。
if (server.appendonly) {
    if (loadAppendOnlyFile(server.appendfilename) == REDIS_OK)
        redisLog(REDIS_NOTICE,"DB loaded from append only file: %ld seconds",time(NULL)-start);
} else {
    if (rdbLoad(server.dbfilename) == REDIS_OK) {
        redisLog(REDIS_NOTICE,"DB loaded from disk: %ld seconds",
            time(NULL)-start);
    } else if (errno != ENOENT) {
        redisLog(REDIS_WARNING,"Fatal error loading the DB. Exiting.");
        exit(1);
    }
}
建立事件循环
接着,redis注册beforeSleep()函数到事件循环中,这个函数在每次进入事件循环时首先调用它,它主要做两件事:
- 对开启vm的情况下,将那些请求已经交换到磁盘的key的客户端解除阻塞并处理这些客户端请求。
- 调用 flushAppendOnlyFile()将AOF文件刷到磁盘,最终调用的是aof_fsync()或者aof_background_fsync()。
进入事件循环
redis接着正式调用 aeMain() 函数进入事件循环。当有时间事件或者文件事件需要处理时,会调用他们对应的处理函数进行处理。aeProcessEvents()封装了处理函数,时间事件通过自定义的函数处理,而文件事件则通过epoll或者kqueue或者select系统调用来处理,在Linux里面通常使用的是epoll。
aeProcessEvents() 会优先处理文件事件,其次才是处理时间事件。文件事件就是通过 aeApiPoll() 函数来获取事件,并将触发的事件加入到 server.el.fired 数组中,最终就是调用epoll_wait()获取事件,其中超时时间设置的是距离最近一次时间事件的时间,这样如果没有文件事件也不会太耽误时间事件执行。获取到文件事件后,会根据事件类型是读还是写调用相应的方法处理。如读事件就是调用的 acceptTcpHandler() 处理的。
接着处理时间事件,从 server.el.timeEventHead 可以拿到时间事件链表的头,遍历该链表,如果有时间事件的执行时间到了,则执行对应的函数即可。这里有个地方注意下,如果时间事件处理函数返回值不是-1,则表示该时间事件需要定期执行,需要设置该事件下一次执行时间而不是从时间事件链表移除它,如serverCron这个时间事件,就是这样的定期执行事件,100ms执行一次。
2 请求处理和响应
现在服务器已经启动完毕了,接下来看看redis是如何在事件循环中接收客户端请求并处理请求的,这里以 TCP 方式为例分析,unix socket的类似。
接收连接
redis处理客户端请求是在函数 acceptTcpHandler() 完成的。这个函数通过 anetTcpAccept() 接收客户端请求,然后调用的 acceptCommandHandler() 来处理客户端请求。
在acceptCommandHandler最终调用的是 createClient(fd)函数创建了redisClient对象,初始化该对象的变量,并将该连接fd注册到事件循环中,事件类型为 AE_READABLE(EPOOLIN)。
该事件处理函数为 readQueryFromClient(),用于处理客户端连接的命令。这样,我们之前注册了listenfd,用于在新连接到来时接收新连接,现在将客户端连接fd注册到事件循环,完成客户端命令处理。注意这里必须通过anetNonBlock(NULL, fd)将客户端连接fd设置为非阻塞的。
void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
    int cport, cfd;
    char cip[128];
    cfd = anetTcpAccept(server.neterr, fd, cip, &cport);
    if (cfd == AE_ERR) {
        redisLog(REDIS_WARNING,"Accepting client connection: %s", server.neterr);
        return;
    }
    redisLog(REDIS_VERBOSE,"Accepted %s:%d", cip, cport);
    acceptCommonHandler(cfd);
}
redisClient *createClient(int fd) {
    redisClient *c = zmalloc(sizeof(redisClient));
    c->bufpos = 0;
    anetNonBlock(NULL,fd);
    anetTcpNoDelay(NULL,fd);
    if (aeCreateFileEvent(server.el,fd,AE_READABLE,
        readQueryFromClient, c) == AE_ERR)
    {
        close(fd);
        zfree(c);
        return NULL;
    }
    ...
}
从客户端读取命令
当客户端发送命令时,会通过readQueryFromClient()处理。它每次读取最多REDIS_IOBUF_LEN(16*1024)16K字节到缓存数组buf中,最后将缓存的数据拷贝到 redisClient->querybuf 中,然后调用 processInputBuffer() 函数处理客户端命令。
processInputBuffer() 解析客户端的原始命令字符串并将命令参数设置到 redisClient->argv 数组中,命令参数是 redisObject 类型的结构体。注意命令类型有两种,我们通过 redis-cli 发送的命令类型为 REDIS_REQ_MULTIBULK,这种命令以*开头,符合 redis protocol,调用processMultibulkBuffer()函数处理。另外一种命令是 REDIS_REQ_INLINE,这种命令是你通过其他工具连接的时候发的,比如通过 telnet localhost 6379,这种命令是直接的原生字符串,没有使用 redis protocol封装客户端命令。
void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {
    redisClient *c = (redisClient*) privdata;
    char buf[REDIS_IOBUF_LEN];
    int nread;
    server.current_client = c;
    nread = read(fd, buf, REDIS_IOBUF_LEN);
    ...
    processInputBuffer(c);
    server.current_client = NULL;
}
void processInputBuffer(redisClient *c) {
    while(sdslen(c->querybuf)) {
        /* Immediately abort if the client is in the middle of something. */
        if (c->flags & REDIS_BLOCKED || c->flags & REDIS_IO_WAIT) return;
        /* Determine request type when unknown. */
        if (!c->reqtype) {
            if (c->querybuf[0] == '*') {
                c->reqtype = REDIS_REQ_MULTIBULK;
            } else {
                c->reqtype = REDIS_REQ_INLINE;
            }
        }
        if (c->reqtype == REDIS_REQ_INLINE) {
            if (processInlineBuffer(c) != REDIS_OK) break;
        } else if (c->reqtype == REDIS_REQ_MULTIBULK) {
            if (processMultibulkBuffer(c) != REDIS_OK) break;
        } else {
            redisPanic("Unknown request type");
        }
        /* Multibulk processing could see a <= 0 length. */
        if (c->argc == 0) {
            resetClient(c);
        } else {
            /* Only reset the client when the command was executed. */
            if (processCommand(c) == REDIS_OK)
                resetClient(c);
        }
    }
}
在processInputBuffer()解析到完整命令后,便会调用 processCommand()函数开始处理客户端命令。先通过lookupCommand根据命令字符串找到命令的处理程序。在调用命令处理程序执行实际的命令前,会先执行一系列的检查:
- 如果命令不存在或者参数个数不对,返回错误。
- 如果redis配置了密码而客户端请求没有通过认证就发正式命令,返回错误。
- 比如如果当前是在一个 PUB/SUB上下文中,则只允许subscribe/unsubscribe等命令。
- 如果设置了最大内存maxmemory,则执行 SET等设置了REDIS_CMD_DENYOOM标识的命令时会返回错误。
- 如果redis服务器正在加载DB文件,则只允许info命令,其他命令报错。
- 如果是事务命令 MULTI,只要输入的不是exec,discard,multi,watch等命令,则将命令加入队列中以便后面批量执行。
执行命令
在上一节的检查OK后执行 call() 函数真正开始执行命令,它调用的是 redisCommand 的proc指向的函数,为 redisCommandProc 类型对象。命令执行完后,会通过 addReply() 函数将执行结果缓存到 redisClient 的 buf 数组中。
那这个响应数据什么时候会发送给客户端呢?这是因为在 addReply() 中会调用 _installWriteEvent(),该函数就是将客户端连接的fd加入到事件循环中,事件类型为AE_WRITABLE(EPOLLOUT),然后将响应数据通过_addReplyToBuffer()和_addReplyObjectToList()写入到响应缓存redisClient->buf和redisClient->reply中。这里的buf和reply两个地方都是用于写响应缓存的,如果响应的总的数据长度(响应数据长度+数据本身)小于 REDIS_REPLY_CHUNK_BYTES(7500)字节,则用buf数组缓存数据,否则用reply链表来存储数据。
当下一个事件循环到来时,会读取到该客户端连接fd,然后通过函数 sendReplyToClient()从响应缓存读取数据并发送响应数据给客户端,然后移除写事件。命令执行完成后,redis会重置redisClient对象并接收后续命令。
void call(redisClient *c) {
    long long dirty, start = ustime(), duration;
    dirty = server.dirty;
    c->cmd->proc(c);
    dirty = server.dirty-dirty;
    duration = ustime()-start;
    slowlogPushEntryIfNeeded(c->argv,c->argc,duration);
    if (server.appendonly && dirty > 0)
        feedAppendOnlyFile(c->cmd,c->db->id,c->argv,c->argc);
    if ((dirty > 0 || c->cmd->flags & REDIS_CMD_FORCE_REPLICATION) &&
        listLength(server.slaves))
        replicationFeedSlaves(server.slaves,c->db->id,c->argv,c->argc);
    if (listLength(server.monitors))
        replicationFeedMonitors(server.monitors,c->db->id,c->argv,c->argc);
    server.stat_numcommands++;
}
写操作如SET/ZADD等会让redis服务器变的dirty,后台IO线程执行的BGSAVE很重要,它会根据时间和修改过的key的数目来刷数据到rdb中。而如果开启了AOF还需要刷新AOF文件到磁盘。feedAppendOnlyFile 是将客户端命令写入到AOF文件中,所以可以通过AOF文件重放来恢复数据。当然这里会对expire,setex等命令做些转换,将过期时间设置为绝对时间,添加必要的SELECT db的命令等。另外则是直接通过 catAppendOnlyGenericCommand() 将命令写入到AOF文件。
如果有slave连接到该服务器,则通过 replicationFeedSlaves() 将命令发给slave服务器(slave同步时master会先通过BGSAVE保存一份rdb并发送给slave,后续的命令同步则由replicationFeedSlaves()来完成)。如果有客户端通过 monitor 命令连接到该服务器,则还要通过 replicationFeedMonitors() 发送命令字符串过去,并带上命令时间戳。
3 总结
这篇文章主要对redis的初始化流程做了分析,包括启动时配置初始化,使用epoll支持高并发,读取解析和响应命令等,流程图如下(来自 Redis:under the hood 博客)。这里还有篇 Paul Smith的大作 More Redis internals: Tracing a GET & SET 用于跟踪 redis 的 GET/SET 命令流程的,值得一看。

Redis启动流程
4 参考资料
- https://pauladamsmith.com/articles/redis-under-the-hood.html (基本是对这篇文章的翻译加注解)
- https://redis.io/topics/protocol (redis协议,看了可以比较深入理解redis客户端和服务端的交互)
Redis原理学习:Redis主体流程分析的更多相关文章
- Redis cluster学习 & Redis常识 & sort操作
		Redis中的5种数据类型String.Hash.List.Set.Sorted Set. Redis源码总代码一万多行. 这篇文章有一些Redis "常识" http://www ... 
- Netty 源码学习——服务端流程分析
		在上一篇我们已经介绍了客户端的流程分析,我们已经对启动已经大体上有了一定的认识,现在我们继续看对服务端的流程来看一看到底有什么区别. 服务端代码 public class NioServer { pr ... 
- Redis详细讲解(Redis原理,Redis安装,Redis配置,Redis使用,Redis命令)
		一.Redis介绍 Redis是一个开源的使用ANSI C语言编写.支持网络.可基于内存亦可持久化的日志型.Key-Value数据库,并提供多种语言的API.从2010年3月15日起,Redis的开发 ... 
- KdTree && Octree 原理学习对比以及可视化分析--"索引树"
		1. Kdtree 原理 k-d树(k-dimensional树的简称),是一种分割k维数据空间的数据结构.主要应用于多维空间关键数据的搜索(如:范围搜索和最近邻搜索): 索引结构中相似性查询有两种基 ... 
- Mybatis源码学习第六天(核心流程分析)之Executor分析
		今Executor这个类,Mybatis虽然表面是SqlSession做的增删改查,其实底层统一调用的是Executor这个接口 在这里贴一下Mybatis查询体系结构图 Executor组件分析 E ... 
- Mybatis源码学习第六天(核心流程分析)之Executor分析(补充)
		补充上一章没有讲解的三个Executor执行器; 还是贴一下之前的代码吧;我发现其实有些分析注释还是写在代码里面比较好,方便大家理解,之前是我的疏忽,不好意思 @Override public < ... 
- 并发编程原理学习-reentrantlock源码分析
		ReentrantLock基本概念  ReentrantLock是一个可重入锁,顾名思义,就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁,并且在获取锁时支持选择公平模式或者非公平模式 ... 
- Flask启动原理,源码流程分析
		1.执行Flask的实例对象.run()方法 from flask import Flask,request,session app = Flask(__name__) app.secret_key ... 
- SpringBoot启动流程分析(五):SpringBoot自动装配原理实现
		SpringBoot系列文章简介 SpringBoot源码阅读辅助篇: Spring IoC容器与应用上下文的设计与实现 SpringBoot启动流程源码分析: SpringBoot启动流程分析(一) ... 
- redis概要学习
		redis 概要学习 redis简介 Redis 是完全开源免费的,遵守BSD协议,是一个高性能的key-value数据库. Redis 与其他 key - value 缓存产品有以下三个特点: Re ... 
随机推荐
- [转帖]Linux 页表、大页与透明大页
			一. 内存映射与页表 1. 内存映射 我们通常所说的内存容量,指的是物理内存,只有内核才可以直接访问物理内存,进程并不可以. Linux 内核给每个进程都提供了一个独立的虚拟地址空间,并且这个地址空间 ... 
- [转帖]使用 TiUP 部署运维 TiDB 线上集群
			https://docs.pingcap.com/zh/tidb/stable/tiup-cluster 本文重在介绍如何使用 TiUP 的 cluster 组件,如果需要线上部署的完整步骤,可参考使 ... 
- [转帖]jmeter_采样器sampler简介
			1.取样器介绍 取样器是用来模拟用户操作的,向服务器发送请求以及接收服务器的响应数据. 取样器是在线程组内部的元件,也就是说取样器只能在线程组中添加. 取样器(Sampler)是性能测试中向服务器发送 ... 
- 境内下载nodejs二进制文件
			下载 nodejs 安装包的方法 找到一个境内的淘宝源 可以直接使用 下载速度还比较快 但是没有 龙芯的.. http://npm.taobao.org/mirrors/node/v10.20.0/ 
- SQLSERVER2008R2 本地客户端无法连接的问题
			sqlserver 数据库无法连接问题. 1. 问题现象与简单分析 今天同事找我说一个 SQLSERVER2008R2的测试环境无法连接了. 提示信息为: 当时猜测是 数据库的 配置出问题了. 去查看 ... 
- 《Javascript高级程序设计》读书笔记——继承与原型链
			继承与原型链 原型链 在原型那一节中,讲到了用于搜索对象属性的原型搜索机制:而原型链,本质上 就是对原型搜索机制的扩充: 回想下之前的内容,我们要读取一个Person的实例p属性,会先搜索实例p:如果 ... 
- 一文搞懂Redis
			作者: 京东物流 刘丽侠 姚再毅 康睿 刘斌 李振 一.Redis的特性 1.1 Redis为什么快? 基于内存操作,操作不需要跟磁盘交互,单次执行很快 命令执行是单线程,因为是基于内存操作,单次执行 ... 
- GIT专业术语教程-转载
			目录 一.版本控制概要 1.1.什么是版本控制 1.2.常用术语 1.3.常见的版本控制器 1.4.版本控制分类 1.4.1.本地版本控制 1.4.2.集中版本控制 1.4.3.分布式版本控制 1.5 ... 
- C# 输入指定日期获取当前年的第一天 、当前年的最后天、某月的第一天 、某月的最后一天
			方法 /// <summary> /// 取得当前年的第一天 /// </summary> /// <param name="datetime"> ... 
- 窗口管理器 dwm安装
			上一篇博文中,已经完成了archlinux的安装,但是进去仅仅是一个冰冷冷的交互式命令窗口.没有图像,也无法打开浏览器.离日常使用还差的很远,接下来首先需要做的就是安装桌面环境.这里我不打算使用诸如g ... 
