Redis源码(十)——Redis的客户端和服务器

本文详细解析了Redis客户端和服务器的工作原理,包括客户端结构、命令请求处理流程、服务器定时任务等核心内容,揭示了Redis高效运行的秘密。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在前面的博客中,有些内容已经涉及到了Redis服务器或者客户端的一些属性,如上一篇博客关于Redis的RDB持久化中关于save选项来设置服务器的状态等。那么接下来这篇博客中就分析下Redis的客户端以及服务器的属性及操作。

 

一、Redis客户端

redis.h中的redisClient结构定义了Redis的客户端:

/* With multiplexing we need to take per-clientstate.
 * Clientsare taken in a liked list.
 *
 * 因为 I/O 复用的缘故,需要为每个客户端维持一个状态。
 *
 * 多个客户端状态被服务器用链表连接起来。
 */
typedef struct redisClient {
 
    // 套接字描述符
    int fd;
 
    // 当前正在使用的数据库
    redisDb*db;
 
    // 当前正在使用的数据库的 id (号码)
    intdictid;
 
    // 客户端的名字
    robj*name;             /* As set by CLIENTSETNAME */
 
    // 查询缓冲区
    sdsquerybuf;
 
    // 查询缓冲区长度峰值
    size_tquerybuf_peak;   /* Recent (100ms ormore) peak of querybuf size */
 
    // 参数数量
    intargc;
 
    // 参数对象数组
    robj**argv;
 
    // 记录被客户端执行的命令
    structredisCommand *cmd, *lastcmd;
 
    // 请求的类型:内联命令还是多条命令
    intreqtype;
 
    // 剩余未读取的命令内容数量
    intmultibulklen;       /* number of multibulk arguments left to read */
 
    // 命令内容的长度
    longbulklen;           /* length of bulkargument in multi bulk request */
 
    // 回复链表
    list*reply;
 
    // 回复链表中对象的总大小
   unsigned long reply_bytes; /* Tot bytes of objects in reply list */
 
    // 已发送字节,处理 short write 用
    intsentlen;            /* Amount of bytes already sent in thecurrent
                               buffer or objectbeing sent. */
 
    // 创建客户端的时间
    time_tctime;           /* Client creation time*/
 
    // 客户端最后一次和服务器互动的时间
    time_tlastinteraction; /* time of the last interaction, used for timeout */
 
    // 客户端的输出缓冲区超过软性限制的时间
    time_tobuf_soft_limit_reached_time;
 
    // 客户端状态标志
    intflags;              /* REDIS_SLAVE |REDIS_MONITOR | REDIS_MULTI ... */
 
    // 当 server.requirepass 不为 NULL 时
    // 代表认证的状态
    // 0 代表未认证, 1 代表已认证
    intauthenticated;      /* when requirepassis non-NULL */
 
    // 复制状态
    intreplstate;          /* replication stateif this is a slave */
    // 用于保存主服务器传来的 RDB 文件的文件描述符
    intrepldbfd;           /* replication DBfile descriptor */
 
    // 读取主服务器传来的 RDB 文件的偏移量
    off_trepldboff;        /* replication DB fileoffset */
    // 主服务器传来的 RDB 文件的大小
    off_trepldbsize;       /* replication DB filesize */
   
    sdsreplpreamble;       /* replication DBpreamble. */
 
    // 主服务器的复制偏移量
    longlong reploff;      /* replication offsetif this is our master */
    // 从服务器最后一次发送 REPLCONF ACK 时的偏移量
    longlong repl_ack_off; /* replication ack offset, if this is a slave */
    // 从服务器最后一次发送 REPLCONF ACK 的时间
    longlong repl_ack_time;/* replication ack time, if this is a slave */
    // 主服务器的 master run ID
    // 保存在客户端,用于执行部分重同步
    charreplrunid[REDIS_RUN_ID_SIZE+1]; /* master run id if this is a master */
    // 从服务器的监听端口号
    intslave_listening_port; /* As configured with: SLAVECONF listening-port */
 
    // 事务状态
   multiState mstate;      /*MULTI/EXEC state */
 
    // 阻塞类型
    intbtype;              /* Type of blockingop if REDIS_BLOCKED. */
    // 阻塞状态
   blockingState bpop;     /*blocking state */
 
    // 最后被写入的全局复制偏移量
    longlong woff;         /* Last write globalreplication offset. */
 
    // 被监视的键
    list*watched_keys;     /* Keys WATCHED forMULTI/EXEC CAS */
 
    // 这个字典记录了客户端所有订阅的频道
    // 键为频道名字,值为 NULL
    // 也即是,一个频道的集合
    dict*pubsub_channels;  /* channels a clientis interested in (SUBSCRIBE) */
 
    // 链表,包含多个 pubsubPattern 结构
    // 记录了所有订阅频道的客户端的信息
    // 新 pubsubPattern 结构总是被添加到表尾
    list*pubsub_patterns;  /* patterns a client isinterested in (SUBSCRIBE) */
    sdspeerid;             /* Cached peer ID. */
 
    /*Response buffer */
    // 回复偏移量
    intbufpos;
    // 回复缓冲区
    charbuf[REDIS_REPLY_CHUNK_BYTES];
 
} redisClient;

