Redis设计与实现总结--单机数据库的实现

1、数据库

1.1 服务器中的数据库

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

struct redisServer{
    // ...

    // 一个数组,保存着服务器中的所有数据库
    redisDb *db;

    // 服务器的数据库数量,默认是16
    int dbnum;

    // ...
}

1.2 切换数据库

Redis客户端的默认目标数据库为0号数据库,但客户端可以通过执行SELECT命令来切换目标数据库。在服务器内部,客户端状态redisClient结构的db属性记录了客户端当前的目标数据库,这个属性是一个指向redisDb结构的指针。redisClient.db指针指向redisServer.db数组的其中一个元素,而被指向的元素就是客户端的目标数据库。

struct redisClient{
    // ...

    // 记录客户端当前正在使用的数据库
    redisDb *db;

    // ...
}redisClient;

1.3 数据库键空间

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

typedef struct redisDb {

    // ...

    // 数据库键空间,保存着数据库中的所有键值对
    dict *dict;

    // ...

} redisDb;

键空间和用户所见的数据库是直接对应的:

  • 键空间的键也就是数据库的键, 每个键都是一个字符串对象。
  • 键空间的值也就是数据库的值, 每个值可以是字符串对象、列表对象、哈希表对象、集合对象和有序集合对象在内的任意一种 Redis 对象。

1.3.1 添加新键

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

1.3.2 删除键

删除数据库中的一个键, 实际上就是在键空间里面删除键所对应的键值对对象。

1.3.3 更新键

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

1.3.4 对键取值

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

1.3.5 其他键空间操作

还有很多针对数据库本身的 Redis 命令, 也是通过对键空间进行处理来完成的。用于清空整个数据库的 FLUSHDB 命令, 就是通过删除键空间中的所有键值对来实现的。用于随机返回数据库中某个键的 RANDOMKEY 命令, 就是通过在键空间中随机返回一个键来实现的。 用于返回数据库键数量的 DBSIZE 命令, 就是通过返回键空间中包含键值对的数量来实现的。类似的命令还有 EXISTS 、 RENAME 、 KEYS , 等等, 这些命令都是通过对键空间进行操作来实现的。

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

当使用 Redis 命令对数据库进行读写时, 服务器不仅会对键空间执行指定的读写操作, 还会执行一些额外的维护操作, 其中包括:

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

1.4 设置键的生存时间或者过期时间

通过EXPIRE或者PEXPIRE命令,客户端可以以秒或者毫秒精度为数据库中的某个键设置生存时间(Time To Live, TTL),在经过指定的秒数或者毫秒数之后,服务器就会自动删除生存时间为0的键。客户端也可以通过EXPIREAT或者 PEXPIREAT命令,以秒或者毫秒精度给数据库中的某个键设置过期时间(expire time)。TTL 或者PTTL命令接受一个带有生存时间或者过期时间的键,返回这个键的剩余时间

1.4.1 设置过期时间

  • EXPlRE <key> <ttl> 命令用于将键key 的生存时间设置为ttl 秒。
  • PEXPIRE <key> <ttl> 命令用于将键key 的生存时间设置为ttl 毫秒。
  • EXPIREAT <key> < timestamp> 命令用于将键key 的过期时间设置为timestamp所指定的秒数时间戳。
  • PEXPIREAT <key> < timestamp > 命令用于将键key 的过期时间设置为timestamp所指定的毫秒数时间戳。

实际上其余三个命令都是使用PEXPIREAT命令来实现的,转换过程如下图。

1.4.2 保存过期时间

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

  • 过期字典的键是一个指针,这个指针指向键空间中的某个键对象,也就是某个数据库键。
  • 过期字典的值是一个long long类型的整数,这个整数保存了键所指向的数据库键的过期时间——一个毫秒精度的UNIX时间戳。 
typedef struct redisDb{
    // ...

    // 过期字典,保存着键的过期时间
    dict *expires;

    // ...
}redisDb;

1.4.3 移除过期时间

PERSIST命令可以移除一个键的过期时间。在过期字典中查找给定的键,并解除键和值(过期时间)在过期字典中的关联。

1.4.4 计算并返回剩余时间

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

1.4.5 过期键的判定

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

1.5 过期键删除策略

  • 定时删除:在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除操作
  • 惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键
  • 定期删除: 每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于要删除多个过期键,以及要检查多少个数据库,则由算法决定

第一种和第三种为主动删除策略,而第二种则为被动删除策略。

1.5.1 定时删除

定时删除策略对内存是最友好的:通过使用定时器,定时删除策略可以保证过期键会尽可能快地被删除,并释放过期键所占用的内存。定时删除策略的缺点是,它对CPU时间是最不友好的:在过期键比较多的情况下,删除过期键这一行为可能会占用相当一部分CPU时间,在内存不紧张但是CPU时间非常紧张的情况下,将CPU时间用在删除和当前任务无关的过期键上,无疑会对服务器的响应和吞吐时间造成影响。创建一个定时器需要用到Redis服务器中的时间事件,而当前时间事件的实现方式——无序列表,查找一个事件的时间复杂度为O(N)——并不能高效地处理大量时间事件。

1.5.2 惰性删除

惰性删除策略对CPU时间来说是最友好的:程序只会在取出键时才对键进行过期检查,这可以保证删除过期键的操作只会在非做不可的情况下进行,并且删除的目标仅限于当前处理的键,这个策略不会再删除其他无关的过期键上花费任何CPU时间。惰性删除策略的缺点是,它对内存是最不友好的:如果一个键已经过期,而这个键又仍然保留在数据库中,那么只要这个过期键不被删除,它所占用的内存就不会释放。

1.5.1 定期删除

定期删除策略是前两种策略的一种整合和折中:

  • 定期删除策略每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响
  • 通过定期删除过期键,定期删除策略有效地减少了因为过期键而带来的内存浪费

