Redis设计与实现-笔记(二)

文章目录

第二部分 单机数据库的实现

第9章 数据库

9.1 服务器中的数据库

Redis服务器将所有数据库都保存在服务器状态redis.h/redisServer结构的db数据中,db数组的每个项都是一个redis.h/redisDb结构,每个redisDb结构代表一个数据库:

struct redisServer {
	// ...
	// 一个数组,保存着服务器中的所有数据库
	redisDb *db;
	// 服务器的数据库数量
	int dbnum;
	// ...
}

在初始化服务器时,程序会更加服务器状态的dbnum属性来决定创建多少个数据库,dbnum是由服务器配置的database选项决定,默认为16,所以Redis服务器默认会创建16个数据库。

9.2 切换数据库

每个Redis客户端都有自己的目标数据库。默认情况下,Redis客户端的目标数据库为0号数据库,但客户端可以通过执行SELECT命令来切换目标数据库。
通过修改redisClient.db指针,让它指向不同的数据库,从而实现切换。

9.3 数据库键空间

Redis是一个键值对(key-value pair)数据库服务器,服务器中的每个数据库都由一个redis.h/redisDb结构表示,其中redisDb结构的dict字典保存了数据库中的所有键值对,我们将这个字典称为键空间(key space):

typedef struct redisDb {
	// ...
	// 数据库键空间,保存着数据库中的所有键值对
	dict *dict;
	// ...
}redisDb;
9.3.1 添加新键

实际上就是将一个新键值对添加到键空间字典里面,其中键为字符串对象,而值为任意一种类型的Redis对象。

9.3.2 删除键

实际上就是在键空间里面删除键所对应的键值对对象。

9.3.3 更新键

实际上就是对键空间里面键所对应的值对象进行更新,根据值对象的类型不同,更新的具体方法也会有所不同。

9.3.4 对键取值

实际上就是在键空间中取出键所对应的值对象,根据值对象的类型不同,具体的取值方法也会有所不同。

9.3.5 其他键空间操作

FLUSHDB命令,清空整个数据库。就是通过删除键空间中的所有键值对来实现的。
RANDOMKEY命令,随机返回数据库中某个键,就是通过在键空间中随机返回一个键来实现的。
DBSIZE命令,返回数据库键数量,就是通过返回键空间中包含的键值对的数量来实现的。
EXISTS、RENAME、KEYS命令等,都是通过对键空间进行操作来实现的。

9.3.6 读写键空间时的维护操作

维护操作包括:

  • 在读取一个键之后,服务器会根据键是否存在来更新服务器的键空间命中(hit)次数或键空间不命中(miss)次数,这两个值可以再INFO stats命令的keyspace_hits属性和keyspace_misses属性中查看。
  • 在读取一个键之后,服务器会更新键的LRU(最后一次使用)时间,这个值可以用于计算键的闲置时间,使用OBJECT idletime 命令可以查看键key的闲置时间。
  • 如果服务器在读取一个键时发现该键已经过期,那么服务器会先删除这个过期键,然后才执行余下的其他操作。
  • 如果有客户端使用WATCH命令监视了某个键,那么服务器在对被监视的键进行修改之后,会将这个键标记为脏(dirty),从而让事务程序注意到这个键已经被修改过。
  • 服务器每次修改一个键之后,都会对脏键计数器的值增1,这个计数器会触发服务器的持久化以及复制操作。
  • 如果服务器开启了数据库通知功能,那么在对键进行修改之后,服务器将按配置发送相应的数据库通知。
9.4 设置键的生存时间或过期时间

通过EXPIRE命令或者PEXPIRE命令,客户端可以以秒或者毫秒精度为数据库中的某个键设置生存时间,在经过指定的秒数或者毫秒数之后,服务器就会自动删除生存时间为0的键。
客户端可以通过EXPIREAT命令或PEXPIREAT命令,以秒或者毫秒精度给数据库中的某个键设置过期时间。
过期时间是一个UNIX时间戳当键的过期时间来临时,服务器就会自动从数据库中删除这个键。
TTL命令和PTTL命令接受一个带有生存时间或者过期时间的键,返回这个键的剩余生存时间,也就是返回距离这个键被服务器自动删除还有多长时间。

9.4.1 设置过期时间

Redis有4个不同的命令可以用于设置键的生存时间。

  • EXPIRE,秒
  • PEXPIRE,秒
  • EXPIREAT,timestamp所指定的秒数时间戳
  • PEXPIREAT,timestamp所指定的毫秒数时间戳

实际上EXPIRE、PEXPIRE、EXPIREAT三个命令都是使用PEXPIREAT命令来实现的。

9.4.2 保存过期时间

redisDb结构的expires字典保存了数据库所有键的过期时间,我们称这个字典为过期字典。

9.4.3 移除过期时间

PERSIST命令可以移除一个键的过期时间。

9.4.4 计算并返回剩余生存时间

TTL命令以秒为单位返回键的剩余生存时间,而PTTL命令 则以毫秒为单位返回键的生存剩余生存时间。
它们都是通过计算键的过期时间和当前时间之间的差来实现的。

9.4.5 过期键的判定

步骤:

  1. 检查给定键是否存在于过期字典:如果存在,那么取得键的过期时间
  2. 检查当前UNIX时间戳是否大于键的过期时间:如果是的话,那么键已经过期;否则,键未过期。
9.5 过期键删除策略

过期了,那么它什么时候会被删除?

9.5.1 定时删除
9.5.2 惰性删除
9.5.3 定期删除

问题:

  • 定时删除,对内存友好,但占用太多CPU时间,影响服务器的响应时间和吞吐量。
  • 惰性删除,对CPU友好,但浪费太多内存,有内存泄露的危险。
  • 定期删除,每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。难点是确定删除操作执行的时长和频率。
9.6 Redis的过期键删除策略

Redis服务器实际使用的是惰性删除和定期删除两种策略;通过配合使用者两种删除策略,服务器可以很好地合理使用CPU时间和避免浪费内存空间之间取得平衡。

9.6.1 惰性删除策略的实现

过期键的惰性删除策略由db.c/expireIfNeeded函数实现,
所有读写数据库的Redis命令在执行之前都会调用expireIfNeeded函数对输入键进行检查:

  • 如果输入键已经过期,那么expireIfNeeded函数将输入键从数据库中删除
  • 如果输入键未过期,那么expireIfNeeded函数不做动作
    每个命令的实现函数都必须能同时处理键存在以及键不存在这两种情况:
  • 当键存在时,命令按照键存在的情况执行
  • 当键不存在或者键因为过期而被expireIfNeeded函数删除时,命令按照键不存在的情况执行。
9.6.2 定期删除策略的实现

过期键的定期删除策略由redis.c/activeExpireCycle函数实现,每当Redis的服务器周期性操作redis.c/serverCron函数执行时,activeExpireCycle函数就会被调用,它在规定的时间内,分多次遍历服务器中的各个数据库,从数据库的expires字典中随机检查一部分键的过期时间,并删除其中的过期键。
activeExpireCycle函数的工作模式可以总结如下:

  • 函数每次运行时,都从一定数量的数据库中取出一定数量的随机键进行检查,并删除其中的过期键
  • 全局变量current_db会记录当前activeExpireCycle函数检查的进度,并在下一次activeExpireCycle函数调用时,接着上一次的进度进行处理。
  • 随着activeExpireCycle函数的不断执行,服务器中的所有数据库都会被检查一遍,这时函数将current_db变量重置为0,然后再次开始新一轮的检查工作。
9.7 AOF、RDB和复制功能对过期键的处理

要点:
RDB持久化功能
AOF持久化功能
复制功能是如何处理数据库中的过期键的。

9.7.1 生成RDB文件

在执行SAVE命令或者BGSAVE命令创建一个新的RDB文件时,程序会对数据库中的键进行检查,已过期的键不会被保存到新创建的RDB文件中。
数据库中包含过期键不会对生成新的RDB文件造成影响。

9.7.2 载入RDB文件

启动Redis服务器时,如果服务器开启RDB功能,那么服务器将对RDB文件进行载入:

  • 如果服务器以主服务器模式运行,那么在载入RDB文件时,程序会对文件中保存的键进行检查
  • 如果服务器以从服务器模式运行,那么在载入RDB文件时,文件中保存的所有键,不论是否过期,都会被载入到数据库中。不过,因为主从服务器在进行数据同步的时候,从服务器的数据库就会被清空,所以一般来讲,过期键对载入RDB文件的从服务器也不会造成影响。
9.7.3 AOF文件写入