Redis是一对多的服务器,可以有多个客户端,在Redis服务器中维护了一个clients链表,链表中保存着与服务器连接的所有客户端状态。redisClient结构中主要的属性如下:

1. fd套接字描述符

fd属性是客户端正在使用的套接字描述符,发的的取值可以是-1或者大于-1的整数,这与客户端的属性有关:伪客户端(fake client)fd属性为-1,目前Redis服务器在两个地方用到了伪客户端,一个用于载入AOF文件并还原数据库状态,另一个用于执行Lua脚本中的Redis命令;普通客户端的fd大于-1,普通的客户端使用套接字来与服务器进行通信。

2. flags客户端标志

flags属性可以是单个标志,也可以“或”多个标志,每个标志都代表一个常量:

/* Client flags */
#define REDIS_SLAVE (1<<0)   /* This client is a slave server */
#define REDIS_MASTER (1<<1)  /* This client is a master server */
#define REDIS_MONITOR (1<<2) /* This clientis a slave monitor, see MONITOR */
#define REDIS_MULTI (1<<3)   /* This client is in a MULTI context */
#define REDIS_BLOCKED (1<<4) /* The clientis waiting in a blocking operation */
#define REDIS_DIRTY_CAS (1<<5) /* Watchedkeys modified. EXEC will fail. */
#define REDIS_CLOSE_AFTER_REPLY (1<<6) /*Close after writing entire reply. */
#define REDIS_UNBLOCKED (1<<7) /* Thisclient was unblocked and is stored in
                                 server.unblocked_clients */
#define REDIS_LUA_CLIENT (1<<8) /* This isa non connected client used by Lua */
#define REDIS_ASKING (1<<9)     /* Client issued the ASKING command */
#define REDIS_CLOSE_ASAP (1<<10)/* Closethis client ASAP */
#define REDIS_UNIX_SOCKET (1<<11) /* Clientconnected via Unix domain socket */
#define REDIS_DIRTY_EXEC (1<<12)  /* EXEC will fail for errors while queueing*/
#define REDIS_MASTER_FORCE_REPLY(1<<13)  /* Queue replies even ifis master */
#define REDIS_FORCE_AOF (1<<14)   /* Force AOF propagation of current cmd. */
#define REDIS_FORCE_REPL (1<<15)  /* Force replication of current cmd. */
#define REDIS_PRE_PSYNC (1<<16)   /* Instance don't understand PSYNC. */
#define REDIS_READONLY (1<<17)    /* Cluster client is in read-only state. */

3. querybuf查询缓冲区

查询缓冲区保存着客户端发送的命令请求

4. argv命令参数

当查询缓冲区保存客户端发送的命令请求后,服务器会对命令请求的内容进行分析,得出的命令参数会放到argv数组中,命令参数的长度会保存在argc中。argv是一个数组,数组第0位放着执行的命令,后面则放着对应命令的参数,例如客户端发送“SET hello world”,那么argv数组中的状态如下:

argv[0]

argv[1]

argv[2]

StringObject

“SET”

StringObject

“hello”

StringObject

“world”

此时argc的值为3.

5. cmd命令

当获取命令的argv属性及argc属性后,服务器将根据argv[0]对应的命令在命令表中查找对应的命令实现。cmd是一个redisCommand结构

/*
 * Redis 命令
 */
struct redisCommand {
 
    // 命令名字
    char*name;
 
    // 实现函数
   redisCommandProc *proc;
 
    // 参数个数
    intarity;
 
    // 字符串表示的 FLAG
    char*sflags; /* Flags as string representation, one char per flag. */
 
    // 实际 FLAG
    intflags;    /* The actual flags, obtainedfrom the 'sflags' field. */
 
    /* Usea function to determine keys arguments in a command line.
     * Usedfor Redis Cluster redirect. */
    // 从命令中判断命令的键参数。在 Redis 集群转向时使用。
   redisGetKeysProc *getkeys_proc;
 