定期删除策略的难点是确定删除操作执行的时长和频率:

  • 如果删除操作执行得太频繁,或者执行的时间太长,定期删除策略就会退化成定时删除策略,以至于将CPU时间过多地消耗在删除过期键上面
  • 如果删除操作执行得太少,或者执行的时间太短,定期删除策略又会和惰性删除策略一样,出现浪费内存的情况

1.6 Redis的过期键删除策略

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

1.6.1 惰性删除策略的实现 

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

  • 如果输入键已经过期,那么expireIfNeeded函数将输入键从数据库中删除。
  • 如果输入键未过期,那么expireIfNeeded函数不做动作。

因为每个被访问的键都有可能因为过期而被expireIfNeeded函数删除,所以每个命令的实现函数都必须能同时处理键存在以及键不存在这两种情况:

  • 当键存在时,命令按照键存在的情况执行。
  • 当键不存在或者键因为过期而被expireIfNeeded函数删除,命令按照键不存在的情况执行。

1.6.2 定期删除策略的实现

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

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

1.7 AOF、RDB和复制功能对过期键的处理

1.7.1 生成RDB文件

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

1.7.2 载入RDB文件

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

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

1.7.3 AOF文件写入

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

1.7.4 AOF重写

和生成RDB文件时类似,在执行AOF重写的过程中,程序会对数据库中的键进行检查,已过期的键不会被保存到重写后的AOF文件中。

1.7.5 复制

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

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

1.8 数据库通知

数据库通知是2.8版本新增加的功能,这个功能可以让客户端通过订阅给定的频道或者模式,来获知数据库中键的变化,以及数据库中命令的执行情况。

  • 键空间通知(key-space-notification):某个键执行了什么命令的通知
  • 键事件通知(key-event-notification):关注的是某个命令被什么键执行了

1.8.1 发送通知

发送数据库通知的功能是由notify.c/notifyKeyspaceEvent函数实现的。type参数是当前想要发送的通知的类型,程序会根据这个值来判断通知是否就是服务器配置notify-keyspace-events选项所选定的通知类型,从而决定是否发送通知;event是事件的名称;key是产生事件的键;dbid是产生事件的数据库号码。

void notifyKeyspaceEvent(int type, char* event, robj * key, int dbid)
 

在这里插入图片描述

1.8.2 发送通知的实现

以下是notifyKeyspaceEvent函数的伪代码实现:

  •  server.notify_keyspace_events属性就是服务器配置notify-keyspace-events选项所设置的值,如果给定的通知类型type不是服务器允许发迭的通知类型,那么函数会直接返回,不做任何动作。
  •  如果给定的通知是服务器允许发送的通知,那么下一步函数会检测服务器是否允许发送键空间通知,如果允许的话,程序就会构建并发送事件通知。
  •  最后,函数检测服务器是否允许发送键事件通知,如果允许的话,程序就会构建并发送事件通知。

另外,pubsubPublishMessage函数是PUBL1SH命令的实现函数,执行这个函数等同于执行PUBL1SH命令,订阅数据库通知的客户端收到的信息就是由这个函数发出的。

2、RDB持久化

Redis是内存数据库,它将自己的数据库状态存储在内存里面,所以如果不想办法将存储在内存中的数据库状态保存到磁盘,那么服务器 进程一旦退出,服务器中的数据库状态也会消失不见。为了解决这个问题,Redis提供了RDB持久化功能,这个功能可以将数据库状态保存到磁盘里面。

2.1 RDB文件的创建与载入

Redis可以使用SAVE或BGSAVE命令创建RDB文件。SAVE命令会阻塞服务器进程,直到RDB文件创建完毕,在服务器阻塞过程中服务器不能处理任何命令,所以此时客户端发送来的命令都会被拒绝。BGSAVE命令会派生一个子线程,然后由子线程负责创建RDB文件,服务器进程(主进程)继续处理命令。RDB文件的载入是在服务器启动时自动执行,所以Redis并没有专门载入RDB文件的命令,只要Redis服务器在启动时检测到RDB文件存在,它就会自动载入。

由于AOF文件的更新频率通常比RDB文件更新频率高,所以如果服务器开启了AOF,那么服务器优先从AOF文件还原数据库,只有AOF关闭时,服务器才会使用RDB文件还原数据库。

2.1.1 SAVE 命令执行时的服务状态

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

2.1.2 BGSAVE 命令执行时的服务状态 

BGSAVE命令执行时,Redis服务器处理SAVE、BGSAVE、BGREWRITEAOF命令方式会与平时不同。

  • SAVE命令会被服务器拒绝,服务器禁止SAVE、BGSAVE命令同时执行,是为了避免主线程与子线程同时执行rdbSave产生竞争条件。
  • BGSAVE命令同样也会被拒绝,因为两个BGSAVE命令也会产生竞争条件。
  • BGREWRITEAOF命令会被延迟到BGSAVE命令执行完毕后执行,如果 BGREWRITEAOF命令正在执行,那么客户端发送的BGSAVE命令会被服务器拒绝。两个命令不能同时执行是出于性能方面考虑,两个命令都是由子进程执行。

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

RDB文件载入时,服务器会处在阻塞状态,直到载入工作完成。

2.2 自动间隔性保存

因为BGSAVE可以在不阻塞服务器进程的情况下执行,所以Redis允许用户通过设置服务器配置的save选项,让服务器每隔一段时间自动执行一次BGSAVE命令。

2.2.1 设置保存条件

当Redis服务器启动时,用户可以通过指定配置文件或者传入启动参数的方式设置save选项,如果用户没有主动设置save选项,那么服务器会为save选项设置默认条件:

save 900 1
save 300 10
save60 10000

服务器程序会根据save选项所设置的保存条件,设置服务器状态redisServer结构的saveparams属性:

struct redisServer{
	//记录了保存条件的数组
	struct saveparam *saveparams;
};

saveparams属性是一个数组,数组中的每一个元素都是一个saveparam结构,每个saveparam结构都保存了一个save选项设置的保存条件:

struct saveparam{
//秒数
time_t seconds;
//修改次数
int changes;
}

2.2.2 dirty计数器和lastsave属性

除了saveparams数组外,服务器状态还维持一个dirty计数器,以及一个lastsave属性:

  • dirty计数器就距离上次成功执行SAVE命令或者BGSAVE命令之后,服务器对数据库状态进行了多少次修改(更删改操作)。
  • lastsave属性是一个UNIX时间戳,记录了服务器上次成功执行SAVE命令或者BGSAVE命令的时间。
struct redisServer {
    //修改计数器
    long long dirty;
    time_t lastsave;
};

当服务器成功执行一个数据库修改命令后,程序会对dirty计数器进行更新:命令修改了多少次数据库,dirty计数器的值就增加了多少。

2.2.3 检查保存条件是否满足

Redis服务器周期性操作函数serverCron默认每隔100毫秒就执行一次,该函数用于对正在运行的服务器进行维护,它的一项工作就是检查save选项的保存条件是否被满足,如果满足,就执行BGSAVE命令。以下伪代码展示了serverCron函数检查保存条件的过程。

2.3 RBD文件结构

下图展示了一个完整的RDB文件所包含的各个部分:

注意:为了方便区分变量、数据、常量,上图中用全大写单词标识常量,用全小写标识变量和数据。

RDB文件的最开头是Redis部分,这个部分的长度为5字节,保存着"REDIS"五个字符。通过这五个字符,程序可以在载入文件时,快速检查所载入的文件是否是RDB文件。

注意:因为RDB文件保存的是二进制数据,而不是C字符串,为了简单起见,我们用"REDIS"符号代表’R’、‘E’、‘D’、‘I’、‘S’五个字符,而不是待’\0’结尾符号的C字符串’R’、‘E’、‘D’、‘I’、‘S’、’\0’。本章介绍的所有内容,以及展示的所有RDB文件结构都遵循这一规则。

db_version长度为4字节,它的值是一个字符串表示的整数,这个整数记录了RDB文件的版本号,比如"0006"就代表RDB文件的版本为第六版。

databases部分包含着零个至任意多个数据库以及各个数据库中的键值对数据:

  • 如果服务器的数据库状态为空(所有数据库都是空的),那么这个部分也为空,长度为0字节
  • 如果服务器的数据库状态为非空(有至少一个数据库非空),那么这个部分也为非空,根据数据库所保存键值对的数量、类型和内容的不同,这个部分的长度也会有所不同

EOF常量的长度为1字节,这个常量标志着RDB文件正文内容结束,当读入程序遇到这个值的时候,它知道所有数据库的所有键值对都已加载完毕。

check_num是一个8字节长的无符号整数,保存着一个校验和,这个校验和是程序通过对REDIS、db_version、databases、EOF四部分的内容进行计算得出的。服务器在载入RDB文件时,会将载入数据所计算出的校验和与check_num所记录的校验和进行对比,以此来检查RDB文件是否有出错或者损坏的情况出现。

2.3.1  databases部分

一个RDB文件的databases部分可以保存任意多个非空数据库。

  • 例如,如果服务器的0号数据库和3号数据库非空,那么服务器将创建一个如下图所示的RDB文件,图中的databases 0代表0号数据库的所有键值对数据,而databases 3则代表3号数据库中所有键值对数据。

在这里插入图片描述

每个非空数据库在RDB文件中都可以保存为SELECTDB、db_number、key_value_pairs三个部分,如下图所示。

  • SELECTDB常量的长度为1字节,当读入程序遇到这个值的时候,它知道接下来要读入的将是一个数据库号码
  • db_number保存着一个数据库号码,根据号码的大小不同,这个部分的长度可以是1字节、2字节或者5字节。当程序读入db_number部分之后,服务器会调用SELECT命令,根据读入的数据库号码进行数据库切换,使得之后读入的键值对可以载入到正确的数据库中
  • key_value_pairs部分保存了数据库中的所有键值对数据,如果键值对带有过期时间,那么过期时间也会和键值对保存在一起。根据键值对的数量、类型、内容以及是否有过期时间等条件不同,key_value_pairs部分的长度也会有所不同。

下图则展示了一个完成的RDB文件,文件包含了0号数据库和3号数据库。

2.3.2 key_value_pairs部分

RDB文件中的每个key_value_pairs部分都保存了一个以上的键值对。不带过期时间的键值对在RDB文件中由TYPE、key、value三部分组成,如下图所示:

在这里插入图片描述

TYPE记录了value的类型,长度为1字节,值可以是以下常量的其中一个:

  • REDIS_RDB_TYPE_STRING
  • REDIS_RDB_TYPE_LIST
  • REDIS_RDB_TYPE_SET
  • REDIS_RDB_TYPE_ZSET
  • REDIS_RDB_TYPE_HASH
  • REDIS_RDB_TYPE_LIST_ZIPLIST
  • REDIS_RDB_TYPE_SET_INTSET
  • REDIS_RDB_TYPE_ZSET_ZIPLIST
  • REDIS_RDB_TYPE_HASH_ZIPLIST

以上列出的每个TYPE常量都代表了一种对象类型或底层编码,当服务器读入RDB文件中的键值对数据时,程序会根据TYPE的值来决定如何读入和解释value的数据。key和value分别保存了键值对的键对象和值对象。
带有过期时间的键值对在RDB文件中的结构如下图所示:

TYPE、key、value三个部分的意义完全相同,EXPIRETIME_MS和ms意义如下:

  • EXPIRETIME_MS常量的长度为1字节,它告知读入程序,接下来要读入的将是一个以毫秒为单位的过期时间
  • ms是一个8字节长的带符号整数,记录着一个以毫秒为单位的Unix时间戳,这个时间戳就是键值对的过期时间

