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-持久化】
建议搭配源码阅读:源码地址
前面的内容大部分都是在介绍redis中的基本数据结构,从这里开始主要介绍Redis服务端的请求接收,请求响应,接收到的命令解析,指向命令,返回命令响应等等。Redis是服务器是典型的事件驱动服务器,因此事件处理显得格外重要,而Redis将事件分为两大类:
1、
文件事件
(socket的读写)
2、时间事件
(周期性定时任务)
为了更好的理解服务器与客户端的交互,key只能是字符串
,value可以是字符串、列表、集合、有序集合和散列表
,这5种数据类型用通用结构体 robj
表示,我们称之为Redis对象。结构体robj
的type
字段表示对象类型,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在不同情况下可能采用不同的数据结构存储,结构体robj
的encoding
字段表示当前对象底层存储采用的数据结构,即对象的编码,总共定义了如下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;
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
字段。可以看出,为了创建一个字符串或者对象,必须分配两次内存空间,robj与sbs存储空间;两次内存分配效率低下,且数据分离存储降低了计算机的高数缓存的效率。因此提出OBJ_ENCODING_EMBSTR
编码的字符,当字符串内容较短的时候,只分配一次内存,robj与 sbs连续存储,以此提升内存分配效率与数据访问效率。OBJ_ENCODING_EMBSTR
编码字符串内存结构如下如:
⭐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
根据用户配置的缓存淘汰策略存储不同数据,常用的策略就是LRU 和 LFU 。如果当前缓存策略是 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可以在配置文件中配置
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;
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;
下面来介绍一下主要字段的含义:
⭐id
id为当前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
区间的数据都是需要返回给客户端的。可以看到reply
和buf
都用于缓存待返回给客户端的命令回复数据,至于为什么buf
和reply
需要同时存在呢?后面的文章会进行解答。
结构体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
为命令名称,value
为redisCommand对象,后面会进一步介绍。
⭐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地址创建的socket,ipfd_count
为创建的socket文件描述符数目。
⭐clients 和 current_client
client
是当前连接到Redis服务端的所有客户端对象,current_client
是当前正在执行命令的客户端对象。
⭐*delCommand, *multiCommand, *lpushCommand, *lpopCommand, *rpopCommand, *zpopminCommand, *zpopmaxCommand, *sremCommand, *execCommand, *expireCommand, *pexpireCommand, *xclaimCommand, *xgroupCommand
Redis中常用的指令集合,在redisServer创建的时候就初始化好,方便快速调用相应的实现方法。
⭐daemonize
当前Redis是否以守护线程的方式运行。
⭐aof_enabled
是否开启了AOF(后面AOF持久化会介绍)。
⭐rdb_child_pid
当前执行RDB存储的子线程PID(后面RDB持久化会介绍)。
⭐maxidletime
最大空闲时间,可通过参数timeout
配置,结合client对象的lastinteraction
字段,当客户端没有与服务器交互的时间超过maxidletime
的时候,会认为客户端超时并释放该客户端连接。
⭐clients_pending_read
和 clients_pending_write
等待读和写的client链表,和多线程有关,后续会介绍
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
当前的命令名称,如get
,set
,rpop
,lpop
。
⭐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
是命令名称,value
是redisCommand对象,函数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;
从上面的文章内容我们大概可以得出这样的一个关系图 (虽然不是很准确):
可以看出来Redis有很好的封装,将命令执行
,客户端
和实际存储的数据库
分离,体现了较好的解耦合的效果,其次aeEventLoop
也隔离的事件驱动的本身,无论是使用select/NIO/epoll还是时间事件 (下一篇文章介绍) ,能很灵活的切换。
此外:
通过redisCommand的隔离,让Redis后续的命令拓展变的更加方便,也是较好的解耦合,redisServer内部的client
很好的实现了内部缓存的效果,实现了客户端请求和响应的大吞吐量。