    /* Whatkeys should be loaded in background when calling this command? */
    // 指定哪些参数是 key
    intfirstkey; /* The first argument that's a key (0 = no keys) */
    intlastkey;  /* The last argument that's akey */
    intkeystep;  /* The step between first andlast key */
 
    // 统计信息
    //microseconds 记录了命令执行耗费的总毫微秒数
    // calls是命令被执行的总次数
    longlong microseconds, calls;
};

redis.c中定义了命令表:

struct redisCommand redisCommandTable[] = {
   {"get",getCommand,2,"r",0,NULL,1,1,1,0,0},
   {"set",setCommand,-3,"wm",0,NULL,1,1,1,0,0},
   {"setnx",setnxCommand,3,"wm",0,NULL,1,1,1,0,0},
   {"setex",setexCommand,4,"wm",0,NULL,1,1,1,0,0},
   {"psetex",psetexCommand,4,"wm",0,NULL,1,1,1,0,0},
   {"append",appendCommand,3,"wm",0,NULL,1,1,1,0,0},
   {"strlen",strlenCommand,2,"r",0,NULL,1,1,1,0,0},
   {"del",delCommand,-2,"w",0,NULL,1,-1,1,0,0},
   {"exists",existsCommand,2,"r",0,NULL,1,1,1,0,0},
   {"setbit",setbitCommand,4,"wm",0,NULL,1,1,1,0,0},
   {"getbit",getbitCommand,3,"r",0,NULL,1,1,1,0,0},
    ...
    ...
   {"pfselftest",pfselftestCommand,1,"r",0,NULL,0,0,0,0,0},
   {"pfadd",pfaddCommand,-2,"wm",0,NULL,1,1,1,0,0},
   {"pfcount",pfcountCommand,-2,"w",0,NULL,1,1,1,0,0},
   {"pfmerge",pfmergeCommand,-2,"wm",0,NULL,1,-1,1,0,0},
   {"pfdebug",pfdebugCommand,-3,"w",0,NULL,0,0,0,0,0}
};

6. buf[REDIS_REPLY_CHUNK_BYTES]回复缓冲区和bufpos回复偏移量,reply回复链表

当执行对应的命令后,Redis服务器会发送回复给客户端,这个回复就会保存在客户端的输出缓冲区中,每个客户端都有两个输出缓冲区,一个是大小固定的buf,另一个是大小可变的回复链表reply

buf的大小是REDIS_REPLY_CHUNK_BYTES,默认16*1024即16KB,bufpos保存着buf数组已经使用的字节偏移。当buf数组空间用完或者回复太大时,服务器将会把回复放至可变大小缓冲区reply中。

7. authenticated身份验证

当服务器开启身份验证时,即server.requirepass不为null时,如果客户端的authenticated为0,代表客户端未通过身份验证,此时除了AUTH身份验证命令外,其余所有的命令都会被服务器拒绝;只有当authenticated为1时,才可执行其他命令请求。

需要注意的是,这仅仅在服务器开启身份验证时生效,若服务器不开启身份验证,即使authenticated为0也可正常执行命令请求

 

二、Redis服务器

Redis服务器负责与多个客户端建立连接从而可以处理客户端的命令请求。

服务器的结构太长了,不列了,,,


1. 命令请求的执行过程

首先我们来分析从客户端发送请求到服务器执行后得到正确结果的整个过程:

1)首先客户端向服务器发送命令请求:

Redis客户端会将命令请求转换成协议格式,通过连接到服务器的套接字,将命令请求发送给服务器。

2)服务器接受请求,并进行相应操作,并发送命令回复OK给客户端:

服务器在将命令请求进行分析后,通过调用processCommand命令处理函数对命令请求进行处理,若处理成功会返回OK。

processCommand函数将会先查找命令,并进行命令合法性检查,以及命令参数个数检查;随后会通过一系列操作比如说:检查认证信息、是否超过服务器最大内存等确保命令正确地执行。processCommand函数具体逻辑如下:

/* If this function gets called we already read awhole
 * command,arguments are in the client argv/argc fields.
 *processCommand() execute the command or prepare the
 * serverfor a bulk read from the client.
 *
 * 这个函数执行时,我们已经读入了一个完整的命令到客户端,
 * 这个函数负责执行这个命令,
 * 或者服务器准备从客户端中进行一次读取。
 *
 * If 1 isreturned the client is still alive and valid and
 * otheroperations can be performed by the caller. Otherwise
 * if 0 isreturned the client was destroyed (i.e. after QUIT).
 *
 * 如果这个函数返回 1 ,那么表示客户端在执行命令之后仍然存在,
 * 调用者可以继续执行其他操作。
 * 否则,如果这个函数返回 0 ,那么表示客户端已经被销毁。
 */