1.字符串对象

如果TYPE的值REDIS_RDB_TYPE_STRING,那么value保存的就是一个字符串对象,字符串对象的编码可以是REDIS_ENCODING_INT或REDIS_ENCODING_RAW。如果字符串对象对象,字符串对象的为REDIS_ENCODING_INT,那么说明对象中保存的是长度不超过32位的整数,这种编码的对象将以下图所示的结构保存。其中,ENCODING的值可以是REDIS_ENCODING_INT8、REDIS_ENCODING_INT16或者REDIS_ENCODING_INT32三个常量的其中一个,它们分别代表RDB文件使用8位(bit)、16位、32位来保存整数值integer。

在这里插入图片描述

举个例子,如果字符串对象中保存的是可以用8位来保存的整数123,那么这个对象在RDB文件中保存的结构将如下图所示。

在这里插入图片描述

如果字符串对象的编码为REDIS_ENCODING_RAW,那么说明对象所保存的是一个字符串值,根据字符串长度的不同,有压缩和不压缩两种方法来保存这个字符串:

  • 如果字符串的长度小于等于20字节,那么这个字符串会直接被原样保存
  • 如果字符串的长度大于20字节,那么这个字符串会被压缩之后再保存

对于没有被压缩的字符串,RDB程序会以下图所示的结构来保存该字符串。其中,string部分保存了字符串值本身,而len保存了字符串值的长度。

在这里插入图片描述

对于压缩后的字符串,RDB程序会以下图所示的结构来保存该字符串。其中,RDDIS_RDB_ENC_LZF常量标识着字符串已经被LZF算法压缩过,读入程序在碰到这个常量时,会根据之后的compressed_len、origin_len和compressed_string三部分,对字符串进行解压缩:其中compressed_len记录的是字符串被压缩后的长度,而origin_len记录的是字符串原来的长度,compressed_string记录的是被压缩后的字符串。

在这里插入图片描述

下图展示了一个保存无压缩字符串的例子,其中字符串长度为5,字符串的值为"hello"。

在这里插入图片描述

下图展示了一个压缩后的字符串示例,从图中可以看出,字符串原本的长度为21,压缩后的长度为6,压缩之后的字符串内容为"?aa???",其中?代表的是无法用字符串形式打印出来的字节。

在这里插入图片描述

2. 列表对象

如果TYPE的值为REDIS_RDB_TYPE_LIST,那么value保存的就是一个REDIS_ENCODING_LINKEDLIST编码的列表对象,RDB文件保存这种对象的结构如下图所示。list_length记录了列表的长度,它记录列表保存了多少个项(item)。

在这里插入图片描述

作为示例,下图展示了一个包含三个元素的列表:

在这里插入图片描述

3 集合对象

如果TYPE的值为REDIS_RDB_TYPE_SET,那么value保存的就是一个REDIS_ENCODING_HT编码的集合对象,RDB文件保存这种对象的结构如下图所示。其中,set_size是集合的大小,它记录了集合保存了多少个元素。图中以elem开头的部分代表集合的元素,因为每个集合元素都是一个字符串对象,所以程序会以处理字符串对象的方式来保存和读入集合元素。

在这里插入图片描述

4 哈希表对象

如果TYPE的值为REDIS_RDB_TYPE_HASH,那么value保存的就是一个REDIS_ENCODING_HT编码的集合对象,RDB文件保存这种对象的结构如下图所示。hash_size记录了哈希表的大小,也即是这个哈希表保存了多少键值对。

在这里插入图片描述

有key_value_pair开头的部分代表哈希表的键值对,键值对的键和值都是字符串对象,所以程序会以处理字符串对象的方式来保存和读入键值对,因此,从更详细的角度看,上图所展示的结构可以进一步修改为下图。

5 有序集合对象

如果TYPE的值为REDIS_RDB_TYPE_ZSET,那么value保存的就是一个REDIS_ENCODING_SKIPLIST编码的有序集合对象,RDB文件保存这种对象的结构如下图所示,sorted_set_size记录了有序集合的大小。以element开头的部分代表有序集合中的元素,每个元素又分为成员(member)和分值(score)两部分,成员是一个字符串对象,分值则是一个double类型的浮点数,程序在保存RDB文件时会先将分值转换成字符串对象,然后再用保存字符串对象的方法将分值保存起来。

在这里插入图片描述

因此,从详细的角度来看,上图所展示的结构可以进一步修改为下图

在这里插入图片描述

6 . INTSET编码的集合

如果TYPE的值为REDIS_RDB_TYPE_SET_INTSET,那么value保存的就是一个整数集合对象,RDB文件保存这种对象的方法是,先将整数集合转换成字符串对象,然后将这个字符串对象保存到RDB文件里面。如果程序在读入RDB文件的过程中,碰到由整数集合对象转换成的字符串对象,那么程序会根据TYPE值的指示,先读入字符串对象,再将这个字符串对象转换成原来的整数集合对象。

7 . ZIPLIST编码的列表、哈希表或有序集合

如果TYPE的值为REDIS_RDB_TYPE_LIST_ZIPLIST、REDIS_RDB_TYPE_HASH_ZIPLIST或REDIS_RDB_TYPE_ZSET_ZIPLIST,那么value保存的就是一个压缩列表对象,RDB文件保存这种对象的方法是:

  • 将压缩列表转换成一个字符串对象
  • 将转换所得字符串对象保存到RDB文件中

