《Redis设计与实现》笔记2—单机数据库的实现

一、数据库

1、服务器中的数据库

Redis服务器将所有数据库都保存在服务器状态redis.h/redisServer结构的db数组中,db数组的每个项都是一个redis.h/redisDb结构,每个redisDb结构代表一个数据库;在初始化服务器时,程序会根据服务器状态的dbnum属性来决定创建多少个数据库,该属性由服务器配置的database选项决定,默认情况下该属性值为16

2、切换数据库

默认情况下,Redis客户端的目标数据库为0号数据库,但是用户可以通过执行Select命令来切换目标数据库;在服务器内部,客户端状态redisClient结构的db属性记录了客户端当前的目标数据库,该属性是一个指向redisDb结构的指针,它所指向的元素就是客户端的目标数据库;通过redisClient.db指针,让它指向服务器中的不同数据库,从而实现切换目标数据库的功能,这也是Select命令的实现原理

3、数据库键空间

Redis是一个键值对数据库服务器,服务器中的每个数据库都由一个redis.h/redisDb结构表示,redisDb结构的dict字典保存了数据库中的所有键值对,而这个字典也称为键空间(key space);键空间和用户所见的数据库是直接对应的:

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

所有针对数据库的操作,实际上都是通过对键空间字典进行操作来实现的

1、其它键空间操作

除了常用的增删改查操作外,还有很多针对数据库本身的Redis命令,也是通过对键空间进行处理来完成的。比如:

  • FlushDb:通过删除键空间中的所有键值对来实现;
  • RandomKey:通过在键空间中随机返回一个键来实现;
  • DbSize:通过返回键空间中包含的键值对的数量来实现;
  • 类似的还有Exists、ReName、Keys等命令

2、读写键空间时的维护操作

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

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

4、设置键的生存时间或过期时间

1、简介

  • 通过Expire命令或Pexpire命令,客户端可以以秒或毫秒精度为数据库中的某个键设置生存时间(SetEX命令可以设置一个字符串键的同时为键设置过期时间);
  • 与Expire命令或Pexpire命令类似,客户端可以使用ExpireAT命令或PexpireAT命令,以秒或毫秒精度给数据库中的某个键设置过期时间,过期时间是一个Unix时间戳;
  • TTL命令或PTTL命令接受一个带有生存时间或过期时间的键,它们会返回这个键的剩余生存时间

2、设置过期时间

Redis有四个不同的命令可以用于设置键的生存时间或过期时间:

  • Expire命令用于将键的生存时间设置为ttl秒;
  • Pexpire命令用于将键的生存时间设置为ttl毫秒;
  • ExpireAt命令用于将键的过期时间设置为timestamp所指定的秒数时间戳;
  • PexpireAt命令用于将键的过期时间设置为timestamp所指定的毫秒数时间戳;

但实际上,Expire、Pexpire、ExpireAt命令最终都是转换成PexpireAt命令来执行的;

3、保存过期时间

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

过期字典是一个指针,它会指向键空间的某个键对象(也就是某个数据库键);

过期字典的值是一个long long类型的整数,它保存了键所指向数据库键的过期时间(一个毫秒精度的UNIX时间戳);

4、移除过期时间

Persist命令可以移除一个键的过期时间,它是PexpireAt命令的反操作,它会在过期字典中查找给定的键,并解除键和值在过期字典中的关联(如果键不存在或没有设置过期时间,系统会忽略该命令)

5、计算并返回剩余生存时间

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

6、过期键的判定

通过过期字典,程序可以使用以下步骤检查一个键是否过期:

检查该键是否存在于过期字典,如果存在则取得该键的过期时间;

检查当前的UNIX时间戳是否大于键的过期时间,如大于则键以过期,否则键未过期;

5、过期键删除策略

如果一个键过期了,那么它什么时候会被删除呢?如下,代表了三种不同的策略,第一第三种为主动删除策略,第二种为被动删除策略:

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

1、定期删除

定期删除是对内存最友好的,它的缺点是对CPU不友好;在过期键比较多的情况下,删除过期键的行为会占用一部分CPU资源,这无疑会对服务器的响应时间和吞吐量造成影响;

另外,创建一个定时器需要用到Redis服务器中的时间事件,当前时间事件的实现方式为无序链表,即查找一个事件的时间复杂度为O(N),导致它不能高效的处理大量的时间事件;

2、惰性删除

惰性删除对CPU时间来说是最友好的,它的缺点是对内存不友好;如果数据库中有非常多的过期键,而这些过期键又恰好没有被访问到,那么它们将永远不会被删除,这种情况可以看作是一种内存泄漏,因为大量无用的数据占用了内存

3、定期删除

定期删除是基于前面两种策略的折中版本,它会每隔一段时间执行一次删除过期键的操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响;并且它有效减少了因为过期键而带来的内存浪费问题;

定时删除的难点在于确认删除操作执行的时长和频率,所以服务器必须根据情况,合理地设置删除操作的执行时长和执行频率;

6、Redis的过期键删除策略

Redis服务器实际使用的是惰性删除和定期删除两种策略,通过两种策略的配合使用,服务器能在CPU的内存资源间达到很好的平衡

1、惰性删除策略的实现

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

  • 如果输入键已经过期,expireIfNeeded函数会将输入键从数据库中删除;
  • 如果输入键未过期,expireIfNeeded函数不做任何动作;

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

