Redis源码阅读【8-命令处理生命周期-1】

Redis源码阅读【1-简单动态字符串】
Redis源码阅读【2-跳跃表】
Redis源码阅读【3-Redis编译与GDB调试】
Redis源码阅读【4-压缩列表】
Redis源码阅读【5-字典】
Redis源码阅读【6-整数集合】
Redis源码阅读【7-quicklist】
Redis源码阅读【8-命令处理生命周期-1】
Redis源码阅读【8-命令处理生命周期-2】
Redis源码阅读【8-命令处理生命周期-3】
Redis源码阅读【8-命令处理生命周期-4】
Redis源码阅读【番外篇-Redis的多线程】
Redis源码阅读【9-持久化】
建议搭配源码阅读:源码地址

文章目录

  • 1、介绍
  • 2、服务端对象redisObject
    • 2.1、redisObject 结构
  • 3、数据库对象redisDb
  • 4、客户端对象client
  • 5、服务端对象redisServer
  • 6、命令结构体redisCommand
  • 7、总结

1、介绍

前面的内容大部分都是在介绍redis中的基本数据结构,从这里开始主要介绍Redis服务端的请求接收,请求响应,接收到的命令解析,指向命令,返回命令响应等等。Redis是服务器是典型的事件驱动服务器,因此事件处理显得格外重要,而Redis将事件分为两大类:

1、文件事件(socket的读写)
2、时间事件(周期性定时任务)

2、服务端对象redisObject

为了更好的理解服务器与客户端的交互,key只能是字符串value可以是字符串、列表、集合、有序集合和散列表 ,这5种数据类型用通用结构体 robj 表示,我们称之为Redis对象。结构体robjtype字段表示对象类型,5种对象在server.h文件中定义:

/* 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. */

针对某一种类型的对象,Redis在不同情况下可能采用不同的数据结构存储,结构体robjencoding字段表示当前对象底层存储采用的数据结构,即对象的编码,总共定义了如下11种encoding常量:

encoding常量 数据结构 可存储对象类型
OBJ_ENCODING_RAW 简单动态字符(sds) 字符串
OBJ_ENCODING_INT 整数 字符串
OBJ_ENCODING_HT 字典 集合,散列表,有序集合
OBJ_ENCODING_ZIPMAP 未使用 未使用
OBJ_ENCODING_LINKEDLIST 废弃(使用quicklist替代) 废弃(使用quicklist替代)
OBJ_ENCODING_ZIPLIST 压缩列表 散列表、有序集合
OBJ_ENCODING_INTSET 整数集合 集合
OBJ_ENCODING_SKIPLIST 跳跃表 有序集合
OBJ_ENCODING_EMBSTR 简单动态字符串(sds) 字符串
OBJ_ENCODING_QUICKLIST 快速链表(quicklist) 列表
OBJ_ENCODING_STREAM stream stream

对象的整个生命周期中,编码不是一成不变的,比如集合对象。当集合中所有元素都可以用整数表示时,底层数据结构采用整数集合;当执行sadd命令向集合中添加元素时,Redis总会校验待添加元素是否可以解析为整数,如果解析失败,则会将集合存储结构转换为字典。代码如下:

 else if (subject->encoding == OBJ_ENCODING_INTSET) {
        if (isSdsRepresentableAsLongLong(value,&llval) == C_OK) {
            uint8_t success = 0;
            subject->ptr = intsetAdd(subject->ptr,llval,&success);
            if (success) {             
                if (intsetLen(subject->ptr) > server.set_max_intset_entries)
                	//编码转换
                    setTypeConvert(subject,OBJ_ENCODING_HT);
                return 1;
            }
        } else {
            //编码转换
            setTypeConvert(subject,OBJ_ENCODING_HT);          
            serverAssert(dictAdd(subject->ptr,sdsdup(value),NULL) == DICT_OK);
            return 1;
        }

对象在不同的情况下可能采用不同的数据结构存储,比如字典与跳跃表在不同的命令下各有优势 (例如:跳跃表擅长 zrange 与zrank ,字典的查询时间复杂度为O(1) )Redis会同时采用字典与跳跃表存储有序集合。例如有序集合定义如下:

typedef struct zset {
    dict *dict; //字典
    zskiplist *zsl; //跳跃表
} zset;

2.1、redisObject 结构

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). */
    //lru 缓存淘汰使用 (16位最后访问时间-时间戳 + 8位-调用次数)
    int refcount; //当前被引用的计数
    void *ptr; //指向数据结构的指针
} robj;