如果程序在读入RDB文件的过程中,碰到由压缩列表对象转换成的字符串对象,那么程序会根据TYPE值的指示,执行以下操作:

  • 读入字符串对象,并将它转换成原来的压缩列表对象
  • 根据TYPE的值,设置压缩列表对象的类型:如果TYPE的值为REDIS_RDB_TYPE_LIST_ZIPLIST,那么压缩列表对象的类型为列表;如果TYPE的值为REDIS_RDB_TYPE_HASH_ZIPLIST,那么压缩列表对象的类型为哈希表;如果TYPE的值为REDIS_RDB_TYPE_ZSET_ZIPLIST,那么压缩列表对象的类型为有序集合

2.4 分析RDB文件

我们使用od命令来分析Redis服务器产生的RDB文件,该命令可以用给定的格式转存(dump)并输入文件。比如说,给定-c参数可以以ASCII编码的方式打印输入文件,给定-x参数可以以十六进制的方式打印输出文件。

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

让我们从最简单的情况开始,执行以下命令,创建一个数据库状态为空的RDB文件。

2.4.2 包含字符串键的RDB文件

这次我们来分析下一个带有单个字符串键的数据库:

2.4.3 包含带有过期时间的字符串键的RDB文件

现在,让我们来创建一个带有过期时间的字符串键:

2.4.4 包含一个集合键的RDB文件

最后,让我们试试在RDB文件中包含集合键:

3、AOF持久化

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

3.1 AOF持久化的实现

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

3.1.1 命令追加

服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾:

struct redisServer {
    // ...
    // AOF 缓冲区
    sds aof_buf;
    // ...
};

3.1.2 AOF文件的写入与同步

因为服务器在处理文件事件时可能会执行写命令,使得一些内容被追加到aof_buf缓冲区里面,所以在服务器每次结束一个事件循环之前,它都会调用flushAppendOnlyFile函数,考虑是否需要将aof_buf缓冲区中的内容写入和保存到AOF文件里面。

flushAppendOnlyFile函数的行为由服务器配置的appendfsync选项的值来决定,各个不同值产生的行为如下表所示:

如果用户没有主动为appendfsync选项设置值,那么appendfsync选项的默认值为everysec。

  • always:效率最慢,最安全
  • everysec:每隔一秒就要在子现成中对AOF文件进行一次同步,从效率上来讲足够快,并且就算出现故障停机,数据库也只丢失一秒钟的命令数据
  • no:AOF文件写入速度最快,但是出现故障停机将丢失上次同步AOF文件之后的所有写命令数据

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

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

创建一个不带网络连接的伪客户端(fake client):因为Redis的命令只能在客户端上下文中执行,而载入AOF文件时所使用的命令直接来源于AOF文件而不是网络连接,所以服务器使用了一个没有网络连接的伪客户端来执行AOF文件保存的写命令,伪客户端执行命令的效果和带网络连接的客户端执行命令的效果完全一样。

3.3 AOF重写

重写是为了解决AOF文件体积膨胀的问题,Redis提供了AOF文件重写功能,可以创建一个新的AOF文件来替代现有的AOF文件。

3.3.1 AOF文件重写的实现

AOF重写并不需要对现有的AOF文件进行任何读取、分析或者写入操作,这个功能是通过读取服务器当前的数据库状态来实现的。首先从数据库中读取键现在的值,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令,这就是AOF重写功能的实现原理。

如:
redis> SADD animals "Cat"            
   // {"Cat"}
(integer) 1
redis> SADD animals "Dog" "Panda" "Tiger"    // {"Cat", "Dog", "Panda", "Tiger"}
(integer) 3
redis> SREM animals "Cat"                    // {"Dog", "Panda", "Tiger"}
(integer) 1
redis> SADD animals "Lion" "Cat"             // {"Dog", "Panda", "Tiger", "Lion", "Cat"}
(integer) 2 
                              
可以用:SADD animals"Dog""Panda""Tiger""Lion""Cat" 命令代替。

注意:在实际中,为了避免在执行命令时造成客户端输入缓冲区溢出,重写程序在处理列表、哈希表、集合、有序集合这四种可能会带有多个元素的键时,会先检查键所包含的元素数量,如果元素的数量超过了redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD常量的值,那么重写程序将使用多条命令来记录键的值,而不单单使用一条命令。在目前版本中,REDIS_AOF_REWRITE_ITEMS_PER_CMD常量的值为64,这也就是说,如果一个集合键包含了超过64个元素,那么重写程序会用多条SADD命令来记录这个集合,并且每条命令设置的元素数量也为64个。

3.3.2 AOF后台重写

aof的重写是也是放在子程序中进行。不过使用子进程也有一个问题需要解决,因为子进程在进行AOF重写期间,服务器进程还需要继续处理命令请求,而新的命令可能会对现有的数据库状态进行修改,从而使得服务器当前的数据库状态和重写后的AOF文件所保存的数据库状态不一致

为了解决这种数据不一致问题,Redis服务器设置了一个AOF重写缓冲区,这个缓冲区在服务器创建子进程之后开始使用,当Redis服务器执行完一个写命令之后,它会同时将这个写命令发送给AOF缓冲区和AOF重写缓冲区,如图所示:

这也就是说,在子进程执行AOF重写期间,服务器进程需要执行以下三个工作:

  • 执行客户端发来的命令;
  • 将执行后的写命令追加到AOF缓冲区;
  • 将执行后的写命令追加到AOF重写缓冲区。

这样一来可以保证:

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

当子进程完成AOF重写工作之后,它会向父进程发送一个信号,父进程在接到该信号之后,会调用一个信号处理函数,并执行以下工作:

  • 将AOF重写缓冲区中的所有内容写入到新AOF文件中,这时新AOF文件所保存的数据库状态将和服务器当前的数据库状态一致。
  • 对新的AOF文件进行改名,原子地(atomic)覆盖现有的AOF文件,完成新旧两个AOF文件的替换。

这个信号处理函数执行完毕之后,父进程就可以继续像往常一样接受命令请求了。