当服务器以AOF持久化模式运行时,如果数据库中的某个键已经过期,但它还没有被惰性删除或者定期删除,那么AOF文件不会因为这个过期键而产生任何影响。
当过期键被惰性删除或者定期删除之后,程序会向AOF文件追加(append)一条DEL命令,来显示地记录该键已被删除。
例,如果客户端使用GET message命令,执行3个动作:

  1. 从数据库中删除message键
  2. 追加一条DEL message命令到AOF文件
  3. 向执行GET命令的客户端返回空回复。
9.7.4 AOF重写

和生成RDB文件时类似。

9.7.5 复制

当服务器运行在复制模式下时,从服务器的过期键删除动作由主服务器控制:

  • 主服务器在删除一个过期键之后,会显式地想所有从服务器发送一个DEL命令,告知从服务器删除这个过期键
  • 从服务器在执行客户端发送的读命令时,即使碰到过期键也不会将过期键删除,而是继续想处理未过期的键一样来处理过期键
  • 从服务器只有在街道主服务器发来的DEL命令之后,才会删除过期键
9.8 数据库通知

这个功能可以让客户端通过订阅给定的频道或者模式,来货值数据库中键的变化,以及数据库中命令的执行情况。
服务器配置notify-keyspace-events选项决定了服务器所发送通知的类型:

  • 所有类型的键空间通知和键事件通知,设置为AKE
  • 所有类型的键空间通知,设置为AK
  • 所有类型的键空间事件,设置为AE
  • 只发送和字符串键有关的键空间通知,设置为K$。
  • 只发送和字列表键有关的键事件通知,设置为E1。
9.8.1 发送通知

由notify.c/notifyKeyspaceEvent函数实现

9.8.2 发送通知的实现
  1. server.notify_keyspace_events属性就是服务器配置notify-keyspace-events选项所设置的值,如果给定的通知类型type不是服务器允许发送的通知类型,那么函数会直接返回,不做任何动作。
  2. 如果给定的通知是服务器允许发送的通知,那么下一步函数会检测服务器是否允许发送键空间通知,如果允许的话,程序就会构建并发送事件通知。
  3. 最后,函数检测服务器是否允许发送键事件通知,如果允许的话,程序就会构建并发送事件通知。
  4. 另外,pubsubPublishMessage函数是PUBLISH命令的实现函数,执行这个函数等同于执行PUBLISH命令,订阅数据库通知的客户端收到的信息就是由这个函数发出的。
9.9 重点回顾

第10章 RDB持久化

RDB持久化既可以手动,也可以根据服务器配置选项定期执行,该功能可以将某个时间点上的数据库状态保存到一个RDB文件中,RDB文件是一个经过压缩的二进制文件,可还原生成RDB文件时的数据库状态。

10.1 RDB文件的创建与载入

两个命令可以用于生成,一个SAVE,另一个是BGSAVE。
SAVE命令会阻塞Redis服务器进程,知道RDB文件创建完毕为止,在阻塞期间,服务器不能处理任何命令请求。
BGSAVE命令会派生出一个子进程,然后由子进程负责创建RDB文件,服务器进程(父进程)继续处理命令请求。
创建RDB文件的实际工作由rdb.c/rdbSave函数完成。
RDB文件的载入工作是在服务器启动时自动执行的。所以Redis并没有专门用于载入RDB文件的命令,只要检测到RDB文件存在,它就会自动载入RDB文件。
因为AOF文件的更新频率通常比RDB文件的更新频率高,所以:

  • 如果服务器开启了AOF持久化功能,那么服务器会优先适用AOF文件来还原数据库状态
  • 只有在AOF持久化功能处于关闭状态时,服务器才会适用RDB文件来还原数据库状态。
10.1.1 SAVE 命令执行时的服务器状态

当执行SAVE命令执行时,Redis服务器会被阻塞,客户端发送的所有命令请求都会被拒绝。
只有在服务器执行完SAVE命令、重新开始接受命令请求之后,客户端发送的命令才会被处理。

10.1.2 BGSAVE命令执行时的服务器状态

Redis服务器仍然可以继续处理客户端的命令请求,但是在BGSAVE命令执行期间,服务器处理SAVE、BGSAVE、BGREWRITEAOF三个命令的方式会和平时有所不同。
首先,客户端发送的SAVE命令会被服务器拒绝
其次,客户端发送的BGSAVE命令会被服务器拒接。
最后,BGREWRITEAOF和BGSAVE两个命令不能同时执行。BGSAVE执行时,BGREWITEAOF命令会被延迟到BG命令执行结束;BGREWITEAOF命令执行时,BGSAVE会被拒绝。只是为了性能方面的考虑

10.1.3 RDB文件载入时的服务器状态

在载入RDB文件期间,会一直处于阻塞状态,直到载入工作完成为止。

10.2 自动间隔性保存

Redis允许用户通过设置服务器配置的save选项,让服务器每隔一段时间自动执行一次BGSAVE命令。
配置及其执行条件:

// 服务器在900秒之内,对数据库进行了至少1次修改
save 900 1
// 服务器在300秒之内,对数据库进行了至少10次修改
save 300 10
服务器在60秒之内,对数据库进行了至少10000次修改
save 60 10000
10.2.1 设置保存条件

如果用户没有主动设置save选项,那么服务器会为save选项设置默认条件:如上。
redisServer结构的saveparams属性:

struct redisServer {
	//...
	// 记录了保存条件的数组
	struct saveparam *saveparams;
	// ...
	// 修改计数器
	long long dirty;
	// 上一次执行保存的时间
	time_t lastsave;
	// 数据库键空间,保存着数据库中的所有键值对
	dict *dict;
	// ...
}
// 每个saveparam结构都保存了一个save选项设置的保存条件:
struct saveparam {
	// 秒数
	time_t seconds;
	// 修改数
	int changes;
}
10.2.2 dirty计数器和lastsave属性

服务器状态还维护一个dirty计数器,以及一个lastsave属性如上。

10.2.3 检查保存条件是否满足

Redis的服务器周期性操作函数serverCron默认每隔100毫秒就会执行一次,该函数用于对正在运行的服务器进行维护,其中一项工作就是检查save选项所设置的保存条件是否已经满足,满足就执行BGSAVE命令。
遍历saveparams数组中的所有保存条件,只要有任意一个条件被满足,那么服务器就会执行BGSAVE命令。

10.3 RDB文件结构

一个完整的RDB文件包含的各个部分。

REDISdb_versiondatabasesEOFcheck_sum

REDIS,这个部分的长度为5字节,保存着“REDIS”五个字符。通过这五个字符,程序可以再载入文件时,快速检测所载入的文件是否RDB文件。
db_version,长度为4字节,值时一个字符串表示的整数,记录了RDB文件的版本号,比如“0006”
databases,包含着零个或者任意多个数据库,以及各个数据库中的键值对数据:

  • 如果服务器的数据库状态为空(所有数据库都是空的),那么这个部分也为空,长度为0字节
  • 如果服务器的数据库状态为非空(有至少一个数据库非空),那么这个部分也为非空,根据数据库所保存键值对的数量、类型和内容不同,长度也会有所不同。
    EOF,常量的长度为1字节,这个常量标志着RDB文件正文内容的结束,当读入程序遇到这个值的时候,它知道所有数据库的所有键值对都已经载入完毕了。
    check_num,是一个8字节长的无符号整数,保存着一个检验和,这个校验和是程序通过对REDIS、db_version、databases、EOF四个部分的内容进行计算得出的。数据库在载入RDB文件时,会将载入数据所计算出的检验和与check_sum所记录的校验和进行对比,一次来检查RDB文件是否有出错或者损坏的情况出现。
10.3.1 databases部分

一个RDB文件的databases部分可以保存任意多个非空数据库。
每个非空数据库在RDB文件中都可以保存为SELECTDB、db_number、key_value_pairs三个部分。
SELECTDB,常量,长度为1字节,当读入程序遇到这个值的时候,它知道接下来要读入的将是一个数据库号码。

db_number,保存着一个数据库号码。根据号码大小不同,长度可以是1字节、2字节或者5字节。当读入db_number部分后,会调用SELECT命令,根据读入的数据库号码进行数据库切换,使用之后读入的键值对可以载入到正确的数据库中。

key_value_pairs,保存了数据库中的所有键值对数据,如果键值对带有过期时间,那么过期市价也会和键值对保存在一起。根据是否有过期时间,长度也会有所不同

10.3.2 key_value_pairs部分

每个key_value_pairs都保存了一个或以上数量键值对,如果带有过期时间,过期时间也会保存在内。
不带过期时间的键值对组成:TYPE、key、value
带过期时间的键值对组成:EXPIRETIME_MS、ms、TYPE、key、value

10.3.3 value的编码

RDB文件中的每个value部分都保存了一个值对象,每个值对象的类型都由与之对应的TYPE记录,根据类型的不同,value部分的结构,长度也会有所不同。
各种REDIS_ENCODING_*编码:
1、字符串对象
2、列表对象
3、集合对象
4、哈希表对象
5、有序集合对象
6、INSERT编码的集合
7、ZIPLIST编码的列表、哈希表或者有序集合