2、定期删除策略的实现

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

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

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

提前须知:由于Redis的数据都存放在内存中,如果没有配置持久化,redis重启后数据就全丢失了,于是需要开启redis的持久化功能,将数据保存到磁盘上,当redis重启后,可以从磁盘中恢复数据。redis提供两种方式进行持久化,一种是RDB持久化(原理是将Reids在内存中的数据库记录定时dump到磁盘上),另外一种是AOF持久化(原理是将Reids的操作日志以追加的方式写入文件)

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

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

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

3、当服务器以AOF持久化模式运行时,如果数据库中的某个键已经过期,但它还没有被惰性删除或者定期删除,那么AOF文件不会因为这个过期键产生任何影响;

当过期键被惰性删除或者定期删除后,程序会向AOF文件追加一条命令,来显式地记录该键已经被删除;

4、在执行AOF重写过程中,程序会对数据库中的键进行检查,已过期的键不会被保存到重写后的AOF文件中,因此数据库中包含过期键不会对AOF重写造成影响

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

  • 主服务器删除一个过期键后,会显式地向所有从服务器发送一个DEL命令,告知从服务器删除这个过期键;
  • 从服务器在执行客户端发送的读命令时,碰到过期键不会做删除操作,而是像未过期键一样进行处理;
  • 从服务器只有在接收到主服务器发送来的DEL命令后,才会删除过期键;

8、数据库通知

1、该功能是Redis2.8中的新增的,它可以让客户端通过订阅给定的频道或模式,来获知数据库中键的变化,以及数据库中命令的执行情况;这类关注“某个键执行了什么命令”的通知称为键空间通知(key-space notification),还有一类称为键事件通知(key-event notifcation),它关注的是某个命令被什么键执行了

2、服务器配置的notify-keyspace-events选项决定了服务器所发送通知的类型:

  • 服务器发送所有类型的键空间通知和键事件通知,可以将选项的值设置为AKE;
  • 服务器发送所有类型的键空间通知,可以将选项的值设置为AK;
  • 服务器发送所有类型的键事件通知,可以将选项的值设置为AE;
  • 服务器发送和字符串键有关的键空间通知,可以将选项的值设置为K$;
  • 服务器发送和列表键有关的键事件通知,可以将选项设置为E1;

3、发送数据库通知的功能由notify.c/notifyKeyspaceEvent函数实现:void notifyKeyspaceEvent(int type,char *event,robj *key,int dbid);

  • type参数是当前要发送的通知的类型,它会匹配notify-keyspace-events选项中的通知类型,来决定是否发送通知;
  • event、keys、dbid分别是事件的名称、产生事件的键、产生事件的数据库号码

二、RDB持久化

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

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

1、RDB文件的创建与载入

1、有两个命令可以用于生成RDB文件,一个是SAVE,一个是BGSAVE;创建RDB文件的工作由rdb.c/rdbSave函数完成,SAVE命令和BGSAVE命令会以不同的方式调用这个函数

  • SAVE命令会阻塞Redis服务器进程,直到RDB文件创建完毕为止;
  • BGSAVE命令会派生出一个子进程,然后由子进程负责创建RDB文件,服务器进程继续处理命令请求;

2、RDB文件的载入工作是在服务器启动时自动执行的,所以服务器没有专门用于载入RDB文件的命令,只要服务器在启动时检测到RDB文件存在,它就会自动载入RDB文件;需要注意的是,AOF文件的更新频率通常比RDB文件的更新频率高:

  • 如果服务器开启了AOF持久化功能,那么服务器会优先使用AOF文件来还原数据库状态;
  • 只有在AOF持久化功能处于关闭状态,服务器才会使用RDB文件来还原数据库状态

3、①SAVE命令执行时,Redis服务器会被阻塞,所以SAVE命令执行时,客户端发送所有命令请求都会被阻塞;

②BGSAVE命令执行期间,客户端发送SAVE或BGSAVE命令会被服务器拒绝,防止产生竞争关系;

③BGRewaitAOF和BGSAVE两个命令不能同时执行,主要是从性能方面考虑,杜绝同时执行大量的磁盘读写操作;

④服务器在载入RDB文件期间,会一直处于阻塞状态,直到工作完成;

2、自动间隔性保存

BGSAVE命令可以在不阻塞服务器进程的情况下执行,所以Redis允许用户通过配置服务器的save选项,让服务器每隔一段时间自动执行一次BGSAVE命令。用户可以通过save选项设置多个保存条件,但只要其中任意一个条件被满足,服务器就会执行BGSAVE命令。

1、设置保存条件

Redis服务启动时,用户可以通过指定配置文件或传入启动参数的方式设置save选项,如果用户没有主动设置save选项,那么服务器会为save选项设置默认条件,服务器程序会根据save选项设置的保存条件,设置服务器状态redisServer结构中的saveparams属性。该属性是一个保存了多个saveparam结构的数组,而saveparam结构都保存了一个save选项设置的保存条件,示例如下:

2、dirty计数器和lastsave属性

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

  • dirty计数器记录距离上一次成功执行SAVE命令或者BGSAVE命令之后,服务器对数据库状态(服务器中的所有数据库)进行了多少次修改(包括增删改操作);
  • lastsave属性是一个UNIX时间戳,记录了服务器上一次成功执行SAVE命令或BGSAVE命令的时间;