注意:在整个AOF后台重写过程中,只有信号处理函数执行时会对服务器进程(父进程)造成阻塞,在其他时候,AOF后台重写都不会阻塞父进程,这将AOF重写对服务器性能造成的影响降到了最低。

4、事件

Redis服务器是一个事件驱动程序,服务器需要处理以下两类事件:文件事件和时间事件。

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

4.1 文件事件

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

虽然文件事件处理器以单线程方式运行,但通过使用I/0多路复用程序来监听多个套接字。

4.1.1 文件事件处理器组成

文件事件处理器的四个组成部分,它们分别是套接字、I/O多路复用程序、文件事件分派器(dispatcher),以及事件处理器。如下图所示

  • 文件事件是对套接字操作的抽象,每当一个套接字准备好执行连接应答(accept)、写入、读取、关闭等操作时,就会产生一个文件事件。因为一个服务器通常会连接多个套接字,所以多个文件事件有可能会并发地出现。
  • I/O多路复用程序负责监听多个套接字,并向文件事件分派器传送那些产生了事件的套接字。
  • 尽管多个文件事件可能会并发地出现,但I/O多路复用程序总是会将所有产生事件的套接字都放到一个队列里面,然后通过这个队列,以有序(sequentially)、同步(synchronously)、每次一个套接字的方式向文件事件分派器传送套接字。

  • 文件事件分派器接收I/O多路复用程序传来的套接字,并根据套接字产生的事件的类型,调用相应的事件处理器;

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

Redis的I/O多路复用程序的所有功能都是通过包装常见的select、epoll、evport和kqueue这些I/O多路复用函数库来实现的,每个I/O多路复用函数库在Redis源码中都对应一个单独的文件,比如ae_select.c、ae_epoll.c、ae_kqueue.c,诸如此类。

因为Redis为每个I/O多路复用函数库都实现了相同的API,所以I/O多路复用程序的底层实现是可以互换,如图:

4.1.3 事件的类型

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

  • 当套接字变得可读时(客户端对套接字执行write操作,或者执行close操作),或者有新的可应答(acceptable)套接字出现时(客户端对服务器的监听套接字执行connect操作),套接字产生AE_READABLE事件;
  • 当套接字变得可写时(客户端对套接字执行read操作),套接字产生AE_WRITABLE事件。

I/O多路复用程序允许服务器同时监听套接字的AE_READABLE事件和AE_WRITABLE事件,如果一个套接字同时产生了这两种事件,那么文件事件分派器会优先处理AE_READABLE事件,等到AE_READABLE事件处理完之后,才处理AE_WRITABLE事件。

4.1.5 API

  • ae.c/aeCreateFileEvent函数接受一个套接字描述符、一个事件类型,以及一个事件处理器作为参数,将给定套接字的给定事件加入到I/O多路复用程序的监听范围之内,并对事件和事件处理器进行关联;
  • ae.c/aeDeleteFileEvent函数接受一个套接字描述符和一个监听事件类型作为参数,让I/O多路复用程序取消对给定套接字的给定事件的监听,并取消事件和事件处理器之间的关联;
  • ae.c/aeGetFileEvents函数接受一个套接字描述符,返回该套接字正在被监听的事件类型:
    • 如果套接字没有任何事件被监听,那么函数返回AE_NONE;
    • 如果套接字的读事件正在被监听,那么函数返回AE_READABLE;
    • 如果套接字的写事件正在被监听,那么函数返回AE_WRITABLE;
    • 如果套接字的读事件和写事件正在被监听,那么函数返回AE_READABLE|AE_WRITABLE。
  • ae.c/aeWait函数接受一个套接字描述符、一个事件类型和一个毫秒数为参数,在给定的时间内阻塞并等待套接字的给定类型事件产生,当事件成功产生,或者等待超时之后,函数返回
  • ae.c/aeApiPoll函数接受一个sys/time.h/struct timeval结构为参数,并在指定的时间內,阻塞并等待所有被aeCreateFileEvent函数设置为监听状态的套接字产生文件事件,当有至少一个事件产生,或者等待超时后,函数返回;
  • ae.c/aeProcessEvents函数是文件事件分派器,它先调用aeApiPoll函数来等待事件产生,然后遍历所有已产生的事件,并调用相应的事件处理器来处理这些事件;
  • ae.c/aeGetApiName函数返回I/O多路复用程序底层所使用的I/O多路复用函数库的名称:返回"select"表示底层为select函数库,诸如此类。

4.1.6 文件事件的处理器

包括连接应答处理器、命令请求处理器、命令回复处理器、复制处理器

4.2 时间事件

时间事件分为以下两类:

  • 定时事件:让一段程序在指定的时间之后执行一次。
  • 周期性事件:让一段程序每隔指定时间就执行一次。

时间事件主要由以下三个属性组成:

  • id:服务器为时间事件创建的全局唯一ID(标识号)。ID号按从小到大的顺序递增,新事件的ID号比旧事件的ID号要大;
  • when:毫秒精度的UNIX时间戳,记录了时间事件的到达(arrive)时间;
  • timeProc:时间事件处理器,一个函数。当时间事件到达时,服务器就会调用相应的处理器来处理事件。

一个时间事件是定时事件还是周期性事件取决于时间事件处理器的返回值:

  • 如果事件处理器返回ae.h/AE_NOMORE,那么这个事件为定时事件:该事件在达到一次之后就会被删除,之后不再到达;
  • 如果事件处理器返回一个非AE_NOMORE的整数值,那么这个事件为周期性时间:当一个时间事件到达之后,服务器会根据事件处理器返回的值,对时间事件的when属性进行更新,让这个事件在一段时间之后再次到达,并以这种方式一直更新并运行下去。比如说,如果一个时间事件的处理器返回整数值30,那么服务器应该对这个时间事件进行更新,让这个事件在30毫秒之后再次到达。(现在的Redis主要使用这个)