10.4 分析RDB文件

使用od命令来分析Redis服务器产生的RDB文件,该命令可以用给定的格式转存并打印输入文件。
-c参数,可以以ASCII编码打印输入文件
-x参数,可以以16进制的方式打印输入文件。
。。。

10.4.1 不包含任何键值对的RDB文件

创建一个数据库状态为空的RDB文件

FLUSHALL
SAVE
od -c dump.rdb
10.4.2 包含字符串键的RDB文件
10.4.3 包含带有过期时间的字符串键的RDB文件
10.4.4 包含一个集合键的RDB文件
10.4.5 关于分析RDB文件的说明

可同时使用-cx参数调用od命令,打印RDB文件,进行人工分析

10.5 重点回顾
  • RDB文件用于保存和还原Redis服务器所有数据库中的所有键值对数据
  • SAVE命令由服务器进程直接执行保存操作,所以该命令会阻塞服务器
  • BGSAVE命令由子进程执行保存操作,所以该命令不会阻塞服务器
  • 服务器状态中会保存所有用save选项设置的保存条件,当人以一个保存条件被满足时,服务器会自动执行BGSAVE命令
  • RDB文件是一个经过压缩的二进制文件,由多个部分组成。
  • 对于不同类型的键值对,RDB文件会使用不同的方式来保存它们。

第11章 AOF持久化

AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态的。

11.1 AOF持久化的实现

该功能的实现可以分为命令追加(append)、文件写入、文件同步(sync)三个步骤

11.1.1 命令追加

命令会追加到服务器状态redisServer的aof_buf缓冲区的末尾

11.1.2 AOF文件的写入与同步

Redis的服务器进程就是一个事件循环(loop),这个循环中的文件事件负责接收客户端的命令请求,以及向客户端发送命令回复,而时间事件则负责执行像serverCron函数这样需要定时运行的函数。

服务器每次结束一个事件循环之前,它都会调用flushAppendOnlyFile函数,考虑是否需要将aof_buf缓冲区中的内容写入和保存到AOF文件里面。

flushAppendOnlyFile函数的行为由服务器配置的appendfsync选项的值来决定:always、everysec、no。
默认值为everysec。

如果是everysec,会先将aof_buf中的内容写入到AOF文件中,然后再对AOF文件进行同步。

11.2 AOF文件的载入与数据还原

服务器只要读入并重新执行一遍AOF文件并还原里面保存的写命令,就可以还原服务器关闭之前的数据库状态。
Redis读取AOF文件并还原数据库状态的详细步骤:

  1. 创建一个不带网络连接的伪客户端(fake client):因为Redis的命令只能再客户端上下文中执行,而载入AOF文件时所使用的命令直接来源于AOF文件而不是网络连接,所以服务器使用了一个没有网络连接的客户端执行命令的效果完全一样。
  2. 从AOF文件中分析并读取出一条写命令。
  3. 使用伪客户端执行被读出的写命令。
  4. 一直执行步骤2和步骤3,知道AOF文件中的所有写命令都被处理完毕为止。
11.3 AOF重写

随着服务器运行时间的流逝,AOF文件的内容会越来越多,还原所需要的时间就越多。
Redis提供了AOF文件重写功能。Redis服务器可以创建一个新的AOF文件来替代现有的AOF文件。新的AOF文件,没有多余的语句。

11.3.1 AOF文件重写的实现

可以用一条命令代替保存AOF文件中的多条命令。首先从数据库中读取值现在的值,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令,这就是AOF重写功能的实现原理。
如果元素的数量超过了redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD常量的值,那么重写程序将使用多条命令来记录键的值,而不单单使用一条命令。
目前版本中该值为64,也就是说,如果一个集合键包含了超过64个元素,那么重写程序会用多余SADD命令来记录这个集合,并且每条命令设置的元素数量也为64个。

11.3.2 AOF后台重写

由于大量的写入操作,调用这个函数线程被长时间阻塞,所以,Redis决定将AOF重写程序放到子进程里执行,这样做可以同时达到两个目的:

  • 子进程进行AOF重写期间,服务器进程(父进程)可以继续处理命令请求。
  • 子进程带有服务器进程的数据副本,使用子进程而不是线程,可以再避免使用锁的情况下,保证数据的安全性。

问题:接收的新的命令可能会对现有的数据库状态进行修改,从而使得服务器当前的数据库状态与AOF文件所保存的数据库状态不一致。
为了解决这个问题:Redis服务器设置了一个AOF重写缓冲区这个缓冲区在服务器创建子进程之后开始使用,当Redis服务器执行完一个写命令之后,它会同时将这个写命令发送AOF缓冲区和AOF重写缓冲区。
在子进程执行AOF重写期间,服务器进程需要执行以下三个工作:

  1. 执行客户端发来的命令
  2. 将执行后的写命令追加到AOF缓冲区
  3. 将执行后的写命令追加到AOF重写缓冲区

保证了:

  • AOF缓冲区的内容会定期被写入和同步到AOF文件,对先有AOF文件的处理工作会如常进行。
  • 从创建子进程开始,服务器执行的所有写命令都会被记录到AOF重写缓冲区里面。

当子进程完成AOF重写工作之后,它会想父进程发送一个信号,父进程接收后,调用一个信号处理函数,并执行:
1)将AOF重写缓冲区中的所有内容写入到新AOF文件中,这时新AOF文件所保存的数据库状态将和服务器当前的数据库状态一致。
2)对新的AOF文件进行改名,原子地覆盖现有的AOF文件,完成新旧两个AOF文件的替换

这个信号函数处理完之后,父进程就可以继续接受命令请求了。
AOF后台重写,也即是BGREWRITEAOF命令的实现原理。

11.4 重点回顾
  • AOF文件通过保存所有修改数据库的写命令请求来记录服务器的数据库状态
  • AOF文件中的所有命令都以Redis命令请求协议的格式保存
  • 命令请求会先保存到AOF缓冲区里面,之后再定期写入并同步到AOF文件
  • appendfsync选项的不同值对AOF持久化功能的安全性以及Redis服务器的性能有很大的影响。
  • 服务器只要载入并重新执行保存在AOF文件中的命令,就可以还原数据库本来的状态
  • AOF重写可以产生一个新的AOF文件,这个新的AOF文件和原有的AOF文件索保存的数据库状态一样,但体积更小
  • AOF重写是一个由歧义的名字,该功能是通过读取数据库中的键值对来实现的,程序无须对现有AOF文件进行任何读分析或者写入操作。
  • 在执行BGREWRITEAOF命令时,Redis服务器会维护一个AOF重写缓冲区,该缓冲区会在子进程创建新AOF文件期间,记录服务器执行的所有写命令。当子进程完成创建AOF文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新AOF文件的末尾,使得新旧两个AOF文件所保存的数据库状态一致。最后,服务器用新的AOF文件替换旧的AOF文件,以此来完成AOF文件重写操作。

第12章 事件

Redis服务器是一个事件驱动程序,服务器需要处理一下两类事件:

  • 文件事件(file event):Redis服务器通过套接字与客户端(或者其他Redis服务器)进行连接,而文件事件就是服务器对套接字操作的抽象。服务器与客户端(或者其他服务器)的通信会产生相应的文件事件,而服务器则通过监听并处理这些事件来完成一系列网络通信操作。
  • 时间事件(time event):Redis服务器中的一些操作(比如serverCron函数)需要在给定的时间点执行,而时间事件就是服务器对这类定时操作的抽象。
12.1 文件事件

Redis基于Reactor模式开发了自己的网络事件处理器:这个处理器被称为文件事件处理器:

  • 文件事件处理器使用I/O多路复用(multiplexing)程序来同时监听多个套接字,并根据套接字目前执行的任务为套接字关联不同事件处理器。
  • 当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时,与操作想对应的文件事件就会产生,这时文件事件处理器就会调用套接字前关联好的事件处理器来处理这些事件。
12.1.1 文件事件处理器的构成

四个部分:

  1. 套接字
  2. I/O多路复用程序
  3. 文件事件分派器
  4. 事件处理器

尽管多个文件事件可能会并发地出现,但I/O多路复用程序总是会将所有产生事件的套接字都放到一个队列里面,然后通过这个队列,以有序(sequentially)、同步(synchronously)、每次一个套接字的方式向文件事件分派器传送套接字。当上一个套接字产生的事件被处理完毕之后,I/O多路复用程序才会继续向文件事件分派器传送下一个套接字。
文件事件分派器接收I/O多路复用程序传来的套接字,并根据套接字产生的事件的类型,调用相应的事件处理器。

