本文将分五个部分来分析和总结Redis的内部机制,分别是:Redis数据库、Redis客户端、Redis事件、Redis服务器的初始化步骤、Redis命令的执行过程。
首先介绍一下Redis服务器的状态结构。Redis使用一个类型为“redisServer”的数据结构来保存整个Redis服务器的状态(每个属性按照即将讲解的顺序进行排序):
struct redisServer {
int dbnum;//服务器的数据库数量,值由服务器配置的“databases”选项决定,默认为16
redisDb *db;//数组,保存着服务器中的所有数据库
list *clients;//一个链表,保存了所有客户端状态,每个链表元素都是“redisClient”结构
time_t unixtime;//保存秒级精度的系统当前UNIX时间戳,减少获取系统当前时间的系统调用次数,100毫秒更新一次
long long mstime;//保存毫秒级精度的系统当前UNIX时间戳
unsigned lruclock;//默认每10秒更新一次,用于计算数据库键的空转时长,数据库键的空转时长 = 服务器的“lruclock”属性值 - 数据库键值对象的“lru”属性值
long long ops_sec_last_sample_time;//上一次进行服务器每秒执行命令数量抽样的时间
long long ops_sec_last_sample_ops;//上一次进行服务器每秒执行命令数量抽样时,服务器已执行命令的数量
long long ops_sec_samples[REDIS_OPS_SEC_SAMPLE];//环形数组,每个元素记录一次服务器每秒执行命令数量抽样结果,估算服务器在最近一秒钟处理的命令请求数量(数组长度默认为16,100毫秒更新一次)
int ops_sec_idx;//ops_sec_samples数组的索引值,每次抽样后值增1,等于16时重置为0
size_t stat_peak_memory;//已使用内存峰值
int shutdown_asap;//关闭服务器的标识,1表示关闭,0不关闭
pid_t rdb_child_pid;//记录执行BGSAVE命令的子进程的ID,-1表示服务器没有正在执行BGSAVE
pid_t aof_child_pid;//记录执行BGREWRITEAOF命令的子进程的ID,-1表示服务器没有正在执行BGREWRITEAOF
int aof_rewrite_scheduled;//1表示有BGREWRITEAOF命令被延迟了(服务器执行BGSAVE期间收到的BGREWRITEAOF会被延迟到BGSAVE执行完成之后执行)
struct saveparam *saveparams;//记录了自动保存条件的数组(执行BGSAVE的条件)
long long dirty;//修改计数器(上一次执行BGSAVE之后已经产生了多少修改)
time_t lastsave;//上一次执行自动保存操作(BGSAVE)的时间
sds aof_buf;//AOF缓冲区
int cronloops;//serverCron函数的运行次数计数器
lua;//用于执行Lua脚本的Lua环境
redisClient *lua_client;//Lua脚本的伪客户端,在服务器运行的整个生命周期一直存在,直至服务器关闭才会关闭
dict *lua_scripts;//字典,记录所有载入的Lua脚本,键为某个Lua脚本的SHA1校验和,值为对应的Lua脚本
dict *repl_scriptcache_dict;//字典,记录已经传播给所有从服务器的所有Lua脚本,键为脚本的SHA1校验和,值为NULL,用于EVALSHA1命令的复制
long long slowlog_entry_id;//下一条慢查询日志的ID
list *slowlog;//保存了所有慢查询日志的链表
long long slowlog_log_slower_than;//服务器配置“slowlog-log-slower-than”选项的值,表示查询慢于多少微秒便记录慢查询日志
unsigned long slowlog_max_len;//服务器配置“slowlog-max-len”选项的值,表示服务器最多保存多少条慢查询日志记录,若超出,最久的记录会被覆盖
monitors;//链表,监视器客户端列表
dict *pubsub_channels;//字典,保存所有频道的订阅关系,键为某个被订阅的频道,值为链表,记录了所有订阅这个频道的客户端
list *pubsub_patterns;//链表,保存所有模式的订阅关系,每个链表节点都包含了订阅的客户端和被订阅的模式
};
Redis数据库
在Redis服务器状态结构中,“dbnum”属性记录了服务器的数据库数量,它的值可以通过服务器配置的“databases”选项决定,默认为16。“db”属性是一个数组,保存保存着服务器中的所有数据库,其中每一个数据库都对应一个“redisDb”数据结构:
struct redisDb {
dict *dict;//数据库键空间字典,保存数据库中所有的键值对
dict *expires;//过期字典,保存数据库中所有键的过期时间
dict *watched_keys;//字典,正在被WATCH命令监视的键
};
数据库键空间
Redis数据库结构的“dict”属性是Redis数据库的键空间,底层由字典实现。所有在数据库上的增删改查,实际上都是通过对键空间字典进行相应操作来实现的。除此之外,还需要进行一些额外的维护操作,主要有如下操作内容:
(1)在读取一个键之后,服务器会根据键是否存在来更新服务器的键空间命中次数或不命中次数(这两个值可以在'info stats'命令返回中的'keyspace_hits'属性和'keyspace_misses'属性中查看)。
(2)在读取一个键之后,服务器会更新键的LRU属性值(最后一次使用时间,使用'object idletime'命令可以查看键的闲置时间)。
(3)在读取一个键时,若发现该键已过期,则删除这个键。
(4)如果有客户端使用'watch'命令监视了某个键,服务器在对被监视的键进行修改之后,会将这个键标记为脏,从而让事务程序注意到这个键已经被修改过了。
(5)服务器每次修改一个键之后,都会对脏键计数器(即Redis服务器状态的'dirty'属性)的值加一,这个计数器会触发服务器的持久化以及复制操作。
(6)如果服务器开启了数据库通知功能,在对键进行修改之后,服务器将按配置发送相应的数据库通知。
过期字典
Redis数据库结构的“expires”属性保存了Redis数据库所有拥有过期时间的键以及它们对应的过期时间,底层同样由字典实现。数据库键过期时间的设置和删除,实际上都是对过期字典的操作。其中,字典的键是一个个指针,分别指向键空间字典中的一个个键对象(共享对象,节省内存空间);字典的值则是一个个long long类型的整数表示的毫秒精度的UNIX时间戳,保存数据库键的过期时间。
1. 键过期时间设置
expire:以秒为单位,设置Redis键的生存时间。
pexpire:以毫秒为单位,设置Redis键的生存时间。
expireat:以秒为单位,设置Redis键的过期时间。
pexpireat:以毫秒为单位,设置Redis键的过期时间。
注:实际上'expire'、'pexpire'、'expireat'命令最后都会转换为'pexpireat'命令来执行。
2. 键过期时间查看
ttl:以秒为单位,返回键的剩余生存时间。
pttl:以毫秒为单位,返回键的剩余生存时间。
3. 键过期判定
检查当前Unix时间戳是否大于键的过期时间,是则过期,否则不过期。
4. 过期键的理论删除策略
(1)定时删除:
设置一个键过期时间的同时,创建一个定时器。每个带有过期时间的键都对应着一个定时器。
这种策略对内存是最友好的,但对CPU时间是最不友好的。创建一个定时器需要用到Redis服务器中的时间事件,而当前时间事件的实现方式为无序链表,查找一个事件的时间复杂度为O(N),并不能高效地处理大量时间事件。
(2)惰性删除:
访问一个键的时候再检测该键是否过期,是则删除之。
这种策略对CPU时间是最友好的,但对内存是最不友好的。没被访问到的过期键永远不会被删除,可以看做内存泄露。对于运行状态非常依赖于内存的Redis来说,这种策略显然会影响到Redis的性能。
(3)定期删除:
这种策略是对前两种策略的整合与折中方案。使用这种策略需要控制好删除操作每次执行的时长和执行的频率,否则会退化为前两种策略的其中一种。
5. Redis采用的过期键删除策略
Redis服务器实际使用的是惰性删除和定期删除两种策略配合使用的方案。
(1)惰性删除策略的实现:
所有读写数据库的Redis命令在执行之前都会先检查输入键是否已过期,过期则删除之。
(2)定期删除策略的实现:
在规定时间内,分多次遍历服务器中的各个数据库,从数据库的过期字典中随机检查一部分键的过期时间,并删除其中的过期键。
<1>定期删除程序每次运行时,都会从一定数量的数据库中取出一定数量的随机键进行检查,并删除其中的过期键。
<2>使用一个全局变量记录当前删除程序检查的是第几个数据库,下一次运行都会接着上一次的进度进行处理。
<3>随着删除程序的不断执行,服务器中所有的数据库都会被检查一遍,然后这个全局变量被重置为0,开始新一轮的检查工作。
6. AOF、RDB和复制功能对过期键的处理
(1)生成RDB文件:
在执行save或bgsave命令创建一个新的RDB文件时,程序会对数据库中的键进行检查,已过期的键不会被保存到新创建的RDB文件中。
(2)载入RDB文件:
<1>主服务器模式:载入RDB文件时,程序会对文件中保存的键进行检查,只有未过期的键会被载入到数据库中。
<2>从服务器模式:文件中保存的所有键都会被载入到数据库中。不过因为主从服务器在进行数据同步的时候,从服务器的数据库会被清空,所以过期键对载入RDB文件的从服务器也不会造成影响。
(3)AOF文件写入:
当过期键被惰性删除或定期删除之后,程序会向AOF文件追加一条del命令,来显式地记录该键已被删除。
(4)AOF重写:
程序会对数据库中的键进行检查,已过期的键不会被保存到重写后的AOF文件中。
(5)复制:
当服务器运行在复制模式下时,从服务器的过期键删除动作由主服务器控制:
<1>主服务器在删除一个过期键之后,会显式地向所有从服务器发送一个del命令,告知从服务器删除这个过期键。
<2>从服务器在执行客户端发送的读命令时,即使碰到过期键也不会将其删除,而是将过期键的值继续返回给客户端。
<3>从服务器只有在接到主服务器发送来的del命令之后,才会删除过期键。
Redis客户端
Redis服务器状态结构中的“clients”属性是一个链表,保存了所有连接到当前服务器的客户端状态,每个客户端状态使用类型为“redisClient”的数据结构进行表示(每个属性按照即将讲解的顺序进行排序):
//Redis客户端的状态结构
struct redisClient {
redisDb *db;//记录客户端当前正在使用的数据库
int fd;//客户端正在使用的套接字描述符,-1表示伪客户端(AOF文件或者Lua脚本),大于-1表示普通客户端
robj *name;//客户端名字
int flags;//客户端标志,记录了客户端的角色,以及客户端目前所处的状态
sds querybuf;//输入缓冲区,根据输入内容动态地缩小或扩大,但不能超过1GB,否则服务器将关闭这个客户端
robj **argv;//命令与命令参数,数组,每个元素都是一个字符串对象,argv[0]为命令,其余元素为参数
int argc;//argv数组的长度
struct redisCommand *cmd;//当前执行的命令的实现函数,指向命令表中的命令结构
char buf[REDIS_REPLY_CHUNK_BYTES];//固定大小输出缓冲区,数组,默认大小为16KB
int bufpos;//buf数组目前已使用的字节数量
list *reply;//可变大小输出缓冲区,链表
obuf_soft_limit_reached_time:记录了“reply”输出缓冲区第一次到达软性限制的时间,用于计算持续超出软性限制的时长,以此决定是否关闭客户端
int authenticated;//0表示未通过身份验证,1表示已通过身份验证
time_t ctime:创建客户端的时间,可用于计算客户端与服务器连接的时间长度
time_t lastinteraction:客户端与服务器最后一次进行互动的时间,可用于客户端的空转时长
multiState mstate;//事务状态,包含一个事务队列,以及一个已入列命令计数器
};
“db”:
是一个指针,指向Redis服务器状态结构中的“db”数组其中一个元素,表示当前客户端正在使用的数据库。
默认情况下,Redis客户端的目标数据库为0号数据库,可以通过select命令切换,所以select命令的实现原理为:修改redisClient.db指针,让它指向服务器中指定的数据库。
示例图:
“fd”:
连接当前客户端与Redis服务器的套接字描述符。值为-1表示伪客户端(AOF文件或者Lua脚本),值大于-1则表示普通客户端。
Redis客户端分为普通客户端与伪客户端两种类型,其中通过网络连接与Redis服务器进行连接的就是普通客户端,反之则是伪客户端了。伪客户端也有两种类型,分别是Lua脚本的伪客户端和AOF文件的伪客户端。Redis服务器状态结构的“lua_client”属性就保存了Lua脚本的伪客户端,它会在Redis服务器初始化时就被创建,负责执行Lua脚本中包含的Redis命令,在服务器运行的整个生命周期一直存在,直至服务器关闭才会关闭。而AOF伪客户端则是在载入AOF文件时被创建,用于执行AOF文件中的Redis命令,在AOF文件载入完成之后被关闭。
client list:列出目前所有连接到服务器的普通客户端。
“name”:
当前客户端名字。
client setname:为客户端设置一个名字。
“flags”:
客户端标志,记录了客户端的角色,以及客户端目前所处的状态。例如:REDIS_MASTER表示当前客户端是一个主服务器;REDIS_BLOCKED表示当前客户端正在被列表命令阻塞。它的值可以是单个标志,也可以是多个标志的二进制或。
“querybuf”:
输入缓冲区,存储客户端输入的内容,可以根据输入内容动态地缩小或扩大,但不能超过1GB,否则服务器将关闭这个客户端。
“argv” & “argc”:
这两个属性的值都是由输入缓冲区的内容分析得来的。其中“argv”属性是一个数组,数组的每个元素都是一个字符串对象,argv[0]为客户端当前执行的命令,其余元素为传给该命令的参数。而“argc”属性则记录了“argv”数组的长度。
“cmd”:
当前执行的命令的实现函数,指向命令表中的命令结构。
Redis服务器中保存着一个由字典实现的命令表,服务器会根据agrv[0]的值(不区分字母大小写),在命令表中查找命令对应的命令实现函数,然后将“cmd”指针指向这个函数。
命令表示例图:
redisCommand结构:保存了命令的实现函数、命令的标志、命令应该给定的参数个数,命令的总执行次数和总消耗时长等统计信息。
“buf” & “bufpos”:
“buf”属性是一个数组,作为固定大小的输出缓冲区,默认大小为16KB,用于保存长度比较小的回复(服务器给客户端的回复)。
“bufpos”属性则记录了“buf”目前已经使用的字节数。
“reply”:
链表,可变大小的输出缓冲区,用于保存长度比较大的回复。
当“buf”数组的空间已用完,或者因为回复太大而没办法放进“buf”数组时,服务器就会开始使用可变大小缓冲区。
但可变大小的缓冲区也是有限制的,分为硬性限制与软性限制两种模式,一旦超过硬性限制服务器会立刻关闭客户端,若是超过软性限制,客户端不会立刻被关闭,但若是持续一段时间一直超过软性限制,服务器也是会关闭客户端的。这两种限制可以使用Redis配置的“client-output-buffer-limit”选项来进行配置:
client-output-buffer-limit
配置示例(以下分别为普通客户端、从服务器客户端、执行发布与订阅功能的客户端设置不同的软性限制与硬性限制):
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit slave 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60
“obuf_soft_limit_reached_time”:
记录了“reply”输出缓冲区第一次到达软性限制的时间,用于计算持续超出软性限制的时长,以此决定是否关闭客户端。
“authenticated”:
身份验证的标识,值为0表示未通过身份验证,1则表示已通过身份验证。
“ctime”:
创建客户端的时间,可用于计算客户端与服务器连接的时间长度。
client list:“age”域记录了客户端与服务器连接的时间长度。
“lastinteraction”:
客户端与服务器最后一次进行互动的时间,可用于客户端的空转时长。
client list:“idle”域记录了客户端的空转时长。
Redis事件
Redis服务器是一个事件驱动程序,需要处理两类事件:文件事件与时间事件。
文件事件
Redis服务器通过套接字与客户端或其他Redis服务器进行连接,而文件事件就是服务器对套接字操作的抽象。服务器与客户端或其他服务器的通信会产生相应的文件事件,而服务器通过监听并处理这些事件来完成一系列网络通信操作。
Redis基于Reactor模式开发了自己的网络事件处理器——文件事件处理器,文件事件处理器使用I/O多路复用程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。当被监听的套接字准备好执行连接应答、读取、写入、关闭等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前已关联好的事件处理器来处理这些事件。
文件事件处理器的构成:
(其中I/O多路复用程序通过队列向文件事件分派器传送套接字)
时间事件
Redis服务器中的一些操作需要在给定时间点执行,时间事件就是服务器对这类定时操作的抽象。
1. 时间事件的分类
(1)定时事件:让一段程序在指定的时间之后执行一次。
(2)周期性事件:让一段程序每隔指定的时间就执行一次。
2. 时间事件的属性
(1)id:Redis服务器为每个时间事件创建的全局唯一ID,从小到大递增。
(2)when:毫秒精度的UNIX时间戳,记录了时间事件的到达时间。
(3)timeProc:时间事件处理器,一个时间事件到达时就会被服务器调用的函数。
3. 时间事件的实现
服务器将所有时间事件都放在一个无序链表中,新加入的时间事件总是插入到链表的表头中。每当时间事件执行器运行时,它就遍历整个链表,查找所有已经到达的时间事件,并调用相应的事件处理器。
正常模式下Redis服务器只使用serverCron一个时间事件,而在benchmark模式下,服务器也只使用两个时间事件。所以时间事件无序链表几乎退化成一个指针,使用它来保存时间事件并不影响事件执行的性能。
4. 周期性事件serverCron
持续运行的Redis服务器需要定期对自身的资源和状态进行检查和调整,从而确保服务器可以长期、稳定地运行,这些定期操作就由周期性事件serverCron来负责执行。周期性事件serverCron会每隔一段时间执行一次,直到服务器关闭为止。serverCron默认每隔100毫秒执行一次,可以通过Redis配置中的“hz”选项来设置serverCron的每秒回执行次数。
serverCron函数主要负责执行的有以下操作:
(1)更新服务器时间缓存
为了减少获取系统当前时间需要执行的系统调用次数,Redis服务器使用状态结构中的“unixtime”和“mstime”两个属性分别缓存秒级精度和毫秒级精度的系统当前UNIX时间戳,但是只能用于对时间精确度要求不高的功能,对时间精确度高的功能还是会执行系统调用来获取系统当前时间。
(2)更新LRU时钟
使用服务器状态的“lruclock”属性来保存,默认每10秒更新一次,用于计算数据库键的空转时长:数据库键的空转时长 = 服务器的“lruclock”属性值 - 数据库键值对象的“lru”属性值。
INFO SERVER:“lru_clock”域的值就是服务器状态的“lruclock”属性值。
(3)更新服务器每秒执行命令次数
INFO STATUS:“instantaneous_ops_per_sec”域的值就是Redis服务器在最近一秒钟执行的命令数量。
这个值是根据抽样计算得到的所有结果的平均值。serverCron每100毫秒就进行一次抽样计算,其中,Redis服务器状态的“ops_sec_last_sample_time”属性记录上一次进行抽样的时间,“ops_sec_last_sample_ops”属性记录上一次抽样时服务器已执行命令的数量,“ops_sec_samples”数组则用于存放所有抽样计算的结果,默认长度为16,“ops_sec_idx”属性指定本次抽样计算结果应放入“ops_sec_samples”数组的哪个索引位置,它的值在每次抽样后自增1,等于16时重置为0。所以每次抽样计算的过程大概如下:
ops_sec_samples[ops_sec_idx] = (服务器当前已执行命令数量 - ops_sec_last_sample_ops属性值) / (服务器当前时间 - ops_sec_last_sample_time属性值) * 1000;
ops_sec_last_sample_ops属性值 = 服务器当前已执行命令数量;
ops_sec_last_sample_time属性值 = 服务器当前时间;
ops_sec_idx ++;
if(ops_sec_idx == 16) ops_sec_idx = 0;
最后,服务器每秒执行命令次数 = ops_sec_samples数组元素总和 / ops_sec_samples数组长度,所以它只是一个估算值。
(4)更新服务器内存峰值记录
服务器状态的“stat_peak_memory”属性记录了服务器已使用的内存峰值。serverCron每次执行都会比较“stat_peak_memory”属性值与Redis服务器当前使用的内存数量,若当前使用的内存数量大于“stat_peak_memory”属性值,则使用当前使用的内存数量更新“stat_peak_memory”属性的值。
INFO MEMORY:“used_memory_peak”域和“used_memory_peak_human”域分别以两种格式记录了服务器的内存峰值。
(5)处理SIGTERM信号
在Redis服务器启动时,Redis会为服务器进程的SIGTERM信号关联一个信号处理器,这个信号处理器负责载服务器收到SIGTERM信号时,将Redis服务器状态的“shutdown_asap”属性值置为1。
“shutdown_asap”属性是Redis服务器的关机标识,serverCron每次运行都会对它的值进行检查,若其值为1则关闭Redis服务器,关闭之前会先进行RDB持久化。
(6)管理客户端资源
serverCron每次运行都会对一定数量的客户端进行以下两个检查:
<1>如果客户端与服务器之间的连接已经超时,即客户端已经在很长一段时间内没有与服务器互动,则关闭这个客户端。
<2>若客户端的输入缓冲区大小超过了一定长度,则释放当前输入缓冲区,重新创建一个默认大小的输入缓冲区,防止耗费过多内存。
<3>若客户端输出缓冲区大小超出限制,则关闭客户端。
(7)管理数据库资源
serverCron每次运行都会对一部分数据库进行检查,删除其中的过期键,并在有需要时对字典进行收缩操作。
(8)检查持久化操作运行状态、标识与运行条件
Redis服务器状态中与持久化操作的运行状态、标识以及运行条件相关的属性有以下六个:
<1>rdb_child_pid:记录执行BGSAVE命令的子进程的ID,值为-1表示服务器没有正在执行BGSAVE命令。
<2>aof_child_pid:记录执行BGREWRITEAOF命令的子进程的ID,值为-1表示服务器没有正在执行BGREWRITEAOF命令。
<3>aof_rewrite_scheduled:BGREWRITEAOF命令延迟执行的标识,值为1表示有BGREWRITEAOF命令被延迟了(服务器执行BGSAVE命令期间收到的BGREWRITEAOF命令请求会被延迟到BGSAVE执行完成之后执行)。
<4>saveparams:记录了自动保存条件的数组,即执行BGSAVE的条件。可由“save”选项进行配置,例如:save 900 1,表示在900秒之内,对数据库至少进行了1次修改,则执行BGSAVE命令。可以配置个条件。
<5>dirty:修改计数器,记录上一次执行BGSAVE之后已经产生了多少修改。
<6>lastsave:上一次执行自动保存操作(BGSAVE)的时间。
serverCron每次运行都会检查“rdb_child_pid”和“aof_child_pid”两个属性的值,只要其中一个属性的值不为-1,就检查子进程是否有信号发送给服务器进程:
<1>若有信号,表示新的RDB文件已经生成完毕,或者AOF文件已经重写完成,服务器需要进行相应命令的后续操作,比如用新的RDB文件替换现有的RDB文件,或者用重写后的AOF文件替换现有的AOF文件。
<2>若没信号,则表示持久化操作尚未完成,程序不做任何操作。
如果检查之后发现“rdb_child_pid”和“aof_child_pid”两个属性的值都为-1,表示服务器没有正在进行持久化操作,这时会按以下三个步骤进行相应检查:
<1>检查“aof_rewrite_scheduled”属性的值,若为1,表示有BGREWRITEAOF操作被延迟了,则开始一次新的BGREWRITEAOF操作。
<2>检查服务器的自动保存条件是否满足:循环取出“saveparams”数组中的所有条件配置,逐个与“dirty”和“lastsave”属性的值进行对比,只要其中一个条件配置的时间间隔大于“dirty”属性值,并且修改数量大于“lastsave”属性值,则表示自动保存的条件已经满足,若此时服务器没有正在执行其他持久化操作,则开始一次新的BGSAVE操作。
<3>检查服务器设置的AOF重写条件是否满足,如果满足,并且服务器没有正在执行其他持久化操作,自动开始一次新的BGREWRITEAOF操作。
整个检查过程的流程图:
(9)将AOF缓冲区的内容写入AOF文件
如果服务器开启了AOF持久化功能,serverCron运行时会检查AOF缓冲区“aof_buf”中有没有内容,若有,则将AOF缓冲区中的内容写入AOF文件中。
(10)增加cronloops计数器的值
Redis服务器状态的“cronloops”属性记录了serverCron函数执行的次数,serverCron会在每次执行之后将“cronloops”属性的值加一。
事件的调度与执行
(1)获取到达时间与当前时间最接近的时间事件。
(2)阻塞并等待文件事件产生。(避免频繁轮询时间事件)
(3)若有文件事件产生,则处理文件事件。
(4)若获取的时间事件的到达时间已到,则执行时间事件,完成之后重新从步骤一开始新一轮的事件循环。
Redis服务器对文件事件和时间事件的处理都是同步、有序、原子地执行的。因为时间事件在文件事件之后执行,并且事件之间不会出现抢占,所以时间事件的实际处理时间,通常会比时间事件设定的到达时间稍晚一些。
Redis服务器初始化步骤
(1)初始化服务器状态结构
创建一个struct redisServer类型的实例变量作为服务器的状态,并为结构中的各个属性设置默认值,例如:服务器的运行ID、默认配置文件路径、默认端口等等,同时创建Redis命令表。
(2)载入配置选项
载入用户指定的配置参数和配置文件,并根据用户设定的配置,对服务器状态变量的相关属性进行修改。
(3)初始化服务器数据结构
这一步主要是为服务器状态中的一些数据结构分配内存,例如:
<1>“clients“:链表,保存所有与服务器连接的客户端的状态结构。
<2>”db“:字典保存服务器的所有数据库。
<3>”pubsub_channels“:字典,保存频道订阅信息。
<4>“pubsub_patterns”:链表,保存模式订阅信息。
<5>”lua“:用于执行Lua脚本的Lua环境。
<6>”slowlog“:用于保存慢查询日志。
除此之外,还会进行一些非常重要的设置操作,例如:
<1>为服务器设置进程信号处理器。
<2>创建共享对象,例如经常经常用到的“OK”回复字符串对象,1到10000的字符串对象等等。
<3>为serverCron函数创建时间事件。
<4>如果AOF持久化功能已经打开,则打开现有的AOF文件,若AOF文件不存在,则创建并打开一个新的AOF文件,为AOF写入做好准备。
<5>初始化服务器的后台I/O模块,为将来的I/O操作做好准备。
(4)还原数据库状态
若服务器启用了AOF持久化功能,则载入AOF文件,否则载入RDB文件,根据AOF文件或RDB文件记录的内容还原数据库状态,同时在日志文件中打印出载入文件并还原数据库状态所耗费的时长。
(5)执行事件循环
一切准备就绪,开始执行服务器的事件循环,开始接受客户端的连接请求,处理客户端发送的命令请求。
Redis命令请求的执行过程
(1)发送命令请求
当用户在客户端中键入一个命令请求时,客户端会将这个命令请求转换成协议格式,然后通过连接到服务器的套接字,将协议格式的命令请求发送给服务器。
(2)读取命令请求
当客户端与服务器之间的连接套接字因为客户端的写入而变得可读时,服务器将调用命令请求处理器来执行以下操作:
<1>读取套接字中协议格式的命令请求,并将其保存到客户端状态的输入缓冲区“querybuf”中。
<2>对输入缓冲区中的命令请求进行分析,提取出命令请求中包含的命令参数,计算命令参数的个数,然后分别将它们保存到客户端状态的“argv”属性和“argc”属性中。
<3>调用命令执行器,执行客户端指定的命令。
(3)命令执行器——查找命令
根据客户端状态的argv[0]参数,在命令表中查找参数所指定的命令(查找结果不受命令名字大小写影响),并将其保存到客户端状态的“cmd”属性中。(命令表是一个字典,字典的键是命令的名字,字典的值是一个“redisCommand”结构,记录着Redis命令的实现函数与一些统计信息)
设置客户端状态的“cmd”属性的示例图:
(4)命令执行器——执行预备操作
<1>检查客户端状态的“cmd”指针是否指向NULL,以此判断用户输入的命令是否存在。
<2>根据客户端状态的“cmd”属性指向的“redisCommand”结构中的“arity”属性值和客户端状态的“argc”属性值,判断用户输入的命令参数个数是否正确。
<3>通过客户端状态的“authenticated”属性值判断客户端是否已经通过了身份验证,未通过只能执行AUTH命令。
<4>如果服务器打开了“maxmemory”功能,在执行命令之前,需要先检查服务器的内存占用情况,并在有需要时进行内存回收,若内存回收失败则不再执行后续步骤,向客户端返回一个错误。
<5>如果服务器上一次执行BGSAVE命令时出错,并且服务器打开了"stop-writes-on-bgsave-error"功能, 而且服务器即将要执行的命令是一个写命令,那么服务器将拒绝执行这个命令,并向客户端返回一个错误。
<6>如果客户端当前正在用SUBSCRIBE命令订阅频道,或者正在用PSUBSCRIBE命令订阅模式, 那么服务器只会执行客户端发来的SUBSCRIBE 、PSUBSCRIBE 、UNSUBSCRIBE 、PUNSUBSCRIBE四个命令,其他别的命令都会被服务器拒绝。
<7>如果服务器正在进行数据载入,那么客户端发送的命令必须带有“l”标识(比如INFO 、SHUTDOWN 、PUBLISH,等等)才会被服务器执行,其他别的命令都会被服务器拒绝。
<8>如果服务器因为执行Lua脚本而超时并进入阻塞状态,那么服务器只会执行客户端发来的SHUTDOWN nosave命令和SCRIPT KILL命令,其他别的命令都会被服务器拒绝。
<9>如果客户端正在执行事务,那么服务器只会执行客户端发来的EXEC 、DISCARD 、MULTI 、WATCH四个命令,其他命令都会被放进事务队列中。
<10>如果服务器打开了监视器功能,那么服务器会将要执行的命令和参数等信息发送给监视器。
(5)命令执行器——调用命令的实现函数
client->cmd->proc(client);//client是指向客户端状态的指针
调用实现函数执行指定操作,产生的相应的命令回复,将其保存到客户端状态的输出缓冲区中,并为客户端的套接字关联命令回复处理器,这个处理器负责将命令回复返回给客户端。
(6)命令执行器——执行后续工作
<1>如果服务器开启了慢查询日志功能,那么慢查询日志模块会检查是否需要为刚刚执行完的命令请求添加一条新的慢查询日志。
<2>根据刚刚执行命令所耗费的时长,更新被执行命令的“redisCommand”结构的“milliseconds”属性, 并将命令的“redisCommand”结构的“calls”计数器的值增一。
<3>如果服务器开启了AOF持久化功能, 那么AOF持久化模块会将刚刚执行的命令请求写入到AOF缓冲区里面。
<4>如果有其他从服务器正在复制当前这个服务器,那么服务器会将刚刚执行的命令传播给所有从服务器。
(7)将命令回复发送给客户端
当客户端套接字变为可写时,服务器就会执行命令回复处理器,将保存在客户端输出缓冲区的命令回复发送给客户端。发送完毕之后,清空客户端输出缓冲区。
(8)客户端接受并打印命令回复
客户端接收到协议格式的命令回复之后,将其转换成人类可读的格式,并打印在客户端屏幕上。