4.2.1 实现

服务器将所有时间事件放在一个无序链表(不按when属性的大小排序)中,每当时间事件执行器运行时,它就遍历整个链表,查找所有已到达的时间事件,并调用相应的时间处理器。

4.2.2 API

  • ae.c/aeCreateTimeEvent函数接受一个毫秒数milliseconds和一个时间事件处理器proc作为参数,将一个新的时间事件添加到服务器,这个新的时间事件将在当前时间的milliseconds毫秒之后到达,而事件的处理器为proc。
  • ae.c/aeDeleteFileEvent函数接受一个时间事件ID作为参数,然后从服务器中删除该ID所对应的时间事件;
  • ae.c/aeSearchNearestTimer函数返回到达时间距离当前时间最接近的那个时间事件;
  • ae.c/processTimeEvents函数是时间事件的执行器,这个函数会遍历所有已到达的时间事件,并调用这些事件的处理器。已到达指的是,时间事件的when属性记录的UNIX时间戳等于或小于当前时间的UNIX时间戳。

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

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

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

4.2.4 事件的调度与执行

  • 文件事件和时间事件之间是合作关系,服务器会轮流处理这两种事件,并且处理事件的过程中也不会进行抢占。
  • 时间事件的实际处理时间通常会比设定的到达时间晚一些。

5、客户端

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

5.1 客户端属性

        客户端属性分为两种,通用的属性:无论客户端执行什么工作,都会用到这些属性。 特殊功能的属性:这是一类和特殊功能相关的属性。下面是通用属性介绍。

5.1.2 套接字描述符

         fd 属性记录了客户端正在使用的套接字描述符。

  • 伪客户端: fd 的值是 -1,来源于 AOF 文件并还原数据库状态,执行 Lua 脚本中包含的 Redis;
  • 普通客户端:普通客户端的套接字描述符的值必须是大于 -1 的值。

5.1.2 名字

        在默认无情况下,一个连接到服务武器的客户端是没有名字的,只有在使用 CLIENT setname 的时候,客户端才会有一个名字,让客户端的身份变得更清晰,这个属性记录在客户端状态的name属性中。

5.1.3 标志

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

  • REDIS_MASTER: 主服务器
  • REDIS_SLAVE: 从服务器

5.1.3 输入缓冲区

        输入缓冲区在 redisClient 的 querybuf 中记录,输入缓冲区的大小会根据输入内容的大小动态的缩小或者扩大,但它的最大不能超过 1GB ,否则服务器会将这个客户端关闭。

5.1.4 命令与命令参数

        服务器将客户端发出的请求保存到 querybuf 中之后,redis 服务器会对放出请求的命令内容进行分析,并将得出的命令参数和命令参数的个数分别保存到 argv 和 argc 中。argv 属性是一个数组,其中 argv[ 0 ] 放的是要执行的命令,而其之后存放的是参数。

5.1.5 命令的实现函数

        执行一条命令其实就是执行服务器的一个函数,redis 中有一个命令列表,其中存放着 redis 服务器的所有命令,这个命令列表是一个字典表,key 为命令的名称,而 value 存放的是一个 redisCommand 结构,当服务器分析并得到 argv 和 argc 属性值后,服务器会根据 argv[ 0 ] 来找到命令列表中所对应的函数结构,然后将服务器状态中的 cmd 属性指向这个结构,之后服务器就可以通过这些信息,调用实现函数,执行客户端发出的请求命令。

5.1.6 输出缓冲区

         执行完命令之后,服务器会返回一个命令回复,这个回复就保存在客户端状态的输出缓冲区内。每个客户端都有两个缓冲区可用,一个大小固定的,一个大小可变的。大小固定的存放的是那些长度比较小的,比如 OK、简单的字符串;大小可变得存放的是一些比较复杂的回复。

5.1.7身份验证

        客户端状态的 authenticated 属性记录了客户端是否通过了身份验证:如果其值为 0 ,那么表示客户端未通过身份验证,这种客户端只能执行AUTO命令,其他所有的命令都会被服务器拒绝,客户端通过该命令通过身份验证之后,这个值就会变为 1。

5.1.8 时间

        ctime记录了客户端创建的时间,lastinteraction 属性记录了客户端与服务器最后一次进行交互的时间。

5.2 客户端的创建与关闭

        服务器使用两种模式限制客户端输出缓冲区的大小:

  • 硬性限制:如果输出缓冲区的大小超过了硬性限制所设置的大小,那么服务器立即关闭客户端
  • 软性限制:客户端在一定时间内,一直超过服务器设置的软性限制,那么客户端也会被关闭

        当一个客户端通过网络连接连上服务器时,服务器会为这个客户创建相应的客户端状态。网络连接关闭、发送了不合协议格式的命令请求、成为CLIENT KILL命令的目标、空转时间超时、输出缓冲区的大小超出限制等都会造成客户端被关闭。

        处理lua脚本的伪客户端在服务器初始化时创建,这个客户端会一直存在,直到服务器关闭。

        载入AOF文件时使用的伪客户端在载入工作开始时动态创建,载入工作完毕之后关闭。

6、服务器

6.1 命令请求的执行过程

6.1.1 发送命令请求

        Redis 服务器的请求命令来自 Redis客户端,当用户键入一条命令的时候,客户端会将这个命令请求转换成协议格式,然后通过连接到服务器的套接字,将协议格式的命令请求发送给服务器。

6.1.2 读取命令请求

  1. 读取套接字中协议格式的命令请求,并保存到输入缓冲区内。
  2. 对输入缓冲区内的命令进行解析,获得命令请求参数和参数的个数,并分别将参数和参数的个数放到 argv 和 argc 中。
  3. 调用命令执行器。