命令修改了多少次数据库,dirty计数器的值就增加多少,比如一次增加了3比数据,那么dirty计数器的值就会增加3

3、检查保存条件是否被满足

Redis服务器的周期性操作函数serverCron默认每隔100毫秒就会执行一次,该函数用于对正在运行的服务器进行维护,它的其中一项工作就是检查save选项所设置的保存条件,当saveparams数组中保存的条件有任意一个被满足,就会执行BGSAVE命令。执行完成后dirty计数器会被重置为0,lastsave属性也会变更为lastsave执行完成后的时间

3、RDB文件结构

一个完整的EDB文件包含以下几个部分(为了方便区分,常量为全大写,变量和数据全小写)

  • REDIS长度为5个字节,保存着REDIS5个字符,通过这5个字符,程序可以在载入文件时,快速检查所载入的文件是否为RDB文件(注意:RDB文件保存的是二进制数据,而不是C字符串,所以REDIS仅代表5个字符);
  • db_version长度为4个字节,它是一个字符串表示的整数数值,表示的是文件的版本号;
  • database部分包含着零个或任意多个数据库,以及各个数据库中的键值对数据:①如果服务器的数据库状态为空,那么该部分也会为空,长度为0字节;②如果服务器状态为非空,那么该部分为非空,其长度受保存数据的数量、类型和内容的影响;
  • EOF常量的长度为1字节,这个常量标志着RDB文件正文内容的结束,程序读入到它的时候,数据库的所有键值对都已经载入完毕了;
  • check_sum是一个8字节长的无符号整数,保存着一个校验和,这个校验和是程序通过前4个部分的内容计算得出的,服务器在载入RDB文件时,会将载入数据所计算出的校验与check_sum进行对比,来检查RDB文件是否有出错或损坏

1、database部分

一个RDB文件的database部分可以保存任意多个非空数据库,而每个非空数据库在RDB文件中都可以保存为SELECTDB、db_number、key_value_pairs三个部分:

  • SELECTDB常量的长度为1字节,当读入程序遇到这个值的时候,它知道接下来要读入的将是一个数据库号码;
  • db_number保存的是一个数据库号码,根据号码长度的不同,其长度可以是1字节、2字节或5字节,程序在读入db_number后会调用SELECT命令,根据读入的数据库号码进行数据库切换
  • key_value_pairs保存的了数据库中所有键值对数据,如果键值对包含过期时间,那么过期时间也会和键值对保存在一起

如下为一个包含了0号和3号数据库的RDB文件:

2、key_value_pairs部分

RDB文件中的每个key_value_pairs部分都保存了一个或以上的键值对,带过期时间的键值对会保存过期时间,不带过期时间的键值对在RDB文件中由Type、key、value三部分组成;其中Type记录了value的类型,长度为1字节,值可以是以下常量的一个:

带过期时间的键值对会多ExpireTime_MS和ms两项:

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

3、key_value_pairs的value编码

根据Type的不同,value部分的结构、长度也会有所不同:

①字符串对象:如果Type的值为Redis_RDB_Type_String,那么value保存的就是一个字符串对象,字符串对象的字符串编码可以是REDIS_Encoding_INT或者REDIS_Encoding_RAW;如果对象保存的是长度不超过32位的整数,那么对象的编码可以为REDIS_Encoding_INT,否则其编码为REDIS_Encoding_RAW;而根据字符串长度是否大于20字节,有压缩和不压缩两种方法来保存这个字符串;没有被压缩的字符串其结构为len和string,len为字符串的长度,string为字符串本身;被压缩的字符串其结构如下:

  • REDIS_RDB_ENC_LZF常量标志着字符串已经被LZF算法压缩过了;compressed_len记录的是字符串被压缩之后的长度,origin_len记录的是字符串原来的长度,compressed_string记录的是被压缩后的字符串

②列表对象:如果Type的值为Redis_RDB_Type_List,那么value保存的就是一个REDIS_Encoding_LinkedList编码的列表对象;RDB文件保存这种对象的结构为list_length和Item;list_length记录了列表的长度,它记录保存了多少个item,示例如下:

③集合对象:如果Type的值为Redis_RDB_Type_Set,那么value保存的就是一个REDIS_Encoding_HT编码的集合对象;RDB文件保存这种对象的结构为set_size和elem;set_size记录了集合保存的元素个数,即保存了多少个elem,示例如下:

④哈希表对象:如果Type的值为Redis_RDB_Type_HASH,那么value保存的就是一个REDIS_Encoding_HT编码的集合对象;RDB文件保存这种对象的结构为hah_size和key_value_pair;hash_size记录了哈希表的大小,即保存了多少个key_value_pair,示例如下:

⑤有序集合对象:如果Type的值为Redis_RDB_Type_ZSet,那么value保存的就是一个REDIS_Encoding_SkipList编码的有序集合对象;RDB文件保存这种对象的结构为sorted_set_size和element;sorted_set_size记录了集合的个数,即element的个数;element代表有序集合中的元素,每个元素又分为成员member和分值score,member是一个字符串对象,score是一个double型的浮点数,保存时会作为字符串对象保存,示例如下:

⑥整数集合对象:如果Type的值为Redis_RDB_Type_IntSet,那么value保存的就是一个整数集合对象;RDB文件保存这种对象的方式是将整数集合转换为字符串对象,然后进行保存,在读入时,会根据Type再转换为整数集合对象;