12.1.2 I/O多路复用程序的实现

Redis的I/O多路复用程序的所有功能都是通过包装常见的select、epoll、evport和kqueue这些I/O多路复用函数库来实现的。

12.1.3 事件的类型

I/O多路复用程序可以监听多个套接字的ae.h/AE_READABLE事件和ae.h/AE_WRITABLE事件,和套接字操作之间的对应关系:

  • 当套接字变得可读时或者有新的可应答套接字出现时,套接字产生AE_READABLE事件。
  • 当套接字变得可写时,套接字产生AE_WRITABLE事件。

I/O多路复用程序允许服务器同时监听套接字的AE_READABLE事件和AE_WRITABLE事件,如果一个套接字同时产生了这两种事件,分派器有限处理AE_READABLE事件,然后再处理AE_WRITABLE事件。

12.1.4 API

ae.c/aeCreateFileEvent函数,接受一个套接字描述符、一个事件类型,以及一个事件处理器作为参数,将给定套接字的给定事件加入到I/O多路复用程序的监听范围之内,并对事件和事件处理器进行关联。
ae.c/aeWait函数,
ae.c/aeApiPoll函数,
ae.c/aeProcessEvents函数,
ae.c/aeGetApiName函数,

12.1.5 文件事件的处理器

Redis为文件事件编写了多个处理器。

  1. 连接应答处理器
  2. 命令请求处理器
  3. 命令回复处理器
  4. 复制处理器

一次完整的客户端与服务器连接事件示例:
(1)假如一个Redis服务器正在运作,那么监听套接字的AE_READABLE时间应该正处于监听状态之下,而该事件所对应的处理器为连接应答处理器
(2)如果这时一个Redis客户端相服务器发起连接,那么监听套接字将产生AE_READABLE事件,触发连接应答处理器执行。处理器会对客户端的连接请求进行应答,然后创建客户端套接字,以及客户端状态,并将客户端套接字的AE_READABLE事件与命令请求处理器进行关联,是的客户端可以向服务器发送命令请求。
(3)假设客户端向主服务器发送一个命令请求,那么客户端套接字将产生AE_READABLE事件,引发命令请求处理器执行,处理器读取客户端的命令内容,然后传给相关程序去执行。
(4)将产生相应的命令回复,服务器会将客户端套接字的AE_WRITABLE事件与命令回复处理器进行关联。当客户端尝试读取命令回复的时候,客户端套接字将产生AE_WRITABLE事件,触发命令回复处理器执行,当命令回复处理器将命令回复全部写入到套接字之后,服务器就会解除客户端套接字的AE_WRITABLE事件与命令回复处理器之间的关联。

12.2 时间事件

Redis的时间事件分为两类:

  • 定时事件:
  • 周期性事件:
    一个时间事件主要由三个属性组成:
  • id:全局唯一ID标识号,顺序递增。
  • when:毫秒精度UNIX时间戳,记录了时间事件的到达时间
  • timeProc:时间事件处理器。当时间事件到达时,服务器就会调用相应的处理器来处理时间。
    定时事件、周期性事件,取决于时间处理器的返回值:
  • 返回ae.h/E_NOMORE,为定时事件:达到一次之后就会被删除,之后不再到达。
  • 返回一个非AE_NOMORE的整数值,那么这个时间为周期性事件:当一个事件事件到达后,服务器会根据事件处理器返回的值,对时间事件的when属性进行更新,让这个事件在一段时间之后再次到达,并以这种方式一直更新并运行下去。

目前版本的Redis只使用周期性事件,而没有使用定时事件。

12.2.1 实现

服务器将所有时间事件都放在一个无序链表中,每当时间事件执行权运行时,它就遍历整个链表,查找所有已到达的时间事件,并调用相应的事件处理器。

新的时间事件总是插入到链表的表头,按ID逆序排序。无序是指,该链表不按when属性的大小排序。(一般只使用serverCron一个时间事件,在benchmark模式下,服务器也只使用两个时间事件,不会影响性能)

12.2.2 API

ae.c/aeCreateTimeEvent函数接受一个毫秒millisecondes一个事件事件处理器proc作为参数,将一个新的时间事件添加到服务器,这个新的时间事件将在当前时间的milliseconds毫秒之后到达,而事件的处理器为proc。

12.2.3 时间事件应用实例:serverCron函数

持续运行的Redis服务器需要定期对自身的资源和状态进行检查和调整,从而确保服务器可以长期、稳定地运行,这些定期操作由redis.c/serverCron函数负责执行,主要工作:

  • 更新服务器的各类统计信息,比如时间、内存占用、数据库占用情况等。
  • 清理数据库中的过期键值对。
  • 关闭和清理连接失效的客户端。
  • 尝试进行AOF或RDB持久化操作。
  • 如果服务器是主服务器,那么对从服务器进行定期同步。
  • 如果处于集群模式,对集群进行定期同步和连接测试。

Redis服务器以周期性事件的方式来运行serverCron函数,在服务器运行期间,每隔一段时间,serverCron就会执行一次,直到服务器关闭为止。
2.6版本,服务器默认规定serverCron每秒运行10次,2.8版本开始,用户可以修改hz选项来调整serverCron的每秒执行次数。

12.3 事件的调度与执行

服务器必须对这两种事件进行调度,
何时
花多少时间?
将aeProcessEvents函数置于一个循环里面,加上初始化和清理函数,这就构成了Redis服务器的主函数。
以下是事件的调度和执行规则:

  1. aeApiPoll函数的最大阻塞时间由到达时间最接近当前时间的时间事件决定,这个方法既可以避免服务器对时间事件进行频繁的轮询,也可以确保aeApiPoll函数不会阻塞过长时间。
  2. 因为文件事件是随机出现的,如果等待并处理完一次文件事件之后,仍未有任何时间事件到达,那么服务器将再次等待并处理文件事件。随着文件事件的不断执行,时间会逐渐向时间事件所设置的到达时间逼近,并最终来到到达时间,这时服务器就可以开始处理到达的时间事件了
  3. 对文件事件和时间事件的处理都是同步、有序、原子地执行的,服务器不会中途中断事件处理,也不会对事件进行抢占,因此,不管是文件事件的处理器,还是时间事件的处理器,它们都会尽可能地减少程序的阻塞时间,并在有需要时主动让出执行权,从而降低造成时间饥饿的可能性。
  4. 因为时间事件在文件事件之后执行,并且事件之间不会出现抢占,所以时间事件的实际处理时间,通常会比时间事件设定的到达时间稍晚一些。
12.4 重点回顾
  • Redis服务器是一个事件驱动程序,服务器处理的事件分为文件事件和时间事件两类。
  • 文件事件处理器是基于Reactor模式实现的网络通信程序
  • 文件事件是对套接字操作的抽象:每次套接字变为可应答(acceptable)、可写(writable)或者可读(readable)时,相应的文件事件就会产生。
  • 文件事件分为AE_READABLE时间(读事件)和AE_WRITABLE事件(写事件)两类。
  • 时间事件分为定时事件和周期性事件:定时事件只在指定的时间到达一次,而周期性事件则每隔一段时间到达一次
  • 服务器在一般情况下只执行serverCron函数一个时间事件,并且这个事件是周期性事件。
  • 文件事件和时间事件之间是合作关系,服务器会轮流处理这两种事件,并且处理事件的过程中也不会进行抢占
  • 时间事件的实际处理时间通常会比设定的到达时间稍晚一些。

第13章 客户端

Redis服务器是典型的一对多服务器程序。
通过使用由I/O多路复用技术实现的文件事件处理器,Redis服务器使用单线程进程的方式来处理命令请求,并于多个客户端进行网络通信。
对于每个与服务器进行连接的客户端,服务器都为这些客户端建立了相应的redis.h/redisClient结构(客户端状态),这个状态保存了客户端当前的状态信息,以及执行相关功能时需要用到的数据结构,包括:

  • 客户端的套接字描述符
  • 客户端的名字
  • 客户端的标志值(flag)
  • 指向客户端正在使用的数据库的指针,以及该数据库的号码
  • 客户端当前要执行的命令、命令的参数、命令参数的个数,以及指向命令实现函数的指针。
  • 客户端的输入缓冲区和输出缓冲区
  • 客户端的复制状态信息,以及进行复制所需的数据结构
  • 客户端执行BRPOP、BLPOP等列表阻塞命令时使用的数据结构
  • 客户端的事务状态,以及执行WATCH命令时用到的数据结构
  • 客户端执行发布与订阅功能时用到的数据结构
  • 客户端的身份验证标志
  • 客户端的创建时间,客户端和服务器最后一次通信的时间,以及客户端的输出的缓冲区大小超出软性限制(soft limit)的时间