int processCommand(redisClient *c) {
    /* TheQUIT command is handled separately. Normal command procs will
     * gothrough checking for replication and QUIT will cause trouble
     * whenFORCE_REPLICATION is enabled and would be implemented in
     * aregular command proc. */
    // 特别处理 quit 命令
    if (!strcasecmp(c->argv[0]->ptr,"quit")){
       addReply(c,shared.ok);
       c->flags |= REDIS_CLOSE_AFTER_REPLY;
       return REDIS_ERR;
    }
 
    /* Nowlookup the command and check ASAP about trivial error conditions
     * suchas wrong arity, bad command name and so forth. */
    // 查找命令,并进行命令合法性检查,以及命令参数个数检查
   c->cmd = c->lastcmd = lookupCommand(c->argv[0]->ptr);
    if(!c->cmd) {
        // 没找到指定的命令
       flagTransaction(c);
       addReplyErrorFormat(c,"unknown command '%s'",
           (char*)c->argv[0]->ptr);
       return REDIS_OK;
    } elseif ((c->cmd->arity > 0 && c->cmd->arity != c->argc)||
              (c->argc < -c->cmd->arity)) {
        // 参数个数错误
       flagTransaction(c);
       addReplyErrorFormat(c,"wrong number of arguments for '%s'command",
           c->cmd->name);
       return REDIS_OK;
    }
 
    /*Check if the user is authenticated */
    // 检查认证信息
    if(server.requirepass && !c->authenticated &&c->cmd->proc != authCommand)
    {
       flagTransaction(c);
       addReply(c,shared.noautherr);
       return REDIS_OK;
    }
 
    /* Ifcluster is enabled perform the cluster redirection here.
     *
     * 如果开启了集群模式,那么在这里进行转向操作。
     *
     *However we don't perform the redirection if:
     *
     * 不过,如果有以下情况出现,那么节点不进行转向:
     *
     * 1)The sender of this command is our master.
     *    命令的发送者是本节点的主节点
     *
     * 2)The command has no key arguments.
     *    命令没有 key 参数
     */
    if(server.cluster_enabled &&
       !(c->flags & REDIS_MASTER) &&
       !(c->cmd->getkeys_proc == NULL && c->cmd->firstkey== 0))
    {
        inthashslot;
 
        // 集群已下线
        if(server.cluster->state != REDIS_CLUSTER_OK) {
           flagTransaction(c);
           addReplySds(c,sdsnew("-CLUSTERDOWN The cluster is down. Use CLUSTERINFO for more information\r\n"));
           return REDIS_OK;
 
        // 集群运作正常
        }else {
           int error_code;
           clusterNode *n =getNodeByQuery(c,c->cmd,c->argv,c->argc,&hashslot,&error_code);
           // 不能执行多键处理命令
           if (n == NULL) {
               flagTransaction(c);
               if (error_code == REDIS_CLUSTER_REDIR_CROSS_SLOT) {
                    addReplySds(c,sdsnew("-CROSSSLOTKeys in request don't hash to the same slot\r\n"));
               } else if (error_code == REDIS_CLUSTER_REDIR_UNSTABLE) {
                    /* The request spawnsmutliple keys in the same slot,
                     * but the slot is not"stable" currently as there is
                     * a migration or import inprogress. */
                   addReplySds(c,sdsnew("-TRYAGAIN Multiple keys request duringrehashing of slot\r\n"));
               } else {
                   redisPanic("getNodeByQuery() unknown error.");
               }
               return REDIS_OK;
 
           // 命令针对的槽和键不是本节点处理的,进行转向
           } else if (n != server.cluster->myself) {
               flagTransaction(c);
                // -<ASK or MOVED> <slot><ip>:<port>
               // 例如 -ASK 10086 127.0.0.1:12345
               addReplySds(c,sdscatprintf(sdsempty(),
                    "-%s %d%s:%d\r\n",
                    (error_code ==REDIS_CLUSTER_REDIR_ASK) ? "ASK" : "MOVED",
                   hashslot,n->ip,n->port));
 
               return REDIS_OK;
           }
 
           // 如果执行到这里,说明键 key 所在的槽由本节点处理
           // 或者客户端执行的是无参数命令
        }
    }
 
    /*Handle the maxmemory directive.
     *
     *First we try to free some memory if possible (if there are volatile
     * keysin the dataset). If there are not the only thing we can do
     * isreturning an error. */
    // 如果设置了最大内存,那么检查内存是否超过限制,并做相应的操作
    if(server.maxmemory) {
        // 如果内存已超过限制,那么尝试通过删除过期键来释放内存
        intretval = freeMemoryIfNeeded();
        // 如果即将要执行的命令可能占用大量内存(REDIS_CMD_DENYOOM)
        // 并且前面的内存释放失败的话
        // 那么向客户端返回内存错误
        if((c->cmd->flags & REDIS_CMD_DENYOOM) && retval == REDIS_ERR){
           flagTransaction(c);
           addReply(c, shared.oomerr);
           return REDIS_OK;
        }
    }
 
    /*Don't accept write commands if there are problems persisting on disk
     * andif this is a master instance. */
    // 如果这是一个主服务器,并且这个服务器之前执行 BGSAVE 时发生了错误
    // 那么不执行写命令
    if(((server.stop_writes_on_bgsave_err &&
         server.saveparamslen > 0 &&
         server.lastbgsave_status == REDIS_ERR) ||
         server.aof_last_write_status == REDIS_ERR) &&
       server.masterhost == NULL &&
       (c->cmd->flags & REDIS_CMD_WRITE ||
        c->cmd->proc == pingCommand))
    {
       flagTransaction(c);
        if(server.aof_last_write_status == REDIS_OK)
           addReply(c, shared.bgsaveerr);
        else
           addReplySds(c,
               sdscatprintf(sdsempty(),
               "-MISCONF Errors writing to the AOF file: %s\r\n",
               strerror(server.aof_last_write_errno)));
       return REDIS_OK;
    }
 
    /*Don't accept write commands if there are not enough good slaves and
     * userconfigured the min-slaves-to-write option. */
    // 如果服务器没有足够多的状态良好服务器
    // 并且 min-slaves-to-write 选项已打开
    if(server.repl_min_slaves_to_write &&
       server.repl_min_slaves_max_lag &&
       c->cmd->flags & REDIS_CMD_WRITE &&
       server.repl_good_slaves_count < server.repl_min_slaves_to_write)
    {
       flagTransaction(c);
       addReply(c, shared.noreplicaserr);
       return REDIS_OK;
    }
 
    /*Don't accept write commands if this is a read only slave. But
     *accept write commands if this is our master. */
    // 如果这个服务器是一个只读 slave 的话,那么拒绝执行写命令
    if(server.masterhost && server.repl_slave_ro &&
       !(c->flags & REDIS_MASTER) &&
        c->cmd->flags &REDIS_CMD_WRITE)
    {
       addReply(c, shared.roslaveerr);
       return REDIS_OK;
    }
 
    /* Onlyallow SUBSCRIBE and UNSUBSCRIBE in the context of Pub/Sub */
    // 在订阅于发布模式的上下文中,只能执行订阅和退订相关的命令
    if((dictSize(c->pubsub_channels) > 0 || listLength(c->pubsub_patterns)> 0)
       &&
       c->cmd->proc != subscribeCommand &&
       c->cmd->proc != unsubscribeCommand &&
       c->cmd->proc != psubscribeCommand &&
       c->cmd->proc != punsubscribeCommand) {
       addReplyError(c,"only (P)SUBSCRIBE / (P)UNSUBSCRIBE / QUIT allowedin this context");
       return REDIS_OK;
    }
 
    /* Onlyallow INFO and SLAVEOF when slave-serve-stale-data is no and
     * weare a slave with a broken link with master. */
    if(server.masterhost && server.repl_state != REDIS_REPL_CONNECTED&&
       server.repl_serve_stale_data == 0 &&
       !(c->cmd->flags & REDIS_CMD_STALE))
    {
       flagTransaction(c);
       addReply(c, shared.masterdownerr);
       return REDIS_OK;
    }
 
    /*Loading DB? Return an error if the command has not the
     *REDIS_CMD_LOADING flag. */
    // 如果服务器正在载入数据到数据库,那么只执行带有 REDIS_CMD_LOADING
    // 标识的命令,否则将出错
    if(server.loading && !(c->cmd->flags & REDIS_CMD_LOADING)) {
        addReply(c,shared.loadingerr);
       return REDIS_OK;
    }
 
    /* Luascript too slow? Only allow a limited number of commands. */
    // Lua 脚本超时,只允许执行限定的操作,比如 SHUTDOWN 和SCRIPT KILL
    if(server.lua_timedout &&
         c->cmd->proc != authCommand &&
         c->cmd->proc != replconfCommand &&
       !(c->cmd->proc == shutdownCommand &&
         c->argc == 2 &&
         tolower(((char*)c->argv[1]->ptr)[0]) == 'n') &&
       !(c->cmd->proc == scriptCommand &&
         c->argc == 2 &&
          tolower(((char*)c->argv[1]->ptr)[0])== 'k'))
    {
       flagTransaction(c);
       addReply(c, shared.slowscripterr);
       return REDIS_OK;
    }
 
    /* Execthe command */
    if(c->flags & REDIS_MULTI &&
       c->cmd->proc != execCommand && c->cmd->proc !=discardCommand &&
       c->cmd->proc != multiCommand && c->cmd->proc !=watchCommand)
    {
        // 在事务上下文中
        // 除 EXEC 、DISCARD 、 MULTI 和 WATCH命令之外
        // 其他所有命令都会被入队到事务队列中
        queueMultiCommand(c);
       addReply(c,shared.queued);
    } else{
        // 执行命令
       call(c,REDIS_CALL_FULL);
 
       c->woff = server.master_repl_offset;
        // 处理那些解除了阻塞的键
        if(listLength(server.ready_keys))
           handleClientsBlockedOnLists();
    }
 
    returnREDIS_OK;
}