⑦ZIPList编码的列表、哈希表或有序集合:如果Type的值为Redis_RDB_Type_List_ZIPList,Redis_RDB_Type_HASH_ZIPList或者Redis_RDB_Type_ZSet_ZIPList,那么value保存的就是一个压缩列表对象;RDB文件保存这种对象的方式是将其转换为字符串对象,然后进行保存,在读入时,会根据Type再转换为原先的对象类型;

4、分析RDB文件

通常情况下,我们会使用od命令来分析Redis服务器产生的RDB文件,该命令可以用给定的格式转存并打印输入文件;如-c会以ASCII编码的方式打印输入文件,-x会以十六进制的方式打印输入文件

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

使用flushAll命令(慎用!)和save命令创建一个状态为空的RDB文件,使用od命令打印RDB文件,示例如下:

2、包含字符串键的RDB文件

新增一个字符串键set MSG HELLO后执行od命令,示例如下:

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

新增一个带过期时间的字符串键set msg 10086 hello后执行od命令,示例如下:

4、包含一个集合键的RDB文件

新增一个集合键sadd lang c java ruby后执行od命令,示例如下:

5、关于分析RDB文件的说明

Redis本身带有RDB文件检查工具redis-check-dump,所以分析RDB文件内容并不是必须要掌握的内容;对于RDB中的数字值,使用-cx参数调用od命令其输出会更加直观

三、AOF持久化

除了RDB持久化功能之外,Redis还提供了AOF(Append Only File)持久化功能,与RDB持久化通过保存数据库中的键值对来记录数据库状态不同,AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态的,如下图:

以添加3个键值对为例,RDB持久化保存数据库的方式是将添加的3个键值对保存到RDB文件中;而AOF则是将添加键值对的3个命令保存到AOF文件中 ,被写入AOF文件的所有命令都是以Redis的命令请求协议格式保存的,其格式是纯文本的;AOF文件除了用于指定数据库的SELECT命令是服务器自动添加的之外,其它都是我们通过客户端发送的命令

1、AOF持久化的实现

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

  • 当AOF持久化功能处于打开状态时,服务器再执行完一个写命令后,会以协议格式将被执行的写命令追加到服务器的aof_buf缓冲区的末尾;
  • Redis的服务器进程是一个时间循环,循环中的文件事件负责接受客户端的命令请求,以及向客户端发送命令回复;而时间事件负责执行像serverCron函数这样需要定时运行的函数;服务器每次结束一个事件循环之前,都会调用flushAppendOnlyFile函数,考虑是否将aof_buf缓冲区中的内存写入保存到AOF文件中,该函数的同步行为由服务器配置的appendfsync选项决定(默认为everysec),其配置会决定AOF持久化的效率和安全性:

2、AOF文件的载入与数据还原

AOF文件保存了重建数据库所需的所有写命令,所以服务器只要读入并重写执行一遍AOF文件中的写命令,就可以还原服务器关闭前的数据库状态,其详细步骤如下:

  1. 因为Redis命令只能在客户端上下文中执行,所以首先会创建一个不带网络连接行为的伪客户端,其执行效果与带网络连接的一样;
  2. 从AOF文件中分析并取出一条写命令,使用伪客户端执行被读出的写命令;
  3. 循环执行第二步直至AOF文件中的所有写命令都被处理完毕为止

3、AOF重写

随着服务器运行时间的增长,AOF文件中的内容会越来越多,如果不加以控制,会对Redis服务器甚至宿主机造成影响,并且AOF文件的体积越大,使用AOF文件进行数据还原所需的时间就越多;为了应对此类情况,Redis提供了AOF重写功能,Redis服务器会创建一个新的AOF文件取代旧的,它不包含浪费空间的冗余命令,所以体积会小很多

1、AOF文件重写的实现

AOF重写操作并不会对现有的AOF文件进行操作,它是通过读取服务器当前的状态来实现的。它会从数据库中读取键现在的值,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令。因为aof_rewrite函数生成的新AOF文件只包含还原当前数据库状态所需的命令,所以不会浪费任何硬盘空间。

为避免执行重写命令时造成客户端输入缓冲区溢出,重写程序在处理列表、哈希表、集合、有序集合这四种可能带有多个元素的键时,会先检查键所包含的元素数量,如果元素的数量大于redis.h/REDIS_AOF_REWRITE_ITIMES_PER_CMD常量的值,那么重写程序会将命令拆分为多条,每条命令元素数量为64位

2、AOF后台重写(BGReWriteAOF)

1、重写函数会进行大量的写操作,调用函数的进程会被长时间阻塞,而服务器又使用单个线程来处理命令请求,所以为避免在重写AOF文件期间,服务器无法处理客户端发来的命令请求,Redis会将AOF重写程序放到子进程中执行,可以达到两个目的:

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

2、子进程在重写AOF文件期间,服务器进程还要继续处理请求命令,而新的命令会对数据库状态进行修改,从而使得服务器数据库状态和重写后的AOF文件不一致。为应对这类情况,Redis服务器设置了一个AOF重写缓冲区,缓冲区会在创建子线程后就开始工作,Redis服务器执行完写命令后,会将写命令发送给AOF缓冲区和AOF重写缓冲区,从而解决上述问题

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

  • 将AOF重写缓冲区的内容写入到AOF文件中,保证AOF文件保存的数据库状态与当前服务器一致;
  • 将新的AOF文件进行改名,原子地(atomic)覆盖现有的AOF文件,完成替换后,父进程开始继续接受请求命令