ptr:是void类型的指针,指向实际存储的某一种数据结构,但是当robj存储的数据可以使用long类型表示的时候,数据直接存储在ptr字段。可以看出,为了创建一个字符串或者对象,必须分配两次内存空间,robjsbs存储空间;两次内存分配效率低下,且数据分离存储降低了计算机的高数缓存的效率。因此提出OBJ_ENCODING_EMBSTR编码的字符,当字符串内容较短的时候,只分配一次内存,robjsbs连续存储,以此提升内存分配效率与数据访问效率。OBJ_ENCODING_EMBSTR编码字符串内存结构如下如:
Redis源码阅读【8-命令处理生命周期-1】_第1张图片

refcount 存储当前对象的引用次数,用于实现对象共享。当共享对象的时候refcount 加 1;删除对象时候refcount 减 1,当refconut 为0 的时候,释放其对象内存空间。删除对象的代码如下:

//减少refcount
void decrRefCount(robj *o) {
    //判断是否释放共享对象
    if (o->refcount == 1) {
        switch(o->type) {
        case OBJ_STRING: freeStringObject(o); break; //释放字符
        case OBJ_LIST: freeListObject(o); break; //释放链表
        case OBJ_SET: freeSetObject(o); break;//释放集合
        case OBJ_ZSET: freeZsetObject(o); break; //释放有序集合
        case OBJ_HASH: freeHashObject(o); break; //释放hash表
       case OBJ_MODULE: freeModuleObject(o); break;
        case OBJ_STREAM: freeStreamObject(o); break;
        default: serverPanic("Unknown object type"); break;
        }
        zfree(o); //回收空间
    } else {
        if (o->refcount <= 0) serverPanic("decrRefCount against refcount <= 0");
        if (o->refcount != OBJ_SHARED_REFCOUNT) o->refcount--; //计数器自减
    }
}

lru字段占24bit,用于实现缓存淘汰策略,可以在配置文件中使用maxmemory-policy配置已用内存达到最大内存限制时的缓存淘汰策略。lru根据用户配置的缓存淘汰策略存储不同数据,常用的策略就是LRULFU 。如果当前缓存策略是 LRU 那么lru上面存储的就是上次对象被访问的时间,如果策略是 LFU,那么lru上面存储的是上次访问时间访问次数。使用如get命令访问数据时,会执行如下代码来更新对象的lru,代码如下:

robj *lookupKey(redisDb *db, robj *key, int flags) {
    dictEntry *de = dictFind(db->dict,key->ptr);
    if (de) {
        robj *val = dictGetVal(de);
        //hasActiveChildProcess 保证当前没有子进程在访问中
        if (!hasActiveChildProcess() && !(flags & LOOKUP_NOTOUCH)){
            //判断当前策略是LRU还是LFU
            if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
                updateLFU(val); //更新时间和访问次数
            } else {
                val->lru = LRU_CLOCK(); //更新时间
            }
        }
        return val;
    } else {
        return NULL;
    }
}

LRU_CLOCK函数用来获取当前时间,这里的时间并不是实时获取的,Redis会在一秒的周期内去获取系统的精确时间,缓存在全局变量server.lruclock中,LRU_CLOCK获取的只是该缓存时间。
updateLFU函数用于更新对象的上次访问时间和访问次数,函数实现如下:

void updateLFU(robj *val) {
    unsigned long counter = LFUDecrAndReturn(val); //具有随时间衰减效果的计数器
    counter = LFULogIncr(counter); //访问次数自增
    val->lru = (LFUGetTimeInMinutes()<<8) | counter; //将访问次数放入到lru的前8位 
}

