Redis源码、面试指南(4)单机数据库、持久化、通知与订阅(中):https://developer.aliyun.com/article/1508243
事件类型
ae.h中定义了IO多路复用程序所监听的两类事件,AE_READABLE和AE_WEUTABLE:
·READABLE事件,当套接字可读(客户端write或close或新的可应答套接字)时产生;
·WRITABLE事件,套接字可写时,如客户端执行read;
当一个套接字同时产生了两类事件,即可读又可写,那么优先执行度再写;(为什么呢?可能是为了防止读操作被覆盖)
时间事件
Redis中的时间事件主要分为两类,一是定时事件(返回AE_NOMORE),二是周期性事件(返回非AE_NOMORE整数值);
时间事件由三个属性构成:
·id:全局唯一ID,递增;
·when:unix时间戳,记录预设时间;
·timeProc:时间事件处理器,类似于回调函数,当时间事件到达时,就会调用相应的处理器进行处理;
Redis的时间事件是基于一个无序(指when无序,而不是id无序)链表实现的,新来的放在表头,当时间事件执行器运行时,遍历整个链表,找出已到达的时间事件,并调用相应的事件处理器(这跟poll、select一样,效率很低?在3.0版本下,正常模式的Redis服务器仅有serverCron时间事件,benchmark模式下也只有两个,几乎不影响性能)。
事件的调度与执行
Redis对时间和文件事件的调度策略如下图所示:
客户端
Redis服务器可以与多个客户端建立连接,每个客户端连接时,服务器都保存了相应状态,如下:
其中每个客户端都具有自己的属性。
基本属性
typedef struct redisClient { // 套接字描述符 int fd; // 当前正在使用的数据库 redisDb *db; // 当前正在使用的数据库的 id (号码) int dictid; // 客户端的名字 robj *name; // 查询缓冲区 sds querybuf; // 查询缓冲区长度峰值 size_t querybuf_peak; // 参数数量 int argc; // 参数对象数组 robj **argv; // 记录被客户端执行的命令 struct redisCommand *cmd, *lastcmd; // 请求的类型:内联命令还是多条命令 int reqtype; // 剩余未读取的命令内容数量 int multibulklen; // 命令内容的长度 long bulklen; // 回复链表 list *reply; // 回复链表中对象的总大小 unsigned long reply_bytes; // 已发送字节,处理 short write 用 int sentlen; // 创建客户端的时间 time_t ctime; /* Client creation time */ // 客户端最后一次和服务器互动的时间 time_t lastinteraction; // 客户端的输出缓冲区超过软性限制的时间 time_t obuf_soft_limit_reached_time; // 客户端状态标志 int flags; /* REDIS_SLAVE | REDIS_MONITOR | REDIS_MULTI ... */ // 当 server.requirepass 不为 NULL 时 // 代表认证的状态 // 0 代表未认证, 1 代表已认证 int authenticated; // 复制状态 int replstate; // 用于保存主服务器传来的 RDB 文件的文件描述符 int repldbfd; /* replication DB file descriptor */ // 读取主服务器传来的 RDB 文件的偏移量 off_t repldboff; /* replication DB file offset */ // 主服务器传来的 RDB 文件的大小 off_t repldbsize; /* replication DB file size */ sds replpreamble; /* replication DB preamble. */ // 主服务器的复制偏移量 long long reploff; /* replication offset if this is our master */ // 从服务器最后一次发送 REPLCONF ACK 时的偏移量 long long repl_ack_off; /* replication ack offset, if this is a slave */ // 从服务器最后一次发送 REPLCONF ACK 的时间 long long repl_ack_time;/* replication ack time, if this is a slave */ // 主服务器的 master run ID // 保存在客户端,用于执行部分重同步 char replrunid[REDIS_RUN_ID_SIZE+1]; /* master run id if this is a master */ // 从服务器的监听端口号 int slave_listening_port; /* As configured with: SLAVECONF listening-port */ // 事务状态 multiState mstate; /* MULTI/EXEC state */ // 阻塞类型 int btype; /* Type of blocking op if REDIS_BLOCKED. */ // 阻塞状态 blockingState bpop; /* blocking state */ // 最后被写入的全局复制偏移量 long long woff; /* Last write global replication offset. */ // 被监视的键 list *watched_keys; /* Keys WATCHED for MULTI/EXEC CAS */ // 这个字典记录了客户端所有订阅的频道 // 键为频道名字,值为 NULL // 也即是,一个频道的集合 dict *pubsub_channels; /* channels a client is interested in (SUBSCRIBE) */ // 链表,包含多个 pubsubPattern 结构 // 记录了所有订阅频道的客户端的信息 // 新 pubsubPattern 结构总是被添加到表尾 list *pubsub_patterns; /* patterns a client is interested in (SUBSCRIBE) */ sds peerid; /* Cached peer ID. */ // 回复偏移量 int bufpos; // 回复缓冲区 char buf[REDIS_REPLY_CHUNK_BYTES]; } redisClient;
主要围绕**其中几个重要属性(从上至下)**进行分析:
·套接字描述符fd:取值为-1(伪客户端,处理的命令请求来自AOF文件或者LUA脚本)或大于-1(普通客户端);
client list 可以列出当前服务器的客户端及其所使用的描述符等;
·name:默认为空,可以通过client setname命令设置;
·flags标志:记录客户端的角色及其状态,即可以是单个标志,也可以是多个标志的或:
每一个标志都是一个常量,查看redis.h:
* 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 client is a slave monitor, see MONITOR */ #define REDIS_MULTI (1<<3) /* This client is in a MULTI context */ #define REDIS_BLOCKED (1<<4) /* The client is waiting in a blocking operation */ #define REDIS_DIRTY_CAS (1<<5) /* Watched keys modified. EXEC will fail. */ #define REDIS_CLOSE_AFTER_REPLY (1<<6) /* Close after writing entire reply. */ #define REDIS_UNBLOCKED (1<<7) /* This client was unblocked and is stored in server.unblocked_clients */ #define REDIS_LUA_CLIENT (1<<8) /* This is a non connected client used by Lua */ #define REDIS_ASKING (1<<9) /* Client issued the ASKING command */ #define REDIS_CLOSE_ASAP (1<<10)/* Close this client ASAP */ #define REDIS_UNIX_SOCKET (1<<11) /* Client connected 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 if is 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. */
·argv、argc命令与命令参数:即是客户端发送给服务器的命令,实例如下:
那么服务器在接收命令之后,如何查找命令的实现函数呢?
·cmd命令实现函数:这个步骤是通过一个Dict结构的命令表实现的:
当在命令表中找到argv[0]所对应的command结构时,cmd就会指向该结构。而后服务器就可以使用cmd以及argv、argc保存的属性来执行相应的函数。
·输出缓冲区,有两个缓冲区,一个大小固定,一个大小可变
大小固定保存长度较小的回复,如ok、简单字符串值、整数值等等,主要由以下两个参数组成:
可变大小的缓冲区保存如非常大(如果固定大小不够用了,或者放不下,就会使用它)的字符串、列表、集合等,其结构是一个链表List* reply:
·身份验证authenticated:表示客户端是否通过了身份验证,0表示未验证,1表示验证。
当为0时,客户端的请求命令除了AUTH之外都会被拒绝。
创建与关闭
服务器启动会创建负责执行Lua脚本的伪客户端(服务器生命周期内一直存在)、执行AOF中redis命令的伪客户端(载入完毕之后关闭)。
除此之外,当使用网络连接到服务器时,会创建普通客户端,普通客户端可能由于发送不合协议格式的命令而被关闭。
服务器
命令请求执行过程
若客户端执行以下命令:set key value,那么从输入该命令到客户端回复ok,需要进行以下操作:
接下来分别介绍这四个部分:
·发送命令请求
首先客户端会将命令请求转换成协议格式,然后通过套接字将该请求数据发送给服务器:
·读取命令请求
当服务器获取到某客户端的套接字可读时,它就会调用命令请求处理其器来执行以下操作:
·命令执行器
分为四个部分:查找命令实现(从命令表中查找所指定的命令结构)、执行预备操作(检查命令是否找到、参数是否合法、客户端是否已验证、是否正在执行事务等等,简而言之,就是看现在是不是执行该命令的时机)、调用命令的实现函数、执行后续操作(日志、更新calls属性、广播给其他服务器等等)
命令表Dict中存储的是Redis命令的结构,如下:
/* * Redis 命令 */ struct redisCommand { // 命令名字 char *name; // 实现函数 redisCommandProc *proc; // 参数个数 int arity; // 字符串表示的 FLAG char *sflags; /* Flags as string representation, one char per flag. */ // 实际 FLAG int flags; /* The actual flags, obtained from the 'sflags' field. */ /* Use a function to determine keys arguments in a command line. * Used for Redis Cluster redirect. */ // 从命令中判断命令的键参数。在 Redis 集群转向时使用。 redisGetKeysProc *getkeys_proc; /* What keys should be loaded in background when calling this command? */ // 指定哪些参数是 key int firstkey; /* The first argument that's a key (0 = no keys) */ int lastkey; /* The last argument that's a key */ int keystep; /* The step between first and last key */ // 统计信息 // microseconds 记录了命令执行耗费的总毫微秒数 // calls 是命令被执行的总次数 long long microseconds, calls; };
注:redis命令不区分大小写;
·命令回复发送给客户端:命令实现函数会将命令回复保存到客户端的输出缓冲区里面,并为客户端的套接字关联命令回复处理器,当客户端套接字变为可写状态时,服务器就会执行命令回复处理器,将保存在客户端输出缓冲区中的命令回复发送给客户端。
当命令回复发送完毕之后,回复处理器会清空客户端状态的输出缓冲区为处理下一个命令做准备。
·客户端接收并打印:如下
serverCron函数
源码地址redis.c/serverCron,1300+行左右位置。
该函数**默认100毫秒(可配置)**执行一次,负责管理服务器资源,并保持服务器自身的良好运转。
建议结合源码及下图理解其功能:
实际 FLAG
int flags; /* The actual flags, obtained from the ‘sflags’ field. /
/ Use a function to determine keys arguments in a command line.
* Used for Redis Cluster redirect. */
// 从命令中判断命令的键参数。在 Redis 集群转向时使用。
redisGetKeysProc getkeys_proc;
/ What keys should be loaded in background when calling this command? /
// 指定哪些参数是 key
int firstkey; / The first argument that’s a key (0 = no keys) /
int lastkey; / The last argument that’s a key /
int keystep; / The step between first and last key */
// 统计信息
// microseconds 记录了命令执行耗费的总毫微秒数
// calls 是命令被执行的总次数
long long microseconds, calls;
};
注:redis命令不区分大小写; ·命令回复发送给客户端:命令实现函数会将命令回复保存到客户端的输出缓冲区里面,并为客户端的套接字关联命令回复处理器,**当客户端套接字变为可写状态时,服务器就会执行命令回复处理器**,将保存在客户端**输出缓冲区中的命令回复发送给客户端**。 当命令回复发送完毕之后,**回复处理器会清空客户端状态的输出缓冲区为处理下一个命令做准备**。 ·客户端接收并打印:如下 [外链图片转存中...(img-BvmHgQDJ-1618293129983)] ### **serverCron函数** 源码地址redis.c/serverCron,1300+行左右位置。 该函数**默认100毫秒(可配置)**执行一次,**负责管理服务器资源**,**并保持服务器自身的良好运转**。 建议结合源码及下图理解其功能: [外链图片转存中...(img-dtyYnj3w-1618293129984)] ![img](https://ucc.alicdn.com/images/user-upload-01/img_convert/01c26db1180d61ab41c3c6d81f68db37.png)