6.1.3 命令执行器(1):查找命令实现

        获得 argv[ 0 ] 内的命令,在命令列表中找到对应的命令,并将找到的命令保存到客户端状态的 cmd 属性里面。redisCommand结构如下:

typedef struct redisCommend{

  name;//命令名字

  proc;函数指针,指向命令的实现函数。

  arity;//命令参数的个数,检查命令请求的格式是否正确

  sflags;//命令属性,比如这个命令是写命令还是读命令

   flags;//对sflags标识符进行分析得出的二进制标识

   calls;//服务器总共执行了多少次命令

   milliseonds;//服务器执行这个命令所消耗的总时长

}

备注:命令名字的大小写不影响命令表的查找结果

6.1.4 命令执行器(2):执行预备操作

真正执行命令之前,还需要预备操作:

  • 检查客户端状态的cmd指针是否指向NULL
  • 根据cmd属性指向的redisCommend结构的arity属性,检查命令请求所给定的参数格式是否正确。如果 arity属性为 -3,那么用户输入的命令参数个数必须大于等于三个
  • 检查客户端是否已经通过了身份验证,没有通过验证的客户端只能执行AUTH命令,执行其他命令,服务器会向客户端返回一个错误。
  • 如果服务器打开了maxmemory功能,在执行命令前,还要先检查服务器的内存占用情况,在有需要时进行内存回收
  • ............

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

        当服务器需要执行命令时,只需要执行以下语句:

        client ----> cmd ---->proc(client)。等于执行语句:setCommend(client);

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

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

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

6.1.7 将命令回复发送给客户端

        命令实现函数会把命令回复保存到客户端的输出缓冲区中,并为客户端的套接字关联命令回复处理器,当套接字变为可写状态时,服务器就会执行命令回复处理器,将保存在客户端输出缓冲区中的命令回复发送给客户端。当命令回复发送完毕后,回复处理器会清空客户端状态的输出缓冲区。

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

当客户端接收到协议格式的命令回复后,把这些回复转换成人可读的格式,一般是“ok”。

6.2 serverCron函数

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

struct redisServer(
	//...
    
    //减少系统调用的执行次数,但精确度并不高(更新服务器的时间缓存)
    time_t unixtime;//保存了秒级精度的系统当前UNIX时间戳    
    long long mstime;//保存了毫秒级精度的系统当前unix时间戳

    //默认每10秒更新一次的时钟缓存,用于计算键空转时间(更新LRU时钟)
    unsigned lruclock:22; 

    //(更新服务器每秒执行次数)
    long long ops_sec_last_sample_time;//上一次的抽样时间    
    long long ops_sec_last_sample;//上一次抽样时,服务器已经执行的命令个数    
    long long ops_sec_samples[REDIS_OPS_SEC_SAMPLES]//REDIS_OPS_SEC_SAMPLES大小的环形数组(默认为16),数组中的每个元素都是一次抽样结果,即会保留前16次的抽样结果    
    int ops_sec_idx;//ops_sec_samples的当前最大索引值,每次抽样后,该值自增1,然后将抽样结果放入ops_sec_samples数组中,在值自增后等于16就要重置为0,注意此时并不会将后面的数据清0,也就是保存了上一组的记录,形成了一个环形数组    

    //(更新服务器内存峰值记录)
    size_t stat_peak_memory;//已经使用的内存峰值

    //在启动服务器时,Redis会为服务器进程的SIGTETM信号关联处理器sigtermHandler函数,这个信号处理器负责在服务器接过SIGTERM信号时,打开服务器状态的shutdown_asap标识,该标识的作用是用来确认服务器是否关机的。用SIGTETM信号去触发关机,是让Redis可以有时间去进行持久化准备(处理SIGTETM信号)
    int shutdown_asap;//关闭服务器的标识:值为1时,关闭服务器,值为0时,不做动作

    //(管理数据库的资源)主要是删除过期资源,serverCron函数每次执行都会去调用databasesCron函数,该函数会对服务器中的一部分数据进行检查,删除其中的过期键,并且在有需要的时候,对字典进行收缩操作

    //(执行被延迟的BGREWRITEAOF)    
    int aof_rewrite_scheduled;//值为1,表示有BGREWRITEAOF命令被延迟了

    //(检查持久化操作的运行状态)    
    pit_t rdb_child_pid;//BGSAVE是否正在执行,-1表示没有 
    pit_t aof_child_pid;//BGREWRITEAOF是否正在执行,-1表示没有

    //cronloops值,记录执行了多少次serverCron
    int cronloops;

    //...
);

6.3 初始化服务器

6.3.1初始化服务器状态结构

        初始化 server 变量的工作由 redis.c/initServerConfig 函数完成,initServerConfig函数完成的主要工作:

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

6.3.2 载入配置文件

        在启动 redis 服务器的时候,我们可以通过修改服务器的给定的一些配置参数(端口号)或者指定配置文件(数据库数量、文件压缩功能等)来修改服务器的默认配置。

6.3.3 初始化服务器数据结构

        在执行 initServerConfig 函数初始化 server 状态时,程序只键了命令表一个数据结构,实际上 redis 还有很多的数据结构。比如:

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

          当初始化服务器进行到这一步的时候,服务器将调用 initServer 函数,为以上提到的数据结构分配内存,并在需要的时候,为这些数据结构设置或者关联初始化值。服务器到现在才初始化数据结构的原因在于,服务器必须先载入用户指定的配置选项,才能正确的对数据结构进行初始化,如果服务器在进行 initServerConfig 的时候就对数据结构初始化完毕,那么一旦用户通过配置选项修改了和数据结构相关的服务状态属性,服务器就需要重新修改调整已创建的数据结构,这样是会影响效率的。

6.3.4 还原数据库状态

        在完成对服务器状态 server 变量的初始化之后,服务器需要载入 RDB 文件或者 AOF 文件(AOF优先),并根据文件记录的内容来还原服务器的数据库状态。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值