在AOF重写过程中,只有信号处理函数对服务器进程造成了阻塞,将性能影响降到了最低

举例说明如下:

四、事件

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

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

1、文件事件

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

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

文件事件处理器以单线程的方式运行,但通过使用I/O多路复用程序来监听多个套接字,即实现了高性能的网络通信模型,又可以很好地与Redis服务器中其他同样以单线程方式运行的模块进行对接,保持了Redis内部单线程设计的简单性

1、文件事件处理器的构成

文件事件处理器由套接字、I/O多路复用程序、文件事件分派器、事件处理器组成

I/O多路复用程序负责监听多个套接字,并将那些产生了事件的套接字以有序、同步、每次一个套接字的方式传送给文件事件分派器,当一个套接字产生的事件被处理完毕后,I/O多路复用程序才继续向文件事件分派器传送下一个套接字;不同任务的套接字会关联不同的事件处理器,这些处理器是一个个的函数,它们定义了某个事件发生时,服务器应该执行的动作

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

Redis的I/O多路复用程序的所有功能都是通过包装常见的select、epoll、evport和kqueue这些I/O多路复用函数库来实现的,Redis为每个I/O多路复用函数库实现了相同的API,所以I/O多路复用程序的底层实现是可以交换的,如下图:

3、事件的类型

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

  • 当套接字变得可读时(客户端对套接字执行write操作或close操作),或者有新的可应答套接字出现时,套接字产生AE_Writeable事件
  • 当套接字变得可写时(客户端对套接字执行read操作),套接字产生AE_Writeable事件

I/O多路复用程序允许服务器同时监听套接字的AE_Readable和AE_Writeable事件,如果同一套接字产生了这两种事件,那么文件事件分派器会优先处理AE_Readable事件,处理完成后再处理AE_Writeable事件,及服务器会先处理读再处理写

4、API

5、文件事件的处理器

Redis为文件事件编写了多个处理器,分别用于实现不同的网络通信需求,比如最常用的与客户端进行通信的连接应答处理器、命令请求处理器和命令回复处理器,如下图:

2、时间事件

Redis的时间事件分为以下两类:

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

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

  • id:服务器为时间事件创建的全局唯一Id,Id号按从小到大的顺序递增;
  • when:UNIX时间戳,毫秒精度,记录时间事件的到达时间;
  • timeProc:时间事件处理器,一个函数;时间事件到达时,服务器就会调用相应的处理器来处理事件;

一个事件是定时事件还是周期性事件取决于时间事件处理器的返回值:如果事件处理器返回ae.h/AE_NOMORE,事件则为定时事件;否则为周期性事件,事件到达后,会对when属性进行更新;目前版本(本书的版本)的Redis只使用周期性事件

1、实现

服务器将所有时间事件都放在一个无序链表中(无序是针对when属性而言),每当时间事件执行器允许时,它就遍历整个链表,查找所有已到达的时间事件,并调用相应的事件处理器;并且新的时间时间总是插入到链表的表头,如下:

2、API

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

3、时间事件的应用实例:serverCron函数

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

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

Redis服务器以周期性事件的方式来运行serverCron函数,在服务器运行期间,每隔一段时间,serverCron函数就会执行一次,知道服务器关闭;用户可以通过修改redis.conf中的hz选项来调整serverCron每秒的执行次数

3、事件的调度与执行

服务器同时存在文件事件和时间时间两种,它们的调度和执行由ae.c/aeProcessEvents函数负责,流程如下:

文件事件和时间事件的处理都是同步、有序、原子地执行,不会中途中断事件处理,也不会对事件进行抢占;时间事件在文件事件之后执行,并且事件之间不会出现抢占,所以时间事件的实际处理时间,通常会比时间事件设定的到达时间稍晚一点

五、客户端

Redis服务器是典型的一对多服务器程序,一个服务器可以与多个客户端建立网络连接,并为这些客户端建立相应的redis.h/redisServer结构,该结构保存了客户端当前的状态信息,以及执行相关功能时需要用到的数据结构,其中包括:

Redis服务器存在一个链表结构的clients属性,该链表保存了所有与服务器连接的客户端的状态结构,对客户端进行批量操作或查找操作都可以通过遍历clients链表来完成

1、客户端属性

客户端状态的属性可以分为两类:

  • 一类是通用的属性,很少与特定功能相关,无论执行什么操作,都要用到这些属性;
  • 一类是与特定功能相关的属性,比如操作数据库时需要用到的db属性和dictid属性,执行事务操作用到的mstate属性等;

本章介绍的是部分通用的属性

1、套接字描述符

客户端状态的fd属性记录了客户端正在使用的套接字描述符,根据客户端类型的不同,fd属性的值可以是-1或大于-1的整数:

  • 伪客户端的fd属性为-1,伪客户端处理的命令请求来源于AOF文件或Lua脚本,不是网络,所以不需要套接字连接,也不需要套接字描述符;
  • 普通客户端的fd属性的值为大于-1的整数,普通客户端会使用套接字来与服务器进行通信,所以服务器使用fd属性来记录客户端套接字的描述符;

