Redis单机数据库实现
为《Redis设计与实现》笔记
数据库
Redis服务器将所有的数据库使用redisServer
来保存,每个redisDb
表示一个数据库
struct redisServer {
// ...
// 使用数组保存所有的数据库
redisDb *db;
// 数据库数量
int dbnum;
// ...
}
数据库的数量由dbnum
属性决定,其默认值为16。当用户使用SELECT
命令切换数据库时,redis服务器将切换到响应的服务器进行操作,在redisClient
结构中使用指针进行记录当前操作的数据库
typedef struct redisClient {
// ...
// 当前使用的数据库
redisDb *db;
// ...
} redisClient;
数据库键空间
redis在redisDb
结构中使用一个dict
字典保存由该数据库所有的键值对,被称为键空间
typedef struct redisDb {
// ...
// 数据库键空间
dict *dict;
// ...
} redisDb;
redis中进行增删改查都是在对该dict
进行操作
在redis数据库进行操作时,服务器除了对键空间进行操作外,还会执行一些额外的维护操作:
- 在读取一个键后,服务器会根据键是否存在来更新服务器的键空间命中(hit)次数和不命中(miss)次数
- 在读取一个键后,服务器会更新键的LRU时间
- 如果服务器在读取一个键时法相改键已经过期,那么服务器会先删除这个过期键,再执行余下操作
- 在修改了一个键后,服务器将对脏(dirty)键计数器的值加1,该计数器会触发持久化以及复制操作
- 如果服务器开启了数据库通知功能,那么在对键进行修改之后,服务器将按照配置发送相应的数据库通知
键的生存时间或过期时间
通过EXPIREAT
命令或PEXPIREAT
命令,客户端可以以秒或毫秒为单位设置键的生存时间,指定时间后自动删除该键
通过EXPIRE
命令或PEXPIRE
命令,客户端可以设置键的过期时间(UNIX时间戳),当到达过期时间后将删除该键
redisDb中保存有一个名为expires
的字典指针,其指向的字典保存了所有键的过期时间,被称为过期字典,其值为long long类型的整数,保存键指向的数据库键的过期时间
typedef struct redisDb {
// ...
// 过期字典
dict *expires;
// ...
} redisDb;
过期删除策略
可能使用的删除策略有三种:
- 定时删除:在设置过期时间的同时,创建一个定时器,让定时器在键的过期来临时,立即执行对键的删除操作。定时删除需要创建大量定时任务,对CPU时间不友好。
- 惰性删除:放任键过期不管,每次从键空间获取键时,都检查键是否过期,若过期则删除该键。惰性删除使得大量过期键未被及时删除,内存占用高。
- 定期删除:每个一段时间,程序就对数据库进行一次检查,删除里面的过期键。定期删除每个一段时间执行一次删除操作,通过限制删除操作的执行时长和频率来减少对CPU时间的影响。
Redis使用的是惰性删除和定期删除两种策略
惰性删除
惰性删除策略由db.c/expireIfNeeded
函数实现,在所有的读写数据库操作前都会调用该函数进行惰性删除操作
定期删除
定期删除策略由redis.c/activeExpireCycle
函数实现,每当Redis服务器周期性调用redis.c/serverCron
函数时,activeExpireCycle
函数就会被调用,分多次遍历服务器中各个数据库,从数据库的过期字典中随机检查一部分过期时间并删除过期键。
伪代码如下:
// 每次检查的数据库数量
int DEFAULT_DB_NUMBER = 16;
//每个数据库检查的键数量
int DEFAULT_KEY_NUMBERs = 20;
// 检查进度记录
int current_db = 0;
void activateExpireCycle() {
// 防止检查的数据库数量大于数据量总数
int db_numbers = min(server.dbnum, DEFAULT_DB_NUMBER);
for(int i = 0; i < db_number; ++i) {
// 当前检查的数据库
redisDb db = server.db[current_db];
// 下一个需要检查的数据库
current_db = (current_db + 1) % serber.dbnum;
// 检查数据库键
for(int j = 0; j < default_key_numbers; ++j) {
// 若该数据库没有设置过期时间则跳过该数据库的操作
if(db.expires.size() == 0)
break;
// 随机获取一个带有过期时间的键
robj* key_with_ttl = db.expires.get_random_key();
// 检查是否过期,过期则删除该键
if(is_expired(key_with_ttl))
delete_key(key_with_ttl);
// 若到达时间上限,直接停止操作
if(reach_time_limit())
return;
}
}
}
RDB中过期键的处理
在进行RDB持久化操作时,程序会对所有的键进行检查,已过期的键不会被保存到新创建的RDB文件中
在启动服务器并对RDB文件进行载入时:
- 如果服务器以主服务器模式运行,那么在载入RDB文件时,会对文件中的键进行检查,过期键不会载入到数据库中
- 如果服务器以从服务器模式运行,那么载入RDB文件时不会进行检查,过期键会在主从服务器同步时被删除
AOF中过期键的处理
当过期键被惰性删除或者定期删除后,程序会在AOF文件中追加一条DEL
命令,显示地记录其被删除
在执行AOF重写时,程序会对数据库中的键进行检查,过期键不会被保存到重写后的AOF文件中
复制是模式下的过期键处理
当服务器运行在复制模式下时,从服务器的过期键删除动作由主服务器控制
- 主服务器删除一个过期键后,会显示地先所有从服务器发送一个
DEL
命令,从服务器删除该键 - 从服务器在执行读命令时,即使碰到过期键也不会将其删除,而是继续像处理未过期的键一样进行处理
- 从服务器只有在街道主服务器发送过来的
DEL
命令之后才会执行删除操作
数据库通知
数据库通知可以让客户端通过订阅给定的频道或模式来获取数据库中键的变化,以及数据库中命令的执行情况,其分为两种:
- 键空间通知:关注某个键执行了什么命令
- 键事件通知:关注某个命令被什么键执行了
通知发送功能由notify.c
文件中的notifyKeyspaceEvent(int type, char *event, robj *key, int dbid)
函数实现,其中type
为当前想要发送的通知的类型,程序根据这个值来判断通知是否就是服务器配置选项锁选定的通知类型,从而决定是否发送通知。
每当一个redis操作需要发送通知的时候,就会调用notifyKeyspaceEvent
函数来实现通知的发送:
void saddCommand(redisClient *c) {
// ...
// 如果有元素添加成功
if(added) {
// ...
// 发送通知
notifyKeyspaceEvent(REDIS_NOTIFY_SET, "sadd", c->argv[1], c->db->id);
}
// ...
}
notifyKeyspaceEvent
函数的伪代码如下:
void notifyKeyspaceEvent(int type, char *event, robj *key, int dbid) {
// 如果给定的通知不是服务器允许发送的通知
if(!(server.notify_keyspace_events & type))
return;
// 发送键空间通知
if(server.notify_keyspace_events & REDIS_NOTIFY_KEYSPACE) {
// 构建频道名字为 __keyspace@{dbid}__:key
sds chan = setChannelName(dbid, key);
// 发送通知
pubsubPublishMessage(chan, event);
}
// 发送键事件通知
if(server.notify_keyspace_events & REDIS_NOTIFY_KEYSPACE) {
// 构建频道名字为 __keyspace@{dbid}__:event
sds chan = setChannelName(dbid, event);
// 发送通知
pubsubPublishMessage(chan, key);
}
}
RDB持久化
Redis为内存数据库,其提供了两种持久化方式,其中RDB持久化方式将数据库的状态,即数据库中所有的键值对信息存储在RDB文件中。
RDB文件的创建由rdb.c
中的rdbSave
函数完成
Redis提供了SAVE
和BGSAVE
两个命令来主动执行RDB持久化,其中SAVE
使用阻塞的方式进行持久化,BGSAVE
会新建一个子线程进行持久化
在BGSAVE
执行期间,服务器会拒绝所有的SAVE
和BGSAVE
操作,防止两个进程同时调用rdbSave
函数而产生竞争条件
redis带有自动保存功能,使用redisServer
的saveparam
数组来保存定时自动保存信息
struct redisServer {
// ...
// 定时自动保存信息
struct saveparam *saveparams;
// ...
};
struct saveparam {
// 秒数
time_t seconds;
// 修改数
int changes;
};
其默认值为:
# 900内执行了1次修改则触发保存操作
save 900 1
# 300内执行了10次修改则触发保存操作
save 300 10
# 60内执行了10000次修改则触发保存操作
save 60 10000
dirty计数器和lastsave属性
redisServer
保存有dirty
和lastsave
两个属性,分别表示上一次执行保存操作到现在的操作数和事件
struct redisServer {
// ...
// 修改计数器
long long dirty;
// 上次执行保存的事件
time_t lastsave;
// ...
};
dirty计数器记录的时修改元素的个数,而不是指令的个数,比如若使用SADD
指令增加了3个元素,则dirty+3
而不是dirty+1
每当Redis服务器周期性调用redis.c/serverCron
函数时,会进行判断是否满足保存条件,若满足则执行持久化操作
RDB文件结构
RDB文件包含以下几个部分:
名称 | 含义 |
---|---|
REDIS | 判断载入的文件是否是RDB文件,其占5个字节,保存"REDIS"五个字符 |
db-version | 记录RDB文件的版本号,占4个字节,为字符串形式的整数 |
databases | 包含零个或多个数据库及各个数据库中的键值对数据 |
EOF | 一个字节,表示正文结束 |
check_sum | 校验和,占8个字节 |
database部分的数据结构如下:
名称 | 含义 |
---|---|
SELECTDB | 数据库数据的开头,占一个字节,表示下一个值为数据库编号 |
db_number | 数据库编号,长度可能是1,2,5字节 |
key_value_pairs | 键值对数据 |
key_value_pairs部分的数据结构如下:
名称 | 含义 |
---|---|
EXPIRETIME_MS | 占一个字节,表示下一个值为过期时间,若该键值对没有过期时间则不存在该部分 |
ms | 过期时间,若该键值对没有过期时间则不存在该部分 |
TYPE | value的类型,决定如何读取和解释value的值 |
key | 保存的键的值,字符串对象数据 |
value | 值,保存大多以[长度 数据]的方式存储 |
value不同类型的存储情况:
- 字符串对象
- INT编码类型:
[encoding integer]
- RAW类型:[len string],若长度大于20字节,则执行LZF压缩:
[REDIS_RDB_ENCC_LZF compressed_len origin_len compressed_string]
,其中REDIS_RDB_ENCC_LZF
表示该字符串被压缩
- 列表对象:
[list_length item1 item2 ... itemN]
- 集合对象:
[set_size elem1 elem2 ... elemN]
- 哈希表对象:
[hash_size key1 value1 key2 value2 ... keyN valueN]
- 有序集合对象:
[sorted_set_size member1 score1 member2 score2 ... memberN scoreN]
,其中score
为一个浮点数,但是以字符串的形式存储,有序链表按照score
进行排序
AOF持久化
AOF持久化通过保存Redis服务器锁执行的写命令来记录数据库的状态
- 如果服务器开启了AOF持久化功能,那么服务器优先使用AOF文件来还原数据库状态
- 只有AOF持久化功能关闭时才会使用RDB文件还原数据库状态
AOF持久化分为以下步骤
- 命令追加:服务器执行完一个写命令后,会以协议格式将被执行的命令追加到服务器状态的
aof_buf
缓冲区的末尾
struct redisServer {
// ...
// AOF缓冲区
sds aof_buf;
// ...
};
- 文件写入和同步:服务器每次在结束完一个事件循环之前,都会调用
flushAppendOnlyFile
函数,考虑是否要将AOF缓冲区中的内容写入并同步到AOF文件中
flushAppendOnlyFile
存在三种持久化行为
- always:将AOF缓冲区中的所有内容写入并同步到AOF中
- everysec:将AOF缓冲区的内容写入AOF文件中,如果距离上次同步时间超过1s则进行同步
- no:将AOF缓冲区的内容写入到AOF文件中,同步时间由操作系统决定
缓冲区数据若为及时同步,若服务器发生停机,则未同步的数据将会永久丢失
AOF文件的载入和还原
Redis服务器只需要执行完AOF文件中所有的写命令即可还原数据库,其步骤如下:
- 创建一个不带网络连接的伪客户端,Redis命令只能在客户端上下文中执行
- 伪客户端载入并解析AOF文件
- 伪客户端执行AOF中解析出来的命令
AOF重写
AOF通过保存写命令来进行持久化,时间久了会造成AOF文件过于庞大导致服务器恢复的时间过长,这是就需要执行AOF重写,其生成的AOF文件将去除浪费空间的冗余命令
AOF重写中,会进行以下操作:
- 遍历每个数据库,忽略空数据库,将数据库中所有的键值对生成新的写命令来进行保存
- 对于集合类型,将对应的写命令转换为一条,表示通过一条指令执行完整个集合类型数据的插入,如果元素数量超过
REDIS_AOF_REWRITE_ITEMS_PER_CMD
的值,则将其拆分成多条指令 - 在遍历过程中,忽略过期键
- 旧的AOF文件将被重命名,新AOF文件将会将其覆盖
由于AOF重写需要消耗大量时间,Redis使用子进程来处理AOF重写任务,但这样会造成AOF重写过程中新的追加命令没有被写入。为了解决这个问题,redisServer
设置了一个AOF重写缓冲区,在创建完子进程后,新的操作会同时向AOF缓冲区和重写缓冲区中进行写入
事件
Redis服务器时一个事件驱动程序,其处理两种事件
- 文件事件:Redis服务器通过套接字与客户端或其他Redis服务器进行连接,文件事件就是套接字操作的抽象
- 时间事件:Redis服务器中的某些操作需要在给定的事件执行,时间事件就是就是这些定时操作的抽象
文件事件
Redis的文件事件处理器基于Reactor模式,以单线程的方式运行,其包括:
- 使用I/O多路复用的方式监听套接字,根据套接字目前执行的任务来为套接字管来奶不同的事件处理器
- 当被监听的套接字准备执行某项操作(连接应答(accept),写入,读取,关闭等)时,将会产生相应的文件事件,此时会调用套接字之前关联好的事件处理器来处理这些事件
文件事件处理器包括套接字,I/O多路复用程序,文件事件分派器(dispatcher)和事件处理器,其结构如下所示
I/O多路复用程序
Redis使用将select
,epoll
,evport
,kqueue
这些多路复用函数均进行了封装,并通过宏定义的方式来选择性能最高的多路复用函数库
#ifdef HAVE_EVPORT
#include "ae_evport.c"
#else
#ifdef HAVE_EPOLL
#include "ae_epoll.c"
#else
#ifdef HAVE_KQEUE
#include "ae_kqeue.c"
#else
#include "ae_select.c"
#endif
#endif
#endif
事件类型包括
名称 | 含义 |
---|---|
AE_READABLE | 当有套接字变得可读(客户端执行write操作或close操作)或有新的可应答(acceptable)时 |
AE_WRITABLE | 当有套接字可写(客户端执行read操作)时 |
AE_NONE | 没有产生事件 |
当套接字同时出现两种事件时,优先处理AE_READABLE
事件
文件事件处理器
文件事件处理器包括
- 连接应答处理器(acceptTcpHandler):用于对连接服务器监听套接字的客户端进行应答,是对
accept
函数的包装 - 命令请求处理器(readQueryFromClient):负责从套接字中读入客户端发送的命令请求内容,是对
read
函数的包装 - 命令恢复处理器(sendReplyToClient):负责将服务器执行命令后得到的命令恢复通过套接字返回给客户端,是对
write
函数的包装。
时间事件
Redis的时间事件分为两种:
- 定时事件:让一段程序在指定的事件之后执行
- 周期性事件:让一段程序每个指定事件就执行一次
当事件处理器返回值为AE_NOMORE
时为定时事件,表示执行后不再执行,当返回值不为AE_NOMORE
时为周期性时间,表示其隔一段时间后还会继续执行
时间事件包括三个属性:
名称 | 含义 |
---|---|
id | 时间事件的全局ID,设置为自增ID |
when | 毫秒精度的UNIX时间戳,记录了时间事件的到达时间 |
timeProc | 时间事件处理器,为一个处理函数 |
Redis服务器将所有的时间事件都放在一个无序链表中,新的事件在链表的头部插入。每当时间事件执行器运行时,就遍历整个链表,查找所有已到达的时间事件并调用相应的事件处理器
正常模式下Redis只是用serverCron
一个时间事件,而在benchmark模式下服务器也只使用两个时间事件,所以使用无序链表并不会影响性能
serverCron
severCron
函数负责定期对Redis服务器子u按和状态进行检查和调整,确保服务器可以长期、稳定地运行,其工作包括:
- 更新g服务器地各类统计信息,如时间,内存占用,数据库占用等
- 清理数据库中的过期键值对
- 关闭和清理连接失效的客户端
- 尝试进行AOF或RDB持久化操作
- 对于主服务器,会对从服务器进行定期同步
- 对于集群模式,进行定期同步和连接测试
redis服务器以周期的形式运行serverCron
函数,每隔一段时间就会调用一次,直到服务器关闭
事件调度与执行
事件的调度和执行有ae.c
中的aeProcessEvents
函数负责,其伪代码如下:
void aeProcessEvent() {
// 获取到达事件离当前事件最近的时间事件
time_event = aeSearchNearestTimer();
// 计算当前距离该时间事件到达还有多少秒
remaind_ms = time_event.when - unix_ts_now();
remaind_ms = remaind_ms > 0 ? remaind_ms : 0;
// 根据remaind_ms创建timevla结构
timeval = create_timeval_with_ms(remaind_ms);
// 阻塞等待文件事件产生,最大阻塞事件由timeval中的参数决定
aeApiPoll(timeval);
// 处理文件事件
processFileEvent();
// 处理已到达的时间事件
processTimeEvent();
}
客户端
当客户端连接时,服务器将使用redisClient
结构保存客户端的信息,其包括:
- 客户端连接套接字
- 客户端名称
- 客户端标志值
- 客户端正在使用的数据库的指针和数据库号码
- 和客户端当前要执行的命令及其参数和参数个数,以及指向命令实现函数的指针
- 客户端的输入和输出缓冲区
- 客户端的复制状态信息,以及进行复制所需的数据结构
- 客户端执行
BRPOP
,BLPOP
等列表阻塞命令时使用的数据结构 - 客户端的事务状态,以及执行
WATCH
命令时用到的数据结构 - 客户端执行发布与订阅功能时用到的数据结构
- 客户端的省份验证标志
- 客户端的创建时间和最后通信时间,以及客户端的处处缓冲区大小超出软性限制的时间
Redis以链表的形式保存客户端连接信息
客户端属性
套接字描述符
客户端属性中的套接字描述符fd
记录了和客户端的连接套接字,其取值情况如下:
- -1:表示其为伪客户端,伪客户端处理的命令请求来源于AOF文件或者Lua脚本而不是网络
- 大于-1的整数:为正常的客户端连接
typedef struct redisClient {
// ...
int fd;
// ...
} redisClient;
名字
默认情况下客户端连接是没有名字的,使用CLIEMENT setname
命令可以为客户端设置名字
typedef struct redisClient {
// ...
robj *name;
// ...
} redisClient;
name
指向的是一个striongObject
对象,若没有设置名字则为空指针
标志
标志flag
记录了客户端角色(role)和目前所述的状态,当具有多个角色状态时使用二进制或|
操作
typedef struct redisClient {
// ...
int flag;
// ...
} redisClient;
客户端角色包括:
名称 | 含义 |
---|---|
REDIS_MASTER | 在主从服务器进行复制操作时,主服务器会成为从服务器的客户端,而从服务器为主服务器的客户端,REDIS_MASTER 表示其为主服务器 |
REDIS_SLAVE | 表示其为从服务器 |
REDIS_PRE_PSYNC | 表示客户端为版本低于Redis2.8的从服务器,主服务器不能使用PSYNC 命令与该从服务器进行同步 |
REDIS_LUA_CLIENT | 表示其为专门用于处理Lua脚本的伪客户端 |
客户端状态包括:
名称 | 含义 |
---|---|
REDIS_MONITOR | 客户端正在执行MONITOR 命令 |
REDIS_UNIX_SOCKET | 服务器使用UNIX套接字来连接客户端 |
REDIS_BLOCKED | 客户端被BRPOP ,BLPOP 等命令阻塞 |
REDIS_UNBLOCKED | 客户端已从阻塞状态脱离出来 |
REDIS_MULTI | 客户端正在执行事务 |
REDIS_DIRTY_CAS | 事务使用WATCH 命令监视的数据库键已被修改,事务的安全性已经被破坏 |
REDIS_DIRTY_EXEC | 事务在命令入队时出现了错误,事务的安全性已经被破坏 |
REDIS_CLOSE_ASAP | 客户端的输出缓冲区大小超过了服务器运行的范围,服务器会在下一次执行serverCron 时将其关闭,缓冲区中的内容不会返回给客户端 |
REDIS_CLOSE_AFTER_REPLY | 由用户对这个客户端执行了CLIENT KILL 命令,或者客户端发送个服务器的命令中包含了错误的协议内容,服务器会返回缓冲区的内容并关闭该客户端 |
REDIS_ASKING | 客户端向集群节点发送了ASKING 命令 |
REDIS_FORCE_AOF | 强制服务器将当前执行的命令写入到AOF文件中 |
REDIS_FPRCE_REPL | 强制主服务器将当前的命令复制给所有从服务器,执行SCRIPT LOAD 命令会时客户端打开REDIS_FORCE_AOF 和REDIS_FPRCE_REPL 状态 |
REDIS_MASTER_FORCE_REPLY | 主从服务器进行命令传播期间,从服务器需要向主服务器发送REPLICATION ACK 命令,在发送这个命令之前,从服务器必须打开主服务器对应的客户端的REDIS_MASTER_FORCE_REPLY 标志,否则发送操作会被拒绝执行 |
输入缓冲区
客户端状态的输入缓冲区用于保存客户端发送的命令请求
客户端执行的命令使用argv
和argc
两个参数进行保存,当服务器从协议内容中分析并得出这两个属性时,服务器将根据argv[0]
的值在命令表中查找命令的实现函数
typedef struct redisClient {
// ...
// 输入缓冲区
sds qyerybuf;
// 客户端命令的命令参数
robj **argv;
// 命令参数的个数
int atgc;
// argv[0]所对应的函数实现
struct redisCommand *cmd;
// ...
} redisClient;
输出缓冲区
输出缓冲区保存执行命令所得到的回复,每个客户端都由两个输出缓冲区可用,其中固定大小的缓冲区用于保存那些长度比较小的恢复,如OK
,简短的字符串值和错误回复等,可变大小的缓冲区用于保存那些长度比较大的回复,如长字符串和列表等
可变大小缓冲区使用多个字符串组成的链表组成
typedef struct redisClient {
// ...
// 固定大小的缓冲区
char buf[REDIS_REPLY_CHUNK_BYTES];
// 固定大小缓冲区目前所使用的字节数
int bufpos;
// 非固定大小的缓冲区
list *reply;
// ...
} redisClient;
身份验证
客户端状态的authenticated
属性用于记录客户端是否通过了身份验证,取值为0表示为通过验证,取值为1表示通过了验证
typedef struct redisClient {
// ...
int authenticated;
// ...
} redisClient;
当客户端为通过验证时,除了AUTH
命令之外其他命令都会被服务器拒绝
时间
typedef struct redisClient {
// ...
// 客户端连接创建时间
time_c ctime;
// 客户端与服务器最后一次互动时间
time_c lastinteraction;
// 输出缓冲区第一次到达软性限制的时间
time_c obuf_soft_limit_reached_time;
// ...
} redisClient;
若输出缓冲区超过硬性限制大小则关闭连接,若超过软性限制而为超过硬性限制时,当obuf_soft_limit_reached_time
超过预设值时关闭连接
服务器
Redis服务器负责与多个客户端建立连接,处理客户端发送的命令请求,保存客户端执行命令所产生的数据,并通过资源管理来维持服务器自身的运转
命令请求执行过程
当客户端发送SET KEY VALUE
时:
- 客户端向服务器发送请求
- 服务器接收并处理客户端发送来的命令请求,在数据库中进行设置操作并产生回复
OK
- 服务器将
OK
发送给客户端 - 客户端接收服务器返回的命令回复,并将这个回复打印给用户观看
用户键入SET KEY VALUE
时,会将其转换成相应协议后再发送给服务器
*3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n
*3\r\n
$3\r\n
SET
\r\n
$3\r\n
KEY\r\n
$5\r\n
VALUE\r\n
服务器接收到指令后进行解析,保存到argc
和argv
中
查找命令实现
服务器根据解析后的内容查找相应的实现函数
typedef clientCommand {
// 命令名称
char *name;
// 函数指针,指向命令实现的函数
redisCommandProc *proc;
// 参数个数
int arity;
// 命令的属性,是读属性还是写属性等
char *sflag;
// 对sflag表示进行分析得出的二进制标识
int flag;
// 服务器执行该命令的次数
long long calls;
// 服务器执行该命令的总时长
long long milliseconds;
} clientCommand;
执行预备操作
为保证命令可以正确运行,服务器需要执行以下操作
- 检查客户端状态的cmd指针是否为空,如果是,则表示用户输入的命令名称找不到相应的实现
- 根据
arity
属性检查参数个数是否正确 - 客户端是否通过了验证,若为通过验证则先客户端返回一个错误信息
- 如果服务器打开了
maxmemory
功能,那么在执行命令之前先检查服务器的内存占用情况,并在有需要时进行内存回收从而使得接下来的命令能够顺利执行 - 如果服务器上一次执行
BGSAVE
命令时出错,并且服务器打开了stop-write-on-bgsave-error
功能,且该命令为写命令,那么服务器拒绝执行该命令并返回一个错误信息 - 如果客户端正在用
SUBSCRIBE
命令订阅频道,或者正在用PSUBSCRIBE
订阅模式,那么服务器只会执行客户端发来的SUBSCRIBE
,PSUBSCRIBE
,UNSUBSCRIBE
,UNPSUBSCRIBE
命令,其他命令都会被拒绝 - 如果服务器正在执行数据载入,那么客户端发送的命令必须带有
l
表示才会被执行 - 如果服务器因为执行lua脚本而超时并进入阻塞状态没把呢服务器只会执行
SHUTDOWN nosave
和SCRIPT KILL
命令 - 如果服务器打开了监视器功能,那么服务器会将要执行的命令和参数等信息发送给监视器
调用命令的实现函数
服务器调用客户端状态cmd
属性中对应的函数执行操作
执行后续工作
在执行完实现函数之后,服务器执行以下后续工作:
- 如果服务器开启了慢查询日志功能,那么慢查询日志模块会检查是否需要为刚刚执行完的命令请求添加一条新的慢查询日志
- 根据刚刚执行命令所耗费的市场,更新
redisCommand
中的milliseconds
属性 - 如果服务器开启了AOF持久化,那么会将该命令写入到AOF缓冲区中
- 如果有其他从服务器正在复制该服务器,那么会将该命令发送给所有从服务器
serverCron函数
Redis服务器中serverCron
函数默认每个100ms执行一次,将会执行以下操作:
- 更新服务器时间缓存
- 更新LRU时钟
- 更新服务器每秒执行命令次数
- 更新服务器内存峰值记录
- 处理SIGTERM型号
- 管理客户端资源
- 管理数据库紫云啊
- 执行被延迟的
BGREWERITEAOF
- 检查持久化操作的运行状态
- 将AOF缓冲区中的内容写入AOF文件
- 关闭异步客户端
- 增加
cronloops
计数器的值,该属性的唯一作用为“每执行N次就执行一次指定代码”
初始化服务器
Redis服务器启动时执行以下操作:
- 初始化服务器状态结构
- 设置服务器运行id
- 设置服务器的默认运行频率
- 设置服务器的默认配置文件路径
- 设置服务器的运行架构
- 设置服务器的默认端口号
- 设置服务器的默认RDB持久化条件和AOF持久化条件
- 初始化服务器LRU时钟
- 创建命令表
- 载入配置选项
- 初始化服务器数据结构
server.clients
链表server.db
数组server.pubsub_patterns
链表,保存有频道订阅信息server.lua
用于执行lua脚本的lua环境- 用于保存慢查询日志的
server.slowlog
属性
- 还原数据库状态
- 如果开启了AOF持久化则使用AOF文件还原数据库状态
- 如果为开启AOF持久化则使用RDB文件还原数据库状态