Redis服务器状态结构的clients属性是一个链表,
链表中保存了

  • 所有与服务器连接的状态结构
  • 对客户端执行批量操作
  • 查找某个指定的客户端
struct redisServer {
	// ...
	// 一个链表,保存了所有客户端状态
	list *clients;
	// 记录了保存条件的数组
	struct saveparam *saveparams;
	
	// 修改计数器
	long long dirty;
	// 上一次执行保存的时间
	time_t lastsave;
	
	// 数据库键空间,保存着数据库中的所有键值对
	dict *dict;
	// ...
}
13.1 客户端属性

可以分为两类:

  • 一类是比价通用的属性,这些属性很少与特定功能相关,无论客户端执行的是什么工作,它们都要用到这些属性。
  • 另外一类是和特定功能相关的属性,比如操作数据库时需要用到的db属性和dictid属性,执行事务时需要用到的mstate属性,以及执行WATCH命令时需要用到watched_keys属性等等。
typedef struct redisClient {
	// 套接字描述符
	int fd;
	// 名字
	robj *name;
	// 标志
	int flags;
	// 输入缓冲区
	sds querybuf;
	// 命令与命令参数
	robj **argv;
	int argc;
	// 命令的实现函数
	struct redisCommad *cmd;
	// 固定大小缓冲区
	char buf[REDIS_REPLY_CHUNK_BYTES];
	int bufpos;
	// 可变大小缓冲区
	list *reply;
	// 身份验证
	int authenticated;
	// 时间
	time_t ctime;
	time_t lastinteraction;
	time_t obuf_soft_limit_reached_time;
} RedisClient;
// 列出目前所有连接服务器的普通客户端
redis> CLIENT list
// 设置名字
redis> CLIENT setname
13.1.1 套接字描述符

根据类型不同,fd属性的值可以是-1或者是大于-1的证书:

  • 伪客户端(fake client)的fd属性的值为-1:处理的命令请求来源于AOF文件或者Lua脚本,而不是网络,所以这种客户端不需要套接字连接,自然也不需要记录套接字描述符。两个地方用到:(1)用于载入AOF文件并还原数据库状态,(2)用于执行Lua脚本中包含的Redis命令。
  • 普通客户端的fd属性的值为大于-1的整数:普通客户端使用套接字来与服务器进行通信,所以会用fd属性来记录客户端套接字的描述符。
13.1.2 名字

默认情况,一个连接到服务器的客户端是没有名字。
可通过CLIENT setname设置名字,没有设置名字name属性指向NULL,设置名字的,指向一个字符串对象。

13.1.3 标志

客户端的标志属性flags记录了客户端的角色,以及客户端目前所处的状态。
flags属性的值可以是单个标志,也可以是多个标志的二进制。
每个标志使用一个常量表示,一部分标志记录了客户端的角色:

  • 在主从服务器进行复制操作时,主服务器成为从服务器的客户端,而从服务器也会成为主服务器的客户端。REDIS_MASTER标志表示客户端代表的是一个主服务器。REDIS_SLAVE标志表示客户端代表的一个从服务器。
  • REDIS_PER_PSYNC标志表示客户端代表的事一个版本低于2.8的从服务器,主服务器不能使用PSYNC命令与这个从服务器进行同步。这个表示只能再REDIS_SLAVE标志处于打开状态时使用
  • REDIS_LUA_CLIENT标志表示客户端是专门用于处理Lua脚本里面包含的Redis命令的伪客户端。
    另外一部分标志则记录了客户端目前所处的状态:
  • REDIS_MONITOR标志表示客户端正在执行的MONITOR命令
  • REDIS_UNIX_SOCKET标志表示服务器使用UNIX套接字来连接客户端。
  • REDIS_BLOCKED标志表示客户端正在被BRPOP、BLPOP等命令阻塞
  • REDIS_UNBLOCKED标志表示客户端已经从REDIS_BLOCKED标志所表示的阻塞状态中脱离出来,不再阻塞。REDIS_UNBLOCKED标志只能再REDIS_BLOCKING标志已经打开的情况下使用
  • REDIS_MULTI标志表示客户端正在执行事务
  • REDIS_DIRTY_CAS标志表示事务使用WATCH命令监视的数据库键已经被修改,REDIS_DIRTY_EXEC标志表示事务在命令入队时出现了错误,以上两个标志都表示事务的安全性已经被破坏,这两个标志中任意一个被打开,EXEC命令必然会执行失败。只能再打开了REDIS_MULTI标志灯额情况下使用。
  • REDIS_CLOSE_ASAP,客户端的输出缓冲区大小超出了服务器允许的范围,服务器会在下一次执行serverCron函数时关闭这个客户端,以免服务器的稳定性收到这个客户端影响。积存在输出缓冲区中的所有内容会直接被释放,不会返回给客户端。
  • REDIS_CLOSE_AFTER_REPLY,表示有用户对这个客户端执行了CLIENT KILL命令,或者客户端发送给服务器的命令请求中包含了错误的协议内容。服务器会将客户端积存在输出缓存区中的所有内容个发送给客户端,然后关闭客户端。
  • REDIS_ASKING,表示客户端向集群节点(运行在集群模式下的服务器)发送了ASKING命令。
  • REDIS_FORCE_AOF,标志强制服务器将当前执行的命令写入到AOF文件里面,REDIS_FORCE_REPL,强制主服务器将当前的命令赋值给所有从服务器。执行PUBSUB命令会使客户端打开该命令,执行SCRIPT LOAD命令会使客户端打开REDIS_FORCE_AOF标志和REDIS_FORCE_REPL标志。
  • 在主从服务器进行命令传播期间,从服务器需要向主服务器发送REPLICATION ACK命令,在发送这个命令之前,从服务器必须打开主服务器对应的客户端REDIS_MASTER_FORCE_REPLY标志,否则发送操作会被拒绝执行。

都定义在redis.h文件里面。
通常,Redis只会将那些对数据库进行了修改的命令写入到AOF文件,并复制到各个从服务器。如果一个命令没有对数据库进行任何修改,那么它就会被认为是只读命令,不会被写入到AOF文件,也不会被复制到从服务器。

不适用于PUBSUB命令和SCRIPT LOAD命令。

PUBSUB命令,向频道的所有订阅者发送消息这一行为带有副作用接收到信息的所有客户端的状态都会因为这个命令而改变。因此服务器需要使用REDIS_FORCE_AOF标志,强制将这个命令写入AOF文件

SCRIPT LOAD命令,他修改了服务器状态,服务器需要使用REDIS_FORCE_REPL标志,强制将SCRIPT LOAD命令复制给所有从服务器。

13.1.4 输入缓冲区

客户端状态的输入缓冲区用于保存客户端发送的命令请求:
输入缓冲区的大小会根据输入内容动态地缩小或者扩大,但它的最大小打不能超过1GB,负责服务器将关闭这个客户端。

13.1.5 命令与命令参数

argv属性是一个数组,数组中的每个项都是一个字符串对象,其中argv[0]是要执行的命令,而之后的其他项则是传给命令的参数
argc属性则负责记录argv数组的长度。(命令也算是一个参数)

13.1.6 命令的实现函数

当服务器从协议内容中分析并得出argv属性和argc属性的值之后,服务器将根据项argv[0]的值,在命令表中查找命令锁对应的命令实现函数。

命令表,是一个字典,字典的键时一个SDS结构,保存了命令的名字,字典的值时命令所对应的redisCommand结构。

redisCommand结构,这个结构保存了命令的实现函数、命令的标志、命令应该给定的参数个数、命令的总执行次数和总消耗时长等统计信息

服务器就可以使用cmd属性所指向的redisCommand结构,以及argv、argc属性中保存的命令参数信息,调用命令实现函数,执行客户端指定的命令。

针对命令表的查找操作不区分字母的大小写。

13.1.7 输出缓冲区

执行命令所得的命令回复会被保存在客户端状态的输出缓冲区里面,每个客户端都有两个输出缓冲区可用,一个缓冲区的大小是固定的,另一个缓冲区的大小是可变的:

  • 固定大小的缓冲区用于保存那些长度比较小的回复,如ok
  • 可变大小的缓冲区用于保存那些长度比较大的回复,如非常长的字符串值

客户端的固定大小缓冲区由buf和bufpos两个属性组成
buf,是一个大小为REDIS_REPLY_CHUNK_BYTES子节点的字节数组,REDIS_REPLY_CHUNK_BYTES默认值为16*1024,为16KB。
bufpos,记录了buf数组目前已使用的字节数量
可变大小缓冲区由reply链表和一个或多个字符串对象组成。
通过使用链表连接多个字符串对象,服务器可以为客户端保存一个非常长的命令回复,而不必受到固定大小缓冲区16KB大小的限制。

13.1.8 身份验证