2、名字

默认情况下,一个连接到服务器的客户端是没有名字的,但是可以使用Client setname命令为客户端设置一个名字;

3、标志

客户端的标志属性flags记录了客户端的角色以及客户端目前所处的状态,flags的值可以是单个标志也可以是多个用或连接的标志,每个标志使用一个常量表示;其中一部分标志记录了客户端的角色,另一部分标记则记录了客户端目前所处的状态;示例如下:

4、输入缓冲区

客户端状态的输入缓冲区(querybuf)用于保存客户端发送的命令请求,输入缓冲区的大小会根据输入内容动态的缩小或扩大,但它的最大大小不能超过1G,否则服务器将关闭这个客户端

5、命令与命令参数

在服务器将客户端发送的命令请求保存到客户端状态的querybuf属性后,服务器会对请求的内容进行分析,并将得出的命令参数以及命令参数的个数分别保存到客户端的argv属性和argc属性;

  • argv属性是一个数组,数组中的每一项都是一个字符串对象,其中argv[0]是要执行的命令,之后的项则是传给命令的参数;
  • argc属性赋值记录argv数组的长度;
  • 示例如下:

当服务器从协议内容中分析得到argv属性和argc属性之后,服务器将根据argv[0]的值,在命令表中查找命令所对应的命令实现函数;以字典为例,字典命令对应的是redisCommand结构,程序则会将客户端状态的cmd指针指向这个结构,之后服务器就会使用agrv、argc属性以及redisCommand结构中保存的信息,调用命令实现函数,执行客户端指定的命令

7、输出缓冲区

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

  • 固定大小的缓冲区用于保存那些长度比较小的回复,如简短的字符串值,整数值等;
  • 可变大小的缓冲区用于保存那些长度比较大的回复,如非常长的字符串值,一个包含了很多元素的集合等;

②固定大小的缓冲区由buf和bufpos两个属性组成:

  • buf是一个大小为redis_reply_chunk_bytes字节的字节数组,redis_reply_chunk_bytes常量的默认值是16*1024,即16kb;
  • bufpos属性则记录了buf数组目前已使用的字节数量

③当buf数组的空间用完后,服务器就会开始使用可变大小的缓冲区,可变大小的缓冲区由reply链表和一个或多个字符串对象组成;通过使用链表来连接多个字符串对象,服务器可以为客户端保存一个非常长的命令回复,不受16kb的限制;

8、身份验证

客户端状态的authenticated属性用于记录客户端是否通过了身份验证,如果authenticated的值为0,那么代表客户端未通过身份验证,如果值为1,代表客户端已经通过了身份验证;

authenticated属性仅在服务器启用了身份验证功能时使用(其设置可以参考配置文件对requireoass选项的说明);若在开启后未通过身份验证,除了AUTH命令,其他命令都会被服务器拒绝;

9、时间

客户端存在几个与时间相关的属性:

  • create属性记录了创建客户端的时间,可以用来计算客户端与服务器已经连接的秒数,ClientList命令的age域记录了这个秒数;
  • lastintercation属性记录了客户端与服务器最后一次进行互动(interaction)的时间,互动可以是发送命令请求也可以是回复;它可以用来计算客户端的空转时间,ClientList命令的idle域记录了这个秒数;
  • obuf_soft_limit_reached_time属性记录了输出缓冲区第一次到达软性限制的时间;

2、客户端的创建与关闭

客户端使用不同的方式创建和关闭不同类型的客户端

1、创建普通客户端

若客户端是通过网络连接与服务器进行连接的普通客户端,那么在客户端使用connect函数连接到服务器时,服务器就会调用连接事件处理器,为客户端创建相应的客户端状态,并将客户端状态添加到服务器状态结构clients链表的末尾;

2、关闭普通客户端

一个普通客户端可以因为多种原因被关闭,如下:

关于输出缓冲区,虽然存在可变大小类型的缓冲区,但为了避免客户端回复过大占用过多的服务器资源,服务器会时刻检查客户端的缓冲区大小,并在缓冲区大小超出范围后,执行相应的限制操作。服务器使用两种模式来限制客户端输出缓冲区的大小:

  • 硬性限制:如果输出缓冲区的大小超出了硬性限制所设置的大小,那么服务器将立即关闭客户端
  • 软性限制:如果输出缓冲区的大小超出了软性限制所设置的大小,但没有超出硬性限制,那么将使用obuf_soft_limit_reached_time属性记录客户端到达软性限制的时间,服务器会继续监控,若一直超出且时长大于设置的规定时长,那么服务器将关闭客户端,若没有超出,那么客户端就不会被关闭,且obuf_soft_limit_reached_time属性也会被清零;

使用client_output_buffer_limit选项可以为普通客户端,从服务器客户端,执行发布与订阅功能的客户端分别设置不同的软性限制和硬性限制,具体用法可以参考配置文件redis.conf

3、Lua脚本的伪客户端

服务器会在初始化时创建负责执行Lua脚本中包含的Redis命令的伪客户端,并将这个伪客户端关联在服务器状态结构的lua_client属性中;lua_client伪客户端在服务器运行的整个生命周期中会一直存在,只有服务器关闭时才会被关闭

4、AOF文件的伪客户端

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

六、服务器