注意: lru一共占用24个字节,可以从上面看出低8位存储的是对象的访问次数,高16位存储的是对象的上次访问时间,以分钟为单位;此外LFUDecrAndReturn函数的返回值是counter访问次数,对象的访问次数在counter上面累加。为了保证存活较长时间的对象长,其访问次数可能会很大,但是访问频率很小,导致无法被淘汰的情况。LFU虽然是基于访问次数的,但是对于访问频率较小的对象也要被淘汰。LFUDecrAndReturn就是实现了访问次数随着时间的推移递减的效果,代码如下:

unsigned long LFUDecrAndReturn(robj *o) {
    unsigned long ldt = o->lru >> 8;//获取最近一次访问时间
    unsigned long counter = o->lru & 255;//获取访问次数
    //num_periods 是当前理应衰减的数量
    unsigned long num_periods = server.lfu_decay_time ? LFUTimeElapsed(ldt) / server.lfu_decay_time : 0;
    //判断释放衰减到0
    if (num_periods)
        counter = (num_periods > counter) ? 0 : counter - num_periods;
    return counter;
}

衰减的具体公式= (当前时间-最近一次访问时间)/ 指定的衰减周期,其中衰减周期server.lfu_decay_time可以在配置文件中配置

3、数据库对象redisDb

Redis为了方便使用,将众多redisObject封装在redisDb里面并提供给外界使用,总所周知Redis实例中不止一个数据库,默认是有16个库,那么这16个库是怎么来的呢?下面我们将介绍一下,首先我们来看看redisDb的定义是怎么样的,代码如下:

typedef struct redisDb {
    dict *dict;               //当前数据库所有的键值对字典  
    dict *expires;            //每个键值对对于的淘汰策略字典
    dict *blocking_keys;      //用于阻塞client场景的key操作 
    dict *ready_keys;         //当前正在被阻塞的key 
    dict *watched_keys;       //用于事务的,监控key 
    int id;                   //当前数据库的ID  
    long long avg_ttl;        //平均ttl
} redisDb;

下面来解释一下各个字段的含义

id为数据库序号,默认情况下Redis有16个数据库,id序号为0~15,可以通过配置文件属性修改数据库数量,通过redisDb[]数组的方式可以快速切换到不同的数据库。
dict存储当前数据库所有的键值对。
expires存储键的过期时间。
avg_ttl存储数据库对象的平均TTL,用于统计。
blocking_keys 和 ready_keys当使用BLPOP命令阻塞的获取列表原始的时候,如果链表为空,会阻塞当前客户端,同时将此列表的键记录在blocking_keys;当使用命令PUSH向列表添加元素的时候,会从字典blocking_keys中查找该key,如果找到说明当前有客户端正在被该key阻塞中,于是将此列表的键记录到ready_keys里面,以便后续响应。
watched_keys Redis支持事务, 使用multi开启事务,命令exec执行事务,但是当开启事务到执行事务期间数据有可能会被修改,Redis采用了类似于乐观锁的方式防止这种情况发生。当开启事务的时候同时会使用watch key命令监控关心的数据键,而watched_keys就是当前被watch命令监控的所有数据键的字典,其中key-value分别是数据键与客户端对象。当Redis服务器接收到写命令时,会从字典watched_keys中查找该key,如果找到说明当前有客户端正在监控该数据键,于是将标记客户端对象为dirty;待Redis服务器接收到客户端exec命令时,如果客户端带有dirty标记,则会拒绝事务执行。

当然redisDb除了以上的几个属性外,还有其它属性,这里就不展开了欢迎大家直接下载源码阅读,大致的功能,完整的定义如下:

typedef struct redisDb {
    dict *dict;               //当前数据库所有的键值对字典  
    dict *expires;            //每个键值对对于的淘汰策略字典 
    dict *blocking_keys;      //用于阻塞client场景的key操作 
    dict *ready_keys;         //当前正在被阻塞的key  
    dict *watched_keys;       //用于事务的,监控key 
    int id;                   //当前数据库的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;

4、客户端对象client

Redis是典型的客户端----服务端结构,客户通过socket与服务端建立网络连接并发送命令请求,服务端处理命令请求并回复。Redis使用结构体client存储客户端连接的所有信息,包括但不限于客户端名称客户端连接的套接字描述符客户端当前选择的数据库ID客户端输入缓冲区客户端输出缓冲区等等。主要结构体定义如下:

typedef struct client {
    uint64_t id;        //该客户端ID   
    connection *conn;  //客户端连接
    int resp;         //客户端使用的RESP协议版本(为了兼容旧版)    
    redisDb *db;      //当前选择的DB      
    robj *name;      //客户端名称    
    time_t lastinteraction; //客户端上次与服务端交互时间,用于实现超时处理
    sds querybuf;    //用于积累查询的缓冲区       
    size_t qb_pos;   //当前在查询缓冲区读取到的位置    
    int argc;        //输入缓冲区的命令请求是按照Redis协议格式(RESP协议)编码的字符串,需要解析出所有的参数,其中参数的个数就存储在argc中
    robj **argv;            //当前命令的参数
    sds pending_querybuf;  //由于主从复制的缓冲区
    struct redisCommand *cmd, *lastcmd; //最后一次执行的命令集合 
    list *reply;  //需要响应对象的链表       
    unsigned long long reply_bytes; //需要响应对象的总大小
    size_t sentlen;       //表示已经返回给客户端的字节数
 
    /* Response buffer */
    int bufpos;
    char buf[PROTO_REPLY_CHUNK_BYTES];
} client;

下面来介绍一下主要字段的含义:
idid为当前client的全局唯一ID,通过全局变量server.next_client_id实现。
conn为当前客户端socket连接
resp当前客户端使用的resp协议版本,Redis是向下兼容,需要兼容低版本的客户端。
db为客户端当前select的数据库对象redisDb
name客户端名称,可以使用命令CLIENT SETNAME 进行设置。
lastinteraction客户端上次与服务端交互时间,用于超时处理。
querybuf输入缓冲区,recv函数接收的客户端命令请求会暂时存放在这里。
qb_pos当前缓冲区读取到的位置指针。
argc输入缓冲区的命令请求是按照Redis协议格式 (resp协议) 编码的字符串,需要解析出所有的参数,其中参数的个数就存储在argc中。
argv当前正在执行的命令的参数,已经被解析成redisObject
pending_querybuf用于主从复制的缓冲区。
cmd待执行的客户端命令;解析命令请求后,会根据命令名称查找该命令对应的命令对象,存储在cmd字段中,对应的命令类型可以参考命令对象redisCommand,后面会进一步讲解。
reply输出链表,存储待返回给客户端的命令回复数据。
reply_bytes输出链表中所有节点的存储空间总大小。
sentlen表示已经返回给客户端的字节数。
buf 和 bufpos输出的缓冲区,存储待返回给客户端的命令回复数据,bufpos表示输出缓冲区中数据的最大字节位置,显然sentlen~bufpos区间的数据都是需要返回给客户端的。可以看到replybuf都用于缓存待返回给客户端的命令回复数据,至于为什么bufreply需要同时存在呢?后面的文章会进行解答。

5、服务端对象redisServer

结构体redisServer存储的是Redis服务器的所有信息,包括单不限于:数据库配置参数命令表监听端口与地址客户端列表若干统计信息RDB与AOF持久化相关信息主从复制相关信息集群相关信息等等。由于redisServer涉及的属性非常多,这里我们也介绍一下经常接触到的属性内容,其余的也欢迎读者自行研究就不做过多的概述,定义代码如下server.h

struct redisServer {
    /* General */
    pid_t pid;                  //当前Redis进程PID
    char *configfile;           //当前Redis配置文件
 ............................【中间省略】..............................
    redisDb *db;                //当前的Redis所有的Db指针
    int dbnum;                     //当前数据库总数
    dict *commands;            	//当前Redis对应的命令字典
    aeEventLoop *el;            //10k的处理方式
   ..........................【中间省略】...............................
    /* Networking */
    int port;                  //当前监听的端口
    char *bindaddr[CONFIG_BINDADDR_MAX]; //绑定的所有IP地址
    int bindaddr_count;         //绑定所有IP地址的数量
    int ipfd[CONFIG_BINDADDR_MAX]; //针对bindaddr字段的所有IP地址创建的socket文件描述符
    int ipfd_count;             //创建socket文件描述符数目
    list *clients;              //当前连接到Redis服务器所有客户端
    client *current_client;    	//当前执行命令的客户端
    list *clients_pending_write; //等待写的client
    list *clients_pending_read;  //等待读的client
    .........................【中间省略】....................................
    //常用指令指针缓存
    struct redisCommand *delCommand, *multiCommand, *lpushCommand,
                        *lpopCommand, *rpopCommand, *zpopminCommand,
                        *zpopmaxCommand, *sremCommand, *execCommand,
                        *expireCommand, *pexpireCommand, *xclaimCommand,
                        *xgroupCommand;   
    .........................【中间省略】.....................................           
    int daemonize;                 //是否守护线程运行
    int aof_enabled;               //是否开启aof
    pid_t rdb_child_pid;           //RDB保存的子进程PID
    .........................【中间省略】....................................
    int maxidletime;               //最大空闲时间
};

字段定义如下:
pid当前Redis进程PID
configfile配置文件绝对路径。
db数据库数组,数组的每个元素都是redisDb类型。
dbnum数据库数目,可以通过databases配置,默认16。
commands命令字典,Redis支持的所有命令都存储在这个字典中,key为命令名称,valueredisCommand对象,后面会进一步介绍。
el Redis是典型的事件驱动模型,el代表Redis的事件循环,类型为aeEventLoop (默认使用epoll模式),后面会介绍。
port服务端监听端口号,可以通过参数port进行配置,默认端口是6379。
bindaddr 和 bindaddr_count绑定的所有IP地址,可以通过参数bind配置多个,例如:bind 192.168.1.11 192.168.1.12,bindaddr_count为用户配置的IP地址数目;CONFIG_BINDADDR_MAX常量为16,即最多绑定16个IP地址;Redis默认会绑定到当前机器所有可用的IP地址。
ipfd 和 ipfd_count针对bindaddr字段的所有IP地址创建的socketipfd_count为创建的socket文件描述符数目。
clients 和 current_client client是当前连接到Redis服务端的所有客户端对象,current_client是当前正在执行命令的客户端对象。
*delCommand, *multiCommand, *lpushCommand, *lpopCommand, *rpopCommand, *zpopminCommand, *zpopmaxCommand, *sremCommand, *execCommand, *expireCommand, *pexpireCommand, *xclaimCommand, *xgroupCommandRedis中常用的指令集合,在redisServer创建的时候就初始化好,方便快速调用相应的实现方法。
daemonize当前Redis是否以守护线程的方式运行。
aof_enabled是否开启了AOF(后面AOF持久化会介绍)。
rdb_child_pid当前执行RDB存储的子线程PID(后面RDB持久化会介绍)。
maxidletime最大空闲时间,可通过参数timeout配置,结合client对象的lastinteraction字段,当客户端没有与服务器交互的时间超过maxidletime的时候,会认为客户端超时并释放该客户端连接。
clients_pending_readclients_pending_write 等待读和写的client链表,和多线程有关,后续会介绍

6、命令结构体redisCommand

Redis支持的所有命令初始都存储在字典commands中,对应的类型为redisCommand,定义初始化代码如下:

struct redisCommand redisCommandTable[] = {
    {"get",getCommand,2,"read-only fast @string",0,NULL,1,1,1,0,0,0},
    {"set",setCommand,-3,"write use-memory @string",0,NULL,1,1,1,0,0,0},
    {"setnx",setnxCommand,3,"write use-memory fast @string",0,NULL,1,1,1,0,0,0},
    {"setex",setexCommand,4,"write use-memory @string",0,NULL,1,1,1,0,0,0},
    {"psetex",psetexCommand,4,"write use-memory @string",0,NULL,1,1,1,0,0,0},
    {"strlen",strlenCommand,2,"read-only fast @string",0,NULL,1,1,1,0,0,0},
    {"del",delCommand,-2,"write @keyspace",0,NULL,1,-1,1,0,0,0},
    {"rpush",rpushCommand,-3,"write use-memory fast @list",0,NULL,1,1,1,0,0,0},
    {"lpush",lpushCommand,-3,"write use-memory fast @list",0,NULL,1,1,1,0,0,0},
    {"rpushx",rpushxCommand,-3,"write use-memory fast @list",0,NULL,1,1,1,0,0,0},
    {"lpushx",lpushxCommand,-3,"write use-memory fast @list",0,NULL,1,1,1,0,0,0},
    {"linsert",linsertCommand,5,"write use-memory @list",0,NULL,1,1,1,0,0,0},
    {"rpop",rpopCommand,2,"write fast @list",0,NULL,1,1,1,0,0,0},
    {"lpop",lpopCommand,2,"write fast @list",0,NULL,1,1,1,0,0,0},
.......................................后面还有很多命令就不列出来了

redisCommand结构相对比较简单,主要定义了命令的名称命令处理函数命令标志等等。其结构体定义如下server.h

struct redisCommand {
    char *name; //命令名称
    redisCommandProc *proc; //命令对应的处理函数指针
    int arity; //命令参数数量
    char *sflags;   // 命令标志
    uint64_t flags; //命令二进制标志
    redisGetKeysProc *getkeys_proc; //获取键的处理函数
    long long microseconds, calls; //当前命令的总执行次数和总执行时间
    int id;   //命令对应的ID ,运行的时候动态分配的,用于ACL鉴权指定命令使用权限  
};

各个字段定义如下;
name当前的命令名称,如getsetrpoplpop
proc命令处理函数的指针。
arity命令参数数目,用于校验命令的请求格式是否正确,当arity小于0时,表示命令参数数目大于等于arity;当arity大于0时,表示命令参数数目必须为arity;注意命令请求中,命令的名称本身也是一个参数,如get命令参数数目为2,命令请求格式为get key
sflags命令标志,例如标识命令是读命令还是写命令。
flags命令的二进制标志,服务器启动时解析sflags字段生成。
calls表示从服务器运行开始到现在,命令被调用次数。
microseconds表示从服务器运行开始到现在,命令总执行时间,通过microseconds/calls可以计算出该命令的平均处理时间,用于统计。

下面是字符标志和二进制标志对应的含义和命令表:

字符标识 二进制标识 含义 相关命令
w CMD_WEITE 写命令 set,del,incr,lpush
r CMD_READONLY 读命令 get,exists,llen
m CMD_DENYOOM 内存不足时,拒绝执行此类命令 set,append,lpush
a CMD_ADMIN 管理命令 save,shutdown,slaveof
p CMD_PUBSUB 发布订阅相关命令 subscribe,unsubscribe
s CMD_NOSCRIPT 命令不可在Lua脚本使用 auth,save,brpop
R CMD_RANDOM 随机命令,即使命令请求参数完全相同,返回结果也可能不同 srandmember,scan,time
S CMD_SORT_FOR_SCRIPT 当在Lua脚本使用此类命令时,需要对输出结果做排序 sinter,save,brpop
l CMD_LOADING 服务器启动载入过程中,只能执行此类命令 sinter,sunion,sdiff
t CMD_STALE 当服务器与主服务器断开连接,且从服务器配置slave-serve-stale-data no的时候,从服务器只能执行此类命令 auth,shutdown,info
M CMD_SKIP_MONITOR 此类命令不会传播给监视器 exec
k CMD_ASKING 集群槽(slot)迁移时使用 restore-asking
F CMD_FAST 命令执行时间超过限制,会记录延迟事件,此标记用于区分延迟事件类型,F表示fast-command get,setnx,strlen,exists

为了提升命令执行的效率,redisCommandTable[]转换为redisServer里面的commands 字典,其中key是命令名称,valueredisCommand对象,函数populateCommandTable函数实现这个数组到字典的转换,代码如下:

void populateCommandTable(void) {
    int j;
    //统计需要放入的命令总数
    int numcommands = sizeof(redisCommandTable)/sizeof(struct redisCommand);
    for (j = 0; j < numcommands; j++) {
        //通过偏移获取对应的redisCommand对象位置
        struct redisCommand *c = redisCommandTable+j;
        int retval1, retval2;       
        //解析 sflags 生成 flags
        if (populateCommandTableParseFlags(c,c->sflags) == C_ERR)
            serverPanic("Unsupported command flag");
        //初始化命令ID,用于ACL
        c->id = ACLGetCommandID(c->name); /* Assign the ID used for ACL. */
        //添加到字典中
        retval1 = dictAdd(server.commands, sdsnew(c->name), c);      
        serverAssert(retval1 == DICT_OK && retval2 == DICT_OK);
    }
}
//解析 sflags 生成 flags
int populateCommandTableParseFlags(struct redisCommand *c, char *strflags) {
    int argc;
    sds *argv;
    argv = sdssplitargs(strflags,&argc);
    if (argv == NULL) return C_ERR;
    for (int j = 0; j < argc; j++) {
        char *flag = argv[j];
        if (!strcasecmp(flag,"write")) {
            c->flags |= CMD_WRITE|CMD_CATEGORY_WRITE;
        } else if (!strcasecmp(flag,"read-only")) {
            c->flags |= CMD_READONLY|CMD_CATEGORY_READ;
        } else if (!strcasecmp(flag,"use-memory")) {
            c->flags |= CMD_DENYOOM;
        } else if (!strcasecmp(flag,"admin")) {
            c->flags |= CMD_ADMIN|CMD_CATEGORY_ADMIN|CMD_CATEGORY_DANGEROUS;
        } else if (!strcasecmp(flag,"pub-sub")) {
            c->flags |= CMD_PUBSUB|CMD_CATEGORY_PUBSUB;
        } else if (!strcasecmp(flag,"no-script")) {
            c->flags |= CMD_NOSCRIPT;
        } else if (!strcasecmp(flag,"random")) {
            c->flags |= CMD_RANDOM;
        } else if (!strcasecmp(flag,"to-sort")) {
            c->flags |= CMD_SORT_FOR_SCRIPT;
        } else if (!strcasecmp(flag,"ok-loading")) {
            c->flags |= CMD_LOADING;
        } else if (!strcasecmp(flag,"ok-stale")) {
            c->flags |= CMD_STALE;
        } else if (!strcasecmp(flag,"no-monitor")) {
            c->flags |= CMD_SKIP_MONITOR;
        } else if (!strcasecmp(flag,"no-slowlog")) {
            c->flags |= CMD_SKIP_SLOWLOG;
        } else if (!strcasecmp(flag,"cluster-asking")) {
            c->flags |= CMD_ASKING;
        } else if (!strcasecmp(flag,"fast")) {
            c->flags |= CMD_FAST | CMD_CATEGORY_FAST;
        } else {
            /* Parse ACL categories here if the flag name starts with @. */
            uint64_t catflag;
            if (flag[0] == '@' &&
                (catflag = ACLGetCommandCategoryFlagByName(flag+1)) != 0)
            {
                c->flags |= catflag;
            } else {
                sdsfreesplitres(argv,argc);
                return C_ERR;
            }
        }
    }
    /* If it's not @fast is @slow in this binary world. */
    if (!(c->flags & CMD_CATEGORY_FAST)) c->flags |= CMD_CATEGORY_SLOW;

    sdsfreesplitres(argv,argc);
    return C_OK;
}

此外对于经常使用的命令,redisServer也直接缓存下来:

    //常用指令指针缓存
    struct redisCommand *delCommand, *multiCommand, *lpushCommand,
                        *lpopCommand, *rpopCommand, *zpopminCommand,
                        *zpopmaxCommand, *sremCommand, *execCommand,
                        *expireCommand, *pexpireCommand, *xclaimCommand,
                        *xgroupCommand;   

7、总结

从上面的文章内容我们大概可以得出这样的一个关系图 (虽然不是很准确)
Redis源码阅读【8-命令处理生命周期-1】_第2张图片
可以看出来Redis有很好的封装,将命令执行客户端实际存储的数据库分离,体现了较好的解耦合的效果,其次aeEventLoop也隔离的事件驱动的本身,无论是使用select/NIO/epoll还是时间事件 (下一篇文章介绍) ,能很灵活的切换。

此外:
Redis源码阅读【8-命令处理生命周期-1】_第3张图片
通过redisCommand的隔离,让Redis后续的命令拓展变的更加方便,也是较好的解耦合,redisServer内部的client很好的实现了内部缓存的效果,实现了客户端请求和响应的大吞吐量。

你可能感兴趣的:(redis,源码阅读)