客户端状态的authenticated属性用于记录客户端是否通过了身份验证。
为0,那么便是客户端未通过身份验证;为1,那么表示已经通过身份验证。
为0,除了AUTH命令之外,客户端发送的所有其他命令都会被服务器拒绝执行。通过AUTH命令进行身份验证后,变为1。
仅在服务器启用了身份验证功能时使用。
更多信息参考requirepass选项。

13.1.9 时间

ctime,记录了创建客户端的时间,可用于计算连接了多少秒。
lastinteraction,记录了客户端与服务器最后一次进行互动时间,这里的互动时间是双方的。可以用来计算客户端的空转时间。
obuf_soft_limit_reached_time,记录了输出缓冲区第一次到达软性限制的时间。

13.2 客户端的创建与关闭
13.2.1 创建普通客户端

客户端使用connect函数连接到服务器,服务器调用连接事件处理器,为客户端创建一个相应的客户端状态,并将这个新的客户端状态添加到服务器结构clients链表的末尾。

13.2.2 关闭普通客户端

关闭的原因:

  • 如果客户端进行退出或者被杀死,那么客户端和服务器之间的网络连接将被关闭,从而造成服务器被关闭。
  • 如果客户端向服务器发送了带有不符合协议格式的命令请求,那么这个客户端也会被服务器关闭
  • 如果客户端成为了CLIENT KILL命令的目标,那么它也会被关闭
  • 如果用户未服务器设置了timeout配置选项,那么当客户端的空转时间超过timeout选项设置的值时,客户端将被关闭。例外情况:如果客户端是主服务器(打开了REDIS_MASTER标志),从服务器(打开了REDIS_SLAVE标志),正在被BLPOP等命令阻塞(打开了REDIS_BLOCKED标志),或者正在执行SUBSCRIBE、PSUBSCRIBE等订阅命令,那么即时客户端的空转时间超过了timeout选项的值,客户端也不会被服务器关闭。
  • 如果客户端发送的命令请求的大小超过了输入缓冲区的限制大小(默认为1GB),那么这个客户端也会被服务器关闭
  • 如果要发送给客户端的命令回复的大小超过了输出缓冲区的限制大小,那么这个客户端会被服务器关闭。
    服务器会时刻检查客户端的输出缓冲区的大小,并在超出范围时,执行相应的限制操作
    服务器使用两种模式来限制客户端缓冲区的大小:
  • 硬性限制:如果输出缓冲区的大小超过了硬性限制所设置的大小,那么服务器立即关闭客户端。
  • 软性限制:如果输出缓冲区的大小超过了软性限制所设置的大小,但还没超过硬性限制,那么服务器将使用客户端状态结构obuf_soft_limit_reached_time属性记录下客户端到达软性限制的起始时间;之后服务器会继续监视客户端,如果输出缓冲区的狭小一致超过软性限制,并且持续时间超过服务器设定的市场,那么服务器将关闭客户端;相反的,如果输出缓冲区的大小在指定时间之内,不再超出软性限制,那么客户端就不会被关闭,并且obuf_soft_limit_reached_time属性的值也会被清零。

client-output-buffer-limit选项可以为普通客户端、从服务器客户端、执行发布与订阅功能的客户端分别设置不同的软性限制和硬性限制

// 将普通客户端的硬性限制和软性限制都设置为0,表示不限制客户端的输出缓冲区大小
client-output-buffer-limit normal 0 0 0
// 将从服务器客户端的硬性限制设置为256MB, 软性设置为64MB,软性限制的时长为60秒
client-output-buffer-limit slave 256mb 64mb 60
// 将执行发布与订阅功能的客户端的硬性限制设置为32MB,软性限制设置为8MB,软性限制的时长为60秒
client-output-buffer-limit pubsub 32mb 8mb 60

可参考配置文件redis.conf。

13.2.3 Lua脚本的伪客户端

服务器会在初始化时创建负责执行Lua脚本中包含的Redis命令的命令的伪客户端,并将这个伪客户端关联在服务器状态结构的lua_client属性中。

struct redisServer {
	redisClient *lua_client;
}

伪客户端在服务器运行的整个生命中会一直存在,只有服务器被关闭时,这个客户端才会被关闭

13.2.4 AOF文件的伪客户端

服务器在载入AOF文件时,会创建用于执行AOF文件包含的Redis命令的伪客户端,并在载入完成后,关闭这个伪客户端。

13.3 重点回顾
  • 服务器状态结构使用clients链表连接起多个客户端状态,新添加的客户端状态会被放到链表的末尾。
  • 客户端状态的flags属性使用不同标志来表示客户端的角色,以及客户端当前所处的状态
  • 输入缓冲区记录了客户端发送的命令请求,这个缓冲区的大小不能超过1GB
  • 命令的参数和参数个数会被记录在客户端状态的argv和argc属性里面,而cmd属性则记录了客户端要执行的实现函数。
  • 客户端有固定大小缓冲区和可变大小缓冲区两种缓冲区可用,其中固定大小缓冲区的最大大小为16kb,而可变大小的缓冲区的最大大小不能超过服务器设置的硬性限制值
  • 输出缓冲区限制值有两种,如果输出缓冲区的大小超过了 服务器设置的硬性限制,那么客户端会被立即关闭;初次之外,如果客户端在一定时间内,一致超过服务器设置软性限制,那么客户端也会被关闭
  • 当一个客户端通过网络连接连上服务器时,服务器会为这个客户端创建相应的客户端状态。网络连接关闭、发送了不喝协议格式的命令请求、成为CLIENT KILL命令的目标、空转时间超时、输出缓冲区的大小超出限制,以上这些原因都会造成客户端被关闭
  • 处理Lua脚本的伪客户端在服务器初始化时创建,这个客户端会一直存在,知道服务器关闭。
  • 载入AOF文件时使用的伪客户端在载入工作开始时动态创建,载入工作完毕之后关闭。

第14章 服务端

14.1 命令请求的执行过程

从客户端发送SET KEY VALUE命令 到获得回复OK期间,客户端和服务器共需要执行以下操作:

  1. 客户端向服务器发送命令请求SET KEY VALUE。
  2. 服务器接收并处理客户端发来的命令请求SET KEY VALUE,在数据库中进行设置操作,并产生命令回复OK。
  3. 服务器将命令回复OK发送给客户端
  4. 客户端接收服务器返回的命令回复OK,并将这个回复打印给用户观看。
14.1.1 发送命令请求

用户 在客户端中键入一个命令请求,客户端会将这个命令请求转换成协议格式,然后通过连接服务器的套接字,将协议格式的命令请求发送给服务器。

14.1.2 读取命令请求

当客户端与服务器之间连接套接字因为客户端的写入而变得可读时,服务器将调用命令请求处理器来执行以下操作:

  1. 读取套接字中协议格式的命令请求,并将其保存到客户端状态的输入缓冲区里面。
  2. 对输入缓冲区中的命令请求进行分析,提取出命令请求中包含的命令参数,以及命令参数的个数,然后分别将参数和参数个数保存到客户端状态的argv属性和argv属性里面。
  3. 调用命令执行器,执行客户端指定的命令。
14.1.3 命令执行(1):查找命令实现

第一件事,根据客户端状态的argv[0]参数,在命令表(command table)中查找参数所指定的命令,并将找到的命令保存到客户端状态的cmd属性里面。
命令表时一个字典,字典的键时一个个命令的名字,比如“set”等;字典的值时一个redisCommand结构。
每个redisCommand结构记录了一个Redis命令的实现。
redisCommand结构的主要属性

属性名类型作用
namechar *命令的名字,比如“set”
procredisCommandProc *函数指针,指向命令的实现函数,比如setCommand。redisCommandProc类型的定义为typedef void redisCommandProc(redisClient *c);
arityint命令参数的个数,用于检查命令请求的格式是否正确。如果这个值为负数-N,那么表示参数的数量大于等于N。注意命令的名字本身也是一个参数。
sflagschar *字符串形式的标志值,这个值记录了命令的属性,比如这个命令是写命令还是读命令,这个命令是否允许在载入数据时使用,这个命令是否允许在Lua脚本中使用等等。
flagsint对sflags标志进行分析得出的二进制标识,由程序自动生成。服务器对命令标识进行检查时使用的都是flags属性而不是sflags属性,因为对二进制标识的检查可以方便地通过&、^、~等操作来完成。
callslong long服务器总共执行了多少次这个命令
millisecondslong long服务器执行这个命令锁耗费的总时长

sflags属性的标识