3)客户端收到命令回复后将回复打印给用户

 

2. 服务器中的serverCron函数

serverCron函数中的所有代码都会每秒调用 server.hz 次,默认是每隔100ms执行一次。serverCron函数主要执行以下逻辑:更新lru时间、管理客户端、触发BGSAVE或者AOF重写等:

/* This is our timer interrupt, called server.hztimes per second.
 *
 * 这是 Redis 的时间中断器,每秒调用 server.hz 次。
 *
 * Here iswhere we do a number of things that need to be done asynchronously.
 * Forinstance:
 *
 * 以下是需要异步执行的操作:
 *
 * - Activeexpired keys collection (it is also performed in a lazy way on
 *   lookup).
 *   主动清除过期键。
 *
 * -Software watchdog.
 *   更新软件 watchdog 的信息。
 *
 * - Updatesome statistic.
 *   更新统计信息。
 *
 * -Incremental rehashing of the DBs hash tables.
 *   对数据库进行渐增式 Rehash
 *
 * -Triggering BGSAVE / AOF rewrite, and handling of terminated children.
 *   触发 BGSAVE 或者 AOF 重写,并处理之后由 BGSAVE 和 AOF 重写引发的子进程停止。
 *
 * -Clients timeout of different kinds.
 *   处理客户端超时。
 *
 * -Replication reconnection.
 *   复制重连
 *
 * - Manymore...
 *   等等。。。
 *
 *Everything directly called here will be called server.hz times per second,
 * so inorder to throttle execution of things we want to do less frequently
 * a macrois used: run_with_period(milliseconds) { .... }
 *
 * 因为 serverCron 函数中的所有代码都会每秒调用 server.hz 次,
 * 为了对部分代码的调用次数进行限制,
 * 使用了一个宏 run_with_period(milliseconds) { ... } ,
 * 这个宏可以将被包含代码的执行次数降低为每 milliseconds 执行一次。
 */
 