Redis服务器负责与多个客户端建立网络连接,处理客户端发送的命令请求,在数据库中保存客户端执行命令所产生的数据,并通过资源管理器来维持服务器自身的运转

1、命令请求的执行过程

一个命令请求从发送到回复的过程中,客户端与服务器需要完成一系列操作

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

  2. 读取命令请求:当客户端与服务器之间的连接套接字因为客户端的写入变得可读时,服务器将调用命令请求处理器来执行以下操作:①读取套接字中协议格式的请求命令,将其保存到客户端状态的输入缓冲区中;②对输入缓冲区中的命令请求进行分析,提取出命令请求中包含的命令参数以及命令参数个数,然后分别将参数和参数个数保存到客户端状态的argv属性和argc属性中;③调用命令执行器,执行客户端指定的命令;

  3. 命令执行器(1)查找命令实现:命令执行器会根据客户端状态的argv[0]参数,在命令表commandTable中查找参数所指定的命令,并将找到的命令保存到客户端状态的cmd属性中。命令表是一个字典,其键是一个个命令名字,值是一个个redisCommand结构,每个redisCommand结构记录了一个Redis命令的实现信息

  4. 命令执行器(2)执行预备操作:执行命令所需要的命令实现函数、参数、参数个数都已经准备完毕,但是在真正执行命令之前,程序还需要进行一些预备操作,确保命令可以正确顺利地被执行,如下:

  5. 命令执行器(3)调用命令的实现函数:此时执行命令的实现已经保存到了客户端状态的cmd属性中,命令的参数和个数也已经保存到了客户端状态的argv属性和argc属性中,当服务器决定要执行命令时只需要一个指向客户端状态的指针作为参数即可。执行完成后,命令回复信息会被写保存到客户端状态的输出缓冲器中(buf和reply属性),之后实现函数会伪客户端套接字关联命令回复处理器,该处理器负责将命令回复返回给客户端

  6. 命令执行器(4)执行后续工作:执行完实现函数后,服务器还需要执行一些后续工作,执行完成后,服务器会继续从文件事件处理器中获取下一条命令进行执行:

  7. 将命令回复给客户端:第5步会将命令回复保存到客户端输出缓冲器,并为客户端的套接字关联命令回复处理器,当客户端套接字转变为可写状态时,服务器就会执行命令回复处理器,将保存到客户端缓冲器中的命令回复发送给客户端,发送完成后客户端输出缓冲区会被回复处理器清空,伪处理下一个请求做好准备

  8. 客户端接受并打印命令回复:客户端接收到协议格式的命令回复后,会将这些回复转换为用户可读的格式,并打印给客户观看

2、serverCron函数

Redis服务器中的serverCron函数负责管理服务器的资源,并保持服务器自身的良好运转,它默认每隔100毫秒执行一次,下面介绍其执行的操作、以及其结构和属性

1、更新服务器事件缓存

Redis服务器中不少功能需要获取系统的当前时间,每次获取系统的当前时间都需要执行一次系统调用,为了减少系统调用的执行次数,服务器状态中的unixtime属性和mstime属性被用作当前时间的缓存。unixtime保存了秒级精度的系统当前UNIX时间戳,mstime保存了毫秒级精度的系统当前UNIX时间戳

serverCron函数默认100毫秒执行一次,所以unixtime属性和mstime属性记录的时间精确度不高。服务器只会在执行时间精确度要求不高的功能上使用这两个属性,而在执行需要高精确度时间的功能时,还是会执行系统调用获取精确时间

2、更新LRU时钟

服务器状态中的lruclock属性保存了服务器的LRU时钟,它默认10秒被更新一次,该属性和第1项中介绍的属性一样,都是服务器时间缓存的一种。每隔Redis对象都有一个lru属性,这个属性保存了对象最后一次被命令访问的时间, 当服务器需要计算一个数据库键的空转时间时会调用lruclock属性记录的时间减去lru属性记录的时间,得出这个对象的空转时间

3、更新服务器每秒执行命令的次数

serverCron函数中的trackOperationsPerSecond函数每隔100毫秒执行一次,该函数以抽样计算的方式,估算并记录服务器在最近一秒钟处理的命令请求数量,这个值可以通过Info status命令的instantaneous_ops_per_sec域查看

trackOperationsPerSecond函数每次执行,都会根据上一次抽样时间和服务器当前时间,以及上一次抽样的已执行命令数量和服务器当前的已执行命令数量,计算出两次trackOperationsPerSecond调用之间,服务器每一毫秒处理了多少条命令请求,乘以1000就得出了服务器一秒内处理命令请求的估算值

4、更新服务器内存峰值记录

服务器状态中的stat_peak_memory属性记录了服务器的内存峰值大小,serverCron函数每次执行都会查看服务器当前使用的内存数量,与stat_peak_memory保存的数值进行比较,如果当前使用的内存数量较大则记录到该属性中。Info memory命令的used_memory_peak和used_memory_human两个域分别以两种格式记录了服务器的内存峰值

5、处理SIGTERM信号

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

6、管理客户端资源

serverCron函数每次执行都会调用clientsCron函数,该函数会对一定数量的客户端进行以下检查:

  • 客户端与服务器的连接已经超时,那么程序将释放这个客户端;
  • 客户端在上一次执行命令请求后,输入缓冲区的大小超过了一定的长度,那么程序会释放客户端当前的输入缓冲区,并重新创建一个默认大小的输入缓冲区,从而防止客户端的输入缓冲区耗费过多的内存;