标识意义带有这个标识的命令
w这是一个写入命令,可能会修改数据库SET、RPUSH、DEL等等
r这是一个只读命令,不会修改数据库GET、STRLEN、EXISTS等等
m这个命令可能会占用大量内存,执行之前需要先检查服务器的内存使用情况,如果内存紧缺的话就禁止执行这个命令SET、APPEND、RPUSH、LPUSH、SADD、SINTERSTORE等等
a这是一个管理命令SAVE、BGSAVE、SHUTDOWN等等
p这是一个发布与订阅功能方面的命令PUBLISH、SUBSCRIBE、PUBSUB等等
s这个命令不可以在Lua脚本中使用BRPOP、BLPOP、BRPOPLPUSH、SPOP等等
R这是一个随机命令,对于相同的数据集和相同的参数,命令返回的结果可能不同SPOP、SRANDMEMBER、SSCAN、RANDOMKEY等等
S当在Lua脚本中使用这个命令时,对这个命令的输出结果进行一次排序,使得命令的结果有序SINTER、SUNION、SDIFF、SMEMBERS、KEYS等等
l这个命令可以再服务器载入数据的过程中使用INFO、SHUTDOWN、PUBLISH等等
t这是一个允许从服务器在带有过期数据时使用的命令SLAVEOF、PING、INFO等等
M这个命令在监视器(monitor)模式下不会自动被传播(propagate)EXEC
14.1.4 命令执行(2):执行预备操作

在真正执行命令之前那,程序还需要进行一些预备操作:

  • 检查客户端状态的cmd指针是否指向NULL,如果是的话,那么说明用户输入的命令名字找不到相应的命令实现,服务器不再执行后续步骤,并向客户端返回一个错误。
  • 根据客户端cmd属性指向的redisCommand结构的arity属性,检查命令请求所给定的参数个数是否正确,当参数个数不正确时,不再执行后续步骤,直接向客户端返回一个错误。
  • 检查客户端是否已经通过了身份验证,未通过身份验证的客户端只能执行AUTH命令,如果未通过身份验证的客户端视试图执行除AUTH命令之外的其他命令,那么服务器将想客户端返回一个错误。
  • 如果服务器打开了maxmemory功能,那么在执行命令之前,先检查服务器的内存占用情况,并在有需要时进行内存回收,从而使得接下来的命令可以顺利执行。如果内存回收失败,那么不再执行后续步骤,向客户端返回一个错误。
  • 如果服务器上一次执行BGSAVE命令时出错,并且服务器打开了stop-writes-on-bgsave-error功能,而且服务器即将要执行的命令时一个写命令,那么服务器将拒绝执行这个命令,并向客户端返回一个错误。
  • 如果客户端当前正在用SUBSCRIBE命令订阅频道,或者正在用PSUBSCRIBE命令订阅模式,那么服务器只会执行客户端发来的SUBSCRIBE、PSUBSCRIBE、UNSUBSCRIBE、PUNSUBSCRIBE四个命令,其他命令都会被服务器拒绝。
  • 如果服务器正在进行数据载入,那么客户端发送的命令必须带有1标识(比如INFO、SHUTDOWN、PUBLISH等等)才会被服务器执行,其他命令都会被服务器拒绝。
  • 如果服务器因为执行Lua脚本而超市并进入阻塞状态,那么服务器只会执行客户端发来的SHUTDOWN nosave命令和SCRIPT KILL命令,其他命令都会被服务器拒绝。
  • 如果客户端正在执行事务,那么服务器只会执行客户端发来的EXEC、DISCARD、MULTI、WATCH四个命令,其他命令都会被放进事务队列中
  • 如果服务器打开了监视器功能,那么服务器会将要执行的命令和参数等信息发送给监视器。
    当完成了以上预备操作之后,服务器就可以开始真正执行命令了。

以上只列出了服务器在单机模式下执行命令时的检查操作,当服务器在复制或者集群模式下执行命令时,预备操作还会更多一些。

14.1.5 命令执行(3):调用命令的实现函数

当服务器决定要执行命令时,只要执行以下语句就可以了:

// client是指向客户端状态的指针
client->cmd->proc(client);
等于执行语句
setCommand(client);

被调用的命令实现函数会执行指定的操作,并产生相应的命令回复,这些回复会被保存在客户端状态的输出缓冲区(buf属性和reply属性),之后实现函数还会为客户端的套接字关联命令回复处理器,这个处理器负责将命令回复返回给客户端。

14.1.6 命令执行(4):执行后续工作

有:

  • 如果服务器开启了慢查询日志功能,那么慢查询日志模块会检查是否需要为刚刚执行完的命令请求添加一条新的慢查询日志
  • 根据刚刚执行命令所耗费的时长,更新被执行命令的redisCommand结构的milliseconds属性,并将命令的redisCommand结构的calls计数器的值增一。
  • 如果服务器开启了AOF持久化功能,那么AOF持久化模块会将刚刚执行的命令请求写入到AOF缓冲区里面
  • 如果有其他从服务器正在复制当前这个服务器,那么服务器会将刚刚执行的命令传播给所有从服务器。
14.1.7 将命令回复发送给客户端

命令实现函数会将命令回复保存到客户端的输出缓冲区里面,并为客户端的套接字关联命令回复处理器,当客户端套接字变为可写状态时,服务器就会执行命令回复处理器,将保存在客户端输出缓冲区中的命令回复发送给客户端。
然后回复处理器会清空客户端状态的输出缓冲区,为处理下一个命令请求做好准备。

14.1.8 客户端接收并打印命令回复

当客户端接收到协议格式的命令回复之后,它会将这些回复转换成人类可读的格式,并打印给用户观看。

14.2 serverCron函数

Redis服务器中的serverCron函数默认每隔100毫秒执行一次,负责管理服务器的资源,并保持服务器自身的良好运转。

struct redisServer {
	// ...
	// 一个链表,保存了所有客户端状态
	list *clients;
	// 记录了保存条件的数组
	struct saveparam *saveparams;
	
	// 修改计数器
	long long dirty;
	// 上一次执行保存的时间
	time_t lastsave;
	
	// 数据库键空间,保存着数据库中的所有键值对
	dict *dict;
	
	// 保存了秒级精度的系统当前UNIX时间戳
	time_t unixtime;
	// 保存了毫秒级
	long long mstime;
	// 默认每10秒更新一次的时钟缓存
	// 用于计算键的空转idle时长
	unsigned lrulock:22; 
	
	// 上次进行抽样的时间
	long long ops_sec_last_sample_time;
	// 上一次抽样时,服务器已执行命令的数量
	long long ops_sec_last_sample_ops;
	// REDIS_OPS_SEC_SAMPLES大小(默认值为16)的环形数组
	// 数组中的每个项都记录了一次抽样结果
	long long ops_sec_samples[REDIS_OPS_SEC_SAMPLES];
	// ops_sec_samples数组的索引值
	// 每次抽样后将值自增一
	// 在值等于16时重置为0
	// 让ops_sec_samples数组构成一个环形数组
	int ops_sec_idx;
	
	// 已使用内存峰值
	size_t stat_peak_memory;

	// 关闭服务器的标识
	// 值为1时,关闭服务器
	// 值为0时,不做动作
	int shutdown_asap;
	
	// 如果值为1,那么表示有BGREWRITEAOF命令被延迟了
	int aof_rewrite_scheduled;

	// 记录执行BGSAVE命令的子进程的ID
	// 如果服务器没有在执行BGSAVE
	// 那么这个属性的值为-1
	pid_t rdb_child_pid;
	// 记录执行BGREWRITEAOF命令的子进程的ID
	// 如果服务器没有在执行BGREWRITEAOF
	// 那么这个属性的值为-1
	pid_t aof_child_pid;
	
	// serverCron函数的运行次数计数器
	// serverCron函数每执行一次,这个属性的值就增一
	int cronloops;
	// ...
}
14.2.1 更新服务器时间缓存

因为Redis服务器中有不少功能需要获取系统的当前时间,所以unixtime和mstime属性被用作当前时间的缓存。
精度不高
对于为键设置过期时间,添加慢查询日志这种需要高精度时间的功能来说,服务器还是会再次执行系统调用。

14.2.2 更新LRU时钟
typedef struct redisObject {
	unsigned lru:22;
} robj;

serverCron函数默认会以每10秒一次的频率更新lruclock属性的值,因为这个时钟不是实时的,所以根据这个属性计算出来的LRU时间实际上只是一个模糊的估算值

14.2.3 更新服务器每秒执行命令次数

serverCron函数中的trackOperationsPerSecond函数会以每100毫秒一次的频率执行,这个函数的功能是以抽样计算的方式,估算并记录服务器在最近一秒中处理的命令请求数量。

INFO stats

在上述redisServer中

14.2.4 更新服务器内存峰值记录

每次执行函数时,都会查看服务器当前使用的内存数量,并于stat_peak_memory保存的值进行比较如果当前使用的内存数量比stat_peak_memory属性记录的值要打,那么程序就将当前使用的内存数量记录到stat_peak_memory属性里面。