int serverCron(struct aeEventLoop *eventLoop,long long id, void *clientData) {
    int j;
   REDIS_NOTUSED(eventLoop);
   REDIS_NOTUSED(id);
   REDIS_NOTUSED(clientData);
 
    /*Software watchdog: deliver the SIGALRM that will reach the signal
     *handler if we don't return here fast enough. */
    if(server.watchdog_period) watchdogScheduleSignal(server.watchdog_period);
 
    /*Update the time cache. */
   updateCachedTime();
 
    // 记录服务器执行命令的次数
   run_with_period(100) trackOperationsPerSecond();
 
    /* Wehave just REDIS_LRU_BITS bits per object for LRU information.
     * Sowe use an (eventually wrapping) LRU clock.
     *
     * Notethat even if the counter wraps it's not a big problem,
     *everything will still work but some object will appear younger
     * toRedis. However for this to happen a given object should never be
     *touched for all the time needed to the counter to wrap, which is
     * notlikely.
     *
     * 即使服务器的时间最终比 1.5 年长也无所谓,
     * 对象系统仍会正常运作,不过一些对象可能会比服务器本身的时钟更年轻。
     * 不过这要这个对象在 1.5 年内都没有被访问过,才会出现这种现象。
     *
     * Notethat you can change the resolution altering the
     *REDIS_LRU_CLOCK_RESOLUTION define.
     *
     * LRU 时间的精度可以通过修改 REDIS_LRU_CLOCK_RESOLUTION 常量来改变。
     */
   server.lruclock = getLRUClock();
 
    /*Record the max memory used since the server was started. */
    // 记录服务器的内存峰值
    if(zmalloc_used_memory() > server.stat_peak_memory)
       server.stat_peak_memory = zmalloc_used_memory();
 
    /*Sample the RSS here since this is a relatively slow call. */
   server.resident_set_size = zmalloc_get_rss();
 
    /* Wereceived a SIGTERM, shutting down here in a safe way, as it is
     * notok doing so inside the signal handler. */
    // 服务器进程收到 SIGTERM 信号,关闭服务器
    if(server.shutdown_asap) {
 
        // 尝试关闭服务器
        if(prepareForShutdown(0) == REDIS_OK) exit(0);
 
        // 如果关闭失败,那么打印 LOG ,并移除关闭标识
       redisLog(REDIS_WARNING,"SIGTERM received but errors trying to shutdown the server, check the logs for more information");
       server.shutdown_asap = 0;
    }
 
    /* Showsome info about non-empty databases */
    // 打印数据库的键值对信息
   run_with_period(5000) {
        for(j = 0; j < server.dbnum; j++) {
           long long size, used, vkeys;
 
           // 可用键值对的数量
           size = dictSlots(server.db[j].dict);
           // 已用键值对的数量
           used = dictSize(server.db[j].dict);
           // 带有过期时间的键值对数量
           vkeys = dictSize(server.db[j].expires);
 
           // 用 LOG 打印数量
           if (used || vkeys) {
               redisLog(REDIS_VERBOSE,"DB %d: %lld keys (%lld volatile) in %lldslots HT.",j,used,vkeys,size);
               /* dictPrintStats(server.dict); */
           }
        }
    }
 
    /* Showinformation about connected clients */
    // 如果服务器没有运行在 SENTINEL 模式下,那么打印客户端的连接信息
    if(!server.sentinel_mode) {
       run_with_period(5000) {
           redisLog(REDIS_VERBOSE,
               "%lu clients connected (%lu slaves), %zu bytes in use",
               listLength(server.clients)-listLength(server.slaves),
               listLength(server.slaves),
               zmalloc_used_memory());
        }
    }
 
    /* Weneed to do a few operations on clients asynchronously. */
    // 检查客户端,关闭超时客户端,并释放客户端多余的缓冲区
   clientsCron();
 
    /*Handle background operations on Redis databases. */
    // 对数据库执行各种操作
   databasesCron();
 
    /*Start a scheduled AOF rewrite if this was requested by the user while
     * aBGSAVE was in progress. */
    // 如果 BGSAVE 和 BGREWRITEAOF都没有在执行
    // 并且有一个 BGREWRITEAOF 在等待,那么执行 BGREWRITEAOF
    if(server.rdb_child_pid == -1 && server.aof_child_pid == -1 &&
       server.aof_rewrite_scheduled)
    {
       rewriteAppendOnlyFileBackground();
    }
 
    /*Check if a background saving or AOF rewrite in progress terminated. */
    // 检查 BGSAVE 或者 BGREWRITEAOF 是否已经执行完毕
    if(server.rdb_child_pid != -1 || server.aof_child_pid != -1) {
        intstatloc;
       pid_t pid;
 
        // 接收子进程发来的信号,非阻塞
        if((pid = wait3(&statloc,WNOHANG,NULL)) != 0) {
           int exitcode = WEXITSTATUS(statloc);
           int bysignal = 0;
           
           if (WIFSIGNALED(statloc)) bysignal = WTERMSIG(statloc);
 
           // BGSAVE 执行完毕
           if (pid == server.rdb_child_pid) {
               backgroundSaveDoneHandler(exitcode,bysignal);
 
           // BGREWRITEAOF 执行完毕
           } else if (pid == server.aof_child_pid) {
               backgroundRewriteDoneHandler(exitcode,bysignal);
 
           } else {
               redisLog(REDIS_WARNING,
                    "Warning, detectedchild with unmatched pid: %ld",
                    (long)pid);
           }
           updateDictResizePolicy();
        }
    } else{
 
        /*If there is not a background saving/rewrite in progress check if
         *we have to save/rewrite now */
        // 既然没有 BGSAVE 或者 BGREWRITEAOF 在执行,那么检查是否需要执行它们
 
        // 遍历所有保存条件,看是否需要执行 BGSAVE 命令
        for (j = 0; j < server.saveparamslen; j++) {
           struct saveparam *sp = server.saveparams+j;
 
           /* Save if we reached the given amount of changes,
            * the given amount of seconds, and if the latest bgsave was
            * successful or if, in case of an error, at least
            * REDIS_BGSAVE_RETRY_DELAY seconds already elapsed. */
           // 检查是否有某个保存条件已经满足了
           if (server.dirty >= sp->changes &&
               server.unixtime-server.lastsave > sp->seconds &&
               (server.unixtime-server.lastbgsave_try >
                REDIS_BGSAVE_RETRY_DELAY ||
                server.lastbgsave_status == REDIS_OK))
           {
               redisLog(REDIS_NOTICE,"%d changes in %d seconds. Saving...",
                    sp->changes,(int)sp->seconds);
               // 执行 BGSAVE
               rdbSaveBackground(server.rdb_filename);
               break;
           }
         }
 
         /*Trigger an AOF rewrite if needed */
        // 出发 BGREWRITEAOF
         if(server.rdb_child_pid == -1 &&
            server.aof_child_pid == -1 &&
            server.aof_rewrite_perc &&
            // AOF 文件的当前大小大于执行 BGREWRITEAOF 所需的最小大小
            server.aof_current_size > server.aof_rewrite_min_size)
         {
           // 上一次完成 AOF 写入之后,AOF 文件的大小
           long long base = server.aof_rewrite_base_size ?
                           server.aof_rewrite_base_size : 1;
 
           // AOF 文件当前的体积相对于 base 的体积的百分比
           long long growth = (server.aof_current_size*100/base) - 100;
 
           // 如果增长体积的百分比超过了 growth ,那么执行 BGREWRITEAOF
           if (growth >= server.aof_rewrite_perc) {
               redisLog(REDIS_NOTICE,"Starting automatic rewriting of AOF on%lld%% growth",growth);
               // 执行 BGREWRITEAOF
               rewriteAppendOnlyFileBackground();
           }
         }
    }
 
    // 根据 AOF 政策,
    // 考虑是否需要将 AOF 缓冲区中的内容写入到 AOF 文件中
    /* AOFpostponed flush: Try at every cron cycle if the slow fsync
     *completed. */
    if(server.aof_flush_postponed_start) flushAppendOnlyFile(0);
 
    /* AOFwrite errors: in this case we have a buffer to flush as well and
     *clear the AOF error in case of success to make the DB writable again,
     *however to try every second is enough in case of 'hz' is set to
     * anhigher frequency. */
   run_with_period(1000) {
        if(server.aof_last_write_status == REDIS_ERR)
           flushAppendOnlyFile(0);
    }
 
    /*Close clients that need to be closed asynchronous */
    // 关闭那些需要异步关闭的客户端
   freeClientsInAsyncFreeQueue();
 
    /*Clear the paused clients flag if needed. */
   clientsArePaused(); /* Don't check return value, just use the sideeffect. */
 
    /*Replication cron function -- used to reconnect to master and
     * todetect transfer failures. */
    // 复制函数
    // 重连接主服务器、向主服务器发送 ACK 、判断数据发送失败情况、断开本服务器超时的从服务器,等等
   run_with_period(1000) replicationCron();
 
    /* Runthe Redis Cluster cron. */
    // 如果服务器运行在集群模式下,那么执行集群操作
   run_with_period(100) {
        if(server.cluster_enabled) clusterCron();
    }
 
    /* Runthe Sentinel timer if we are in sentinel mode. */
    // 如果服务器运行在 sentinel 模式下,那么执行 SENTINEL 的主函数
   run_with_period(100) {
        if (server.sentinel_mode) sentinelTimer();
    }
 
    /*Cleanup expired MIGRATE cached sockets. */
    // 集群。。。TODO
   run_with_period(1000) {
       migrateCloseTimedoutSockets();
    }
 
    // 增加 loop 计数器
   server.cronloops++;
 
    return1000/server.hz;
}

3. 关于RDB持久化的保存条件

上篇博客中讲了Redis可以设置save条件来设置RDB的文件保存条件。在redisServer中维护了一个saveparams数组,此数组是一个saveparam结构

// 服务器的保存条件(BGSAVE 自动执行的条件)
struct saveparam {
 
    // 多少秒之内
    time_tseconds;
 
    // 发生多少次修改
    intchanges;
 
};

并且redisServer结构中还有一个dirty计数器及lastsave属性当服务器对数据库进行操作时,dirty计数器都会进行自增,lastsave属性是一个时间戳,记录了上一次服务器成功执行了SAVE或者BGSAVE命令的时间

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值