7、管理数据库资源

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

8、执行被延迟的BGReWriteAOF

在服务器执行BGSave命令期间,如果客户端向服务器发来BGReWriteAOF命令,那么服务器会将BGReWriteAOF命令的执行时间延迟到BGSave命令执行完毕之后。服务器的aof_rewrite_scheduled标识记录了服务器是否延迟了BGRwWriteAOF命令(为1标表示被延迟了),serverCron函数执行时,会检查BGSave命令或BGRewriteAOF命令是否正在执行,如果这两个命令没有在被执行,并且aof_rewrite_scheduled的属性为1,那么服务器将执行之前被延迟的BGReWriteAOF命令

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

服务器状态使用rdb_child_pid属性和aof_child_pid属性记录执行BGSAVE命令和BGReWriteAOF命令的子进程的ID,这两个属性也可以用于检查BGSAVE命令或BGReWriteAOF命令是否正在执行(没有在被执行则属性值为-1)。

只要其中一个属性为-1,程序就会执行wait3函数,检查子进程是否由信号发来服务器进程:

  • 如果有信号,表示RDB文件已经生成完毕或AOF文件已经重写完毕,服务器则会执行后续的操作,如替换现有RDB文件或AOF文件;
  • 如果没有信号,表示持久化操作未完成,程序不做任何操作

如果两个属性值都为-1,程序会进行以下3个检查:

  • 查看是否有BGReWriteAOF被延迟了,如果有则会开始一次新的BGReWriteAOF操作;
  • 检查服务器的自动保存条件是否被满足,如果条件满足并且服务器没有在执行持久化操作,那么服务器会开始一次新的BGSAVE操作;
  • 检查服务器设置的AOF重写条件是否被满足,如果条件满足并且服务器没有在执行其他持久化操作,那么服务器将开始一次新的BGReWriteAOF操作

10、将AOF缓冲区中的内容写入AOF文件

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

11、关闭异步客户端

服务器会关闭那些输出缓冲区大小超出限制的客户端

12、增加cronloops计数器的值

服务器状态的cronloops属性记录了serverCron函数执行的次数,它的作用时在复制模块中实现没执行serverCron函数N次就执行一次指定代码的功能

13、初始化服务器状态结构

3、初始化服务器

一个Redis服务器从启动到能接受客户端的命令请求,需要经过一系列的初始化和设置过程,比如初始化服务器状态,接受用户指定的服务器配置,创建相应的数据结构和网络连接等等,下面将介绍整个初始化过程

1、初始化服务器状态结构

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

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

initServerConfig函数设置的服务器状态属性基本都是一些整数、浮点数或者字符串属性,处了命令表之外,initServerConfig函数没有创建服务器状态的其他数据结构。该函数执行完成后会进入初始化的第二个阶段—载入配置选项

2、载入配置选项

在启动服务器时,用户可以通过给定配置参数或者指定配置文件来修改服务器的默认配置,如redis-server --port 10086指定服务器的运行端口号,或者通过redis-server redis.conf指定配置文件。如果用户在启动服务器时为这些属性的相应选项指定了新的值,那么服务器就使用用户指定的值,否则则使用默认值。

服务器在载入用户指定的配置选项,并对server状态进行更新后,服务器就可以进入初始化的第三个阶段—初始化服务器数据结构

3、初始化服务器数据结构

服务器状态除了第一步中提到的命令表,还包括其他数据结构,如包含了客户端状态结构的server.clients链表,包含了所有数据库的server.db数组等。服务器会在这一步调用initServer函数,为上述数据结构分配内存,并在需要时为这些数据结构设置或关联初始化值。

服务器在第3步才进行数据结构的初始化,是因为在第2步若用户修改了与数据结构相关的状态配置,服务器需要重新调整已创建的数据结构。为了避免这类情况,服务器通过initServerConfig函数初始化一般属性,而通过initServer函数负责初始化数据结构。

除了初始化数据结构外,initServer函数还进行了一些设置操作,包括:

  • 为服务器设置进程信号处理器;
  • 创建共享对象;
  • 打开服务器监听端口,为监听套接字关联连接应答时间处理器,等待服务器正式运行时接受客户端的连接;
  • 为serverCron函数创建时间事件,等待服务器正式运行serverCron函数;
  • 如果AOF持久化功能已打开,则打开现有的AOF文件,如果不存在则创建一个新的AOF文件,为AOF文件写入做准备;
  • 初始化服务器的后台I/O模块,为将来的I/O操作做好准备;

initServer函数执行完成后,服务器会使用ASCII字符在日志中打印出Redis的图标以及版本号信息;

4、还原数据库状态

在完成对服务器状态server变量的初始化之后,服务器需要载入RDB文件或AOF文件,并根据文件记录的内容来还原服务器的数据库状态。如果开启了AOF持久化功能,那么服务器使用AOF文件来还原数据库状态,否则将使用RDB文件来还原数据库状态。完成数据库状态还原工作后,服务器将在日志中打印出载入文件并还原数据库状态耗费的时长

5、执行事件循环

初始化的最后一步,服务器将打印出准备接受连接的提示日志,并开始执行服务器的事件循环

原文地址:https://www.cnblogs.com/Jscroop/p/13246521.html