INFO momory
14.2.5 处理SIGTERM信号

在启动服务器时,Redis会为服务器进程的SINGTERM信号关联处理器sigtermHandler函数,这个信号处理器负责在服务器街道SIGTERM信号时,打开服务器状态的shoutdown_asap标识。
每次serverCron函数运行时,程序都会对服务器的shutdown_asap属性进行检查,并根据属性的值决定是否关闭服务器。
服务器在关闭自身之前会进行RDB持久化操作,这也是服务器拦截SIGTERM信号的原因。

14.2.6 管理客户端资源

都会执行clientsCron函数,该函数会对一定数量的客户端进行一下两个检查:

  • 如果客户端与服务器之间的连接已经超时(很长一段时间里客户端和服务器都没有互动),那么程序释放这个客户端。
  • 如果客户端在上一次执行命令请求之后,输入缓冲区的大小超过了一定的长度,那么程序会释放客户端当前的输入缓冲区,并重新创建一个默认大小的输入缓冲区,从而防止客户端的输入缓冲区耗费了过多的内存
14.2.7 管理数据库资源

serverCron函数每次执行都会调用databasesCron函数,这个函数会对服务器中的一部分数据库进行检查,删除其中的过期键,并在有需要时,对字典进行收缩操作。

14.2.8 执行被延迟的BGREWRITEAOF

在服务器执行BGSAVE命令的期间,如果客户端向服务器发来BGREWRITEAOF命令,那么服务器会将BGREWRITEAOF命令的执行时间延迟到BGSAVE命令执行完毕之后。
aof_rewrite_scheduled标识了服务器是否延迟了BGREWRITEAOF命令。
每次serverCron函数执行是,函数都会检查BGSAVE命令或者BGREWRITEAOF命令是否正在执行,如果这两个命令都没在执行,并且aof_rewrite_scheduled属性的值为1,那么服务器就会执行之前被推延的BGREWRITEAOF命令。

14.2.9 检查持久化操作的运行状态

rdb_child_pid和aof_child_pid,记录BGSAVE和BGREWRITEAOF的子进程ID,检查是否正在执行。
每次serverCron函数执行时,程序都会检查rdb_child_pid和aof_child_pid,
只要其中一个值不为-1,程序就会执行一次wait3函数,检查子进程是否有信号发来服务器进程。

  • 如果有信号到达,那么表示新的RDB文件已经生成完毕(对于BGSAVE命令来说),或者AOF文件已经重写完毕(对于BGREWRITEAOF命令来说),服务器需要进行相应命令的后续操作,比如用心的RDB的文件替换现有的RDB文件,或者用重写后的AOF文件替换现有的AOF文件
  • 如果没有信号到达,那么表示持久化操作未完成,程序不做动作

如果rdb_child_pid和aof_child_pid两个属性都为-1,那么表示服务器没有在进程持久化操作,程序会进程以下三个检查:

  1. 查看是否有BGREWRITEAOF被延迟了,如果有的话,那么开始一次新的GBREWRITEAOF操作。
  2. 检查服务器的自动保存条件是否已经被满足,如果条件满足,并且服务器没有在执行其他持久化操作,那么服务器开始一次新的GBSAVE操作(因为条件1可能会引发一次BGREWRITEAOF,所以在这个检查中,程序会再次确认服务器是否已经在执行持久化操作了)
  3. 检查服务器设置的AOF重写条件是否满足,如果条件满足,并且服务器没有在执行其他持久化操作,那么服务器将开始一次新的BGREWRITEAOF操作(条件1和条件2都可能会引起新的持久化操作,所以在这个检查中,我们要再次确认服务器是否已经在执行的持久化操作了)。
14.2.10 将AOF缓冲区中的内容写入AOF文件

如果服务器开启了AOF持久化功能,并且AOF缓冲区里面还有待写入的数据,那么serverCron函数会调用相应的程序,将AOF缓冲区中的内容写入到AOF文件里面

14.2.11 关闭异步客户端

服务器会关闭哪些删除缓冲区超出限制的客户端。

14.2.12 增加cronloops计数器的值

服务器状态的cronloops属性记录了serverCron函数执行的次数.
cronloops属性目前在服务器找那个的唯一作用,就是在复制模块中实现”每执行serverCron函数就执行一次指定代码“的功能。

14.3 初始化服务器

过程比如
初始化服务器状态,
接收用户指定的服务器配置,
创建相应的数据结构和网络连接

14.3.1 初始化服务器状态结构

第一步,创建一个struct redisServer类型的实例变量server作为服务器的状态,并为结构中的各个属性设置默认值。
初始化server变量的工作由redis.c/initServerConfig函数完成。
主要工作:

  • 设置服务器的运行ID
  • 设置服务器的默认运行频率
  • 设置服务器的默认配置文件路径
  • 设置服务器的运行架构
  • 设置服务器的默认端口号
  • 设置服务器的默认RDB持久化条件和AOF持久化条件
  • 初始化服务器的LRU时钟
  • 创建命令表

除了命令表之外,initServerConfig函数没有创建服务器状态的其他数据结构,数据库、慢查询日志、Lua环境、共享对象这些在数据结构在后续步骤才会被创建。

14.3.2 载入配置选项
  • 终端中输入的内容
  • redis.conf文件中包含的内容。

初始化载入配置选项时,会有相应的默认值

14.3.3 初始化服务器数据结构

除了命令表之外,服务器状态还包含其他数据结构:

  • server.clients链表,记录了所有与服务器相连的客户端的状态结构,链表的每个节点都包含了一个redisClient结构实例。
  • server.db数组,数组中包含了服务器的所有数据库。
  • 用于保存频道订阅信息的server.pubsub_channels字典,以及用于保存模式订阅信息的server.pubsub_patterns链表。
  • 用于执行Lua脚本的Lua环境server.lua
  • 用于保存慢查询日志的server.slowlog属性。

当初始化服务器进行到这一步,服务器将调用initServer函数,为以上提到的数据结构分配内存,并在有需要时,为这些数据结构设置或者管理初始化值。
为什么要在这时初始化数据结构?
服务器必须先载入用户指定的配置选项,然后才能正确地对数据结构进行初始化。如果在执行initServerConfig函数时就对数据结构进行初始化,那么一旦用户通过配置选项修改了和数据结构有关的服务器状态属性,服务器就要重新调整和修改已创建的数据结构。
所以将server状态的初始化分为两步执行:
initServerConfig函数主要负责初始化一般属性;
initServer函数主要负责初始化数据结构。
initServer还进行了一些非常重要的设置操作:

  • 为服务器设置进程信号处理器
  • 创建共享对象:这些对象包含Redis服务器经常用到的一些值,比如包含“OK”回复的字符串对象,包含“ERR”回复的字符串对象,包含整数1到10000的字符串对象等等,服务器通过重用这些共享对象来避免反复创建相同的对象。
  • 打开服务器的监听端口,并为监听套接字关联连接应答时间处理器,等待服务器正式运行时接受客户端的连接
  • 为serverCron函数创建时间事件,等待服务器正式运行执行serverCron函数
  • 如果AOF持久化功能已经打开,那么打开现有的AOF文件,如果AOF文件不存在,那么创建并打开一个新的AOF文件,为AOF写入做好准备

当initServer函数执行完毕之后,服务器用ASCII字符在日志中个打印出Redis的图标,以及Redis的版本号信息。

14.3.4 还原数据库状态

完成了对服务器状态server变量的初始化之后,服务器需要载入RDB文件或者AOF文件,并根据文件记录的内容来还原服务器的数据库状态。
根据服务器是否启用了AOF持久化功能,载入数据时所使用的目标文件会有所不同:

  • 启用,那么服务器使用AOF文件来还原数据库状态
  • 未启用,服务器使用RDB文件来还原数据库状态
14.3.5 执行时间循环

完成初始化之后,开始执行服务器的时间循环(loop)
至此,服务器的初始化工作圆满完成,服务器现在开始可以接受客户端的连接请求,并处理客户端发来的命令请求了。

14.4 重点回顾
  • 一个命令请求从发送到完成主要包括以下步骤:1)客户端将命令请求发送给服务器;2)服务器读取命令请求,并分析出命令参数;3)命令执行器根据参数查找命令的实现函数,然后执行实现函数并得出命令回复;4)服务器将命令回复返回给客户端。
  • serverCron函数默认每隔100毫秒执行一次,它的工作主要包括更新服务器状态信息,处理服务器接收的SIGTERM信号,管理客户端资源和数据库状态,检查并执行持久化操作等等。
  • 服务器从启动到能够处理客户端的命令请求需要执行以下步骤:1)初始化服务器状态;2)载入服务器配置;3)初始化服务器数据结构;4)还原数据库状态;5)执行事件循环。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值