redis底层设计(五)——内部运做机制

5.1 数据库redis

  5.1.1 数据库的结构:数据库

  Redis 中的每一个数据库,都由一个redis.h/redisDb 结构表示:数组

typedef struct redisDb {
// 保存着数据库以整数表示的号码
int id;
// 保存着数据库中的全部键值对数据
// 这个属性也被称为键空间(key space)
dict *dict;
// 保存着键的过时信息
dict *expires;
// 实现列表阻塞原语,如BLPOP
// 在列表类型一章有详细的讨论
dict *blocking_keys;
dict *ready_keys;
// 用于实现WATCH 命令
// 在事务章节有详细的讨论
dict *watched_keys;
} redisDb;

  5.1.2 数据库的切换:缓存

    redisDb 结构的id 域保存着数据库的号码。这个号码很容易让人将它和切换数据库的SELECT 命令联系在一块儿,可是,实际上,id 属性并非用来实现SELECT 命令,而是给Redis 内部程序使用的。当Redis 服务器初始化时, 它会建立出            redis.h/REDIS_DEFAULT_DBNUM 个数据库, 并将全部数据库保存到redis.h/redisServer.db 数组中, 每一个数据库的id 为从0 到REDIS_DEFAULT_DBNUM - 1 的值。当执行SELECT number 命令时,程序直接使用redisServer.db[number] 来切换数据库。可是,一些内部程序,好比AOF 程序、复制程序和RDB 程序,须要知道当前数据库的号码,若是没有id 域的话,程序就只能在当前使用的数据库的指针,和redisServer.db 数组中全部数据库的指针进行对比,以此来弄清楚本身正在使用的是那个数据库。有了id 域的话,程序就能够经过读取id 域来了解本身正在使用的是哪一个数据库,这样就不用对比指针那么麻烦了。安全

  5.1.3 数据库键空间:服务器

    由于Redis 是一个键值对数据库(key-value pairs database),因此它的数据库自己也是一个字典(俗称key space):
      • 字典的键是一个字符串对象。
      • 字典的值则能够是包括字符串、列表、哈希表、集合或有序集在内的任意一种Redis 类型对象。
    在redisDb 结构的dict 属性中,保存着数据库的全部键值对数据。
    下图展现了一个包含number 、book 、message 三个键的数据库——其中number 键是一个列表,列表中包含三个整数值;book 键是一个哈希表,表中包含三个键值对;而message 键则指向另外一个字符串:网络

    

  5.1.4 键空间的操做:数据结构

    由于数据库自己是一个字典,因此对数据库的操做基本上都是对字典的操做,加上如下一些维护操做:
      • 更新键的命中率和不命中率,这个值能够用INFO 命令查看;
      • 更新键的LRU 时间,这个值能够用OBJECT 命令来查看;
      • 删除过时键(稍后会详细说明);
      • 若是键被修改了的话,那么将键设为脏(用于事务监视),并将服务器设为脏(等待RDB保存);
      • 将对键的修改发送到AOF 文件和附属节点,保持数据库状态的一致;并发

    好比刚开始的数据库存储结构以下:async

    

  那么在客户端执行SET date 2018-12-7 的命令以后,数据库更新为下图状态:

  

    删除和修改都差很少,这里就不一一展现了。

    当执行查询操做时实际上就是在字典空间中取值,再加上一些额外的类型检查:

      • 键不存在,返回空回复;
      • 键存在,且类型正确,按照通信协议返回值对象;
      • 键存在,但类型不正确,返回类型错误。

    举个例子,当前redis数据存储结构以下:

  

    

      * 当客户端执行GET message 时,服务器返回"hello moto" 。
      * 当客户端执行GET not-exists-key 时,服务器返回空回复。
      * 当服务器执行GET book 时,服务器返回类型错误。

     除了上面基本的对数据信息的增删改查以外,还有不少对数据库自己的命令,也是经过对键空间进行处理来完成的:      

      • FLUSHDB 命令:删除键空间中的全部键值对。
      • RANDOMKEY 命令:从键空间中随机返回一个键。
      • DBSIZE 命令:返回键空间中键值对的数量。
      • EXISTS 命令:检查给定键是否存在于键空间中。
      • RENAME 命令:在键空间中,对给定键进行更名。

   5.1.5 键的过时时间

    经过EXPIRE 、PEXPIRE 、EXPIREAT 和PEXPIREAT 四个命令,客户端能够给某个存在的键设置过时时间,当键的过时时间到达时,键就再也不可用:

redis> SETEX key 5 value
OK
redis> GET key
"value"
redis> GET key // 5 秒事后
(nil)
//命令TTL 和PTTL 则用于返回给定键距离过时还有多长时间:
redis> SETEX key 10086 value
OK
redis> TTL key
(integer) 10082
redis> PTTL key
(integer) 10068998

  5.1.6 过时时间的保存;

    在数据库中,全部键的过时时间都被保存在redisDb 结构的expires 字典里:

typedef struct redisDb {
// ...
dict *expires;
// ...
} redisDb;

    expires 字典的键是一个指向dict 字典(键空间)里某个键的指针,而字典的值则是键所指向的数据库键的到期时间,这个值以long long 类型表示。下图展现了一个含有三个键的数据库,其中number 和book 两个键带有过时时间:

      

  5.1.7 设置生存时间:

    Redis 有四个命令能够设置键的生存时间(能够存活多久)和过时时间(何时到期):
      • EXPIRE 以秒为单位设置键的生存时间;
      • PEXPIRE 以毫秒为单位设置键的生存时间;
      • EXPIREAT 以秒为单位,设置键的过时UNIX 时间戳;
      • PEXPIREAT 以毫秒为单位,设置键的过时UNIX 时间戳。
    虽然有那么多种不一样单位和不一样形式的设置方式,可是expires 字典的值只保存“以毫秒为单位的过时UNIX 时间戳” ,这就是说,经过进行转换,全部命令的效果最后都和PEXPIREAT命令的效果同样。

  5.1.8 过时键的判断

    经过expire字典,能够用如下步骤检查某个键是否过时:

      1.检查键是否存在于expires字典中:若是存在,那么取出键的过时时间;

      2.检查当前Unix时间戳是否大于键的过时时间,若是是的话,那么键已通过期;不然键未过时。

  5.1.9 过时键的清除

    一个键已通过期,删除的机制是什么呢:

      1.定时删除:在设置键的过时时间时,建立一个定时事件,当过时时间到达时,由事件处理器来自动执行键的删除操做;

      2.惰性删除:听任键过时无论,可是在每次   从dict字典中取出键值时,要检查是否过时,若是过时的话,删除它,并返回空;若是没过时,就返回键值;

      3.按期删除:每隔一段时间,对expires字典进行检查,删除里面的过时键;

    定时删除:

      定时删除策略对内存是最友好的:由于它保证过时键会在第一时间被删除,过时键所消耗的内存会当即被释放。这种策略的缺点是,它对CPU时间是最不友好的:由于删除操做可能会占用大量的CPU时间——在内存不紧张但CPU时间很是紧张的时候(好比说进行交集计算和排序的时候),将CPU时间花在删除那些和当前任务无关的过时键上,这种作法毫无疑问会是低效的。除此以外,目前redis事件处理器对时间事件的实现方式——无序链表,查找一个时间复杂度为O(N)——并不适合用来处理大量时间事件。

    惰性删除:

      惰性删除对CPU时间来讲是最有好的:它只会在取出键时进行检查,这能够保证删除操做只会在非作不可的状况下进行——而且删除的目标仅限于当前处理的键,这个策略不会再删除其余无关的过时键上花费任何CPU时间。它的缺点是对内存最不友好:若是一个键已通过期,而这个键又任然保留在数据库中,那么dict字典和expires字典都须要继续保存这个件的信息,只要这个过时键不被删除,它占用的内存就不会被释放。

    按期删除:

      经过上面对定时删除和惰性删除的介绍,咱们能够知道这两种方式都存在明显的缺陷:定时删除占用太多CPU时间,惰性删除浪费太多内存。而按期删除是这两种策略的折中:

        1.它每隔一段时间执行一次删除操做,并经过限制删除操做执行的时长和频率来减小删除操做对CPU时间的影响。

        2.经过按期删除过时键,有效的减小了因惰性删除而带来的内存浪费。

    以上是介绍的三种删除策略,而redis对于过时键的删除策略是惰性删除加上按期删除,这两个策略相互配合,能够很好地在合理利用CPU时间和节约内存空间之间取得平衡。

  5.1.10 过时键的惰性删除策略

    实现过时键惰性删除策略的核心是:expireIfNeeded函数——全部命令在读取(get、lrange、smembers)和写入(set、lpush、sadd)数据库以前,程序都会调用expireIfNeeded对输入的键进行检查,并将过时键删除:

    

    expireIfNeeded 的做用是:若是输入键已通过期,那么将键、值和保存在expires字典中的过时时间都删除掉。

  5.1.11 过时键的按期删除策略

    对过时键的按期删除由activeExpireCycle函数执行:每当redis的例行处理程序serverCron执行时activeExpireCycle  都会被调用——这个函数在规定的时间限制内,尽量地遍历各个数据库的expires字典,随机地检查一部分键的过时时间,并删除其中的过时键。    

  5.1.12 过时键对AOF、RDB和辅助的影响

    更新后的RDB文件

      在建立新的RDB文件时,程序会对键进行检查,过时的键不会被写到更新后的RDB文件中,因此过时键对更新后的RDB文件没有任何影响。

    更新后的AOF文件

      在键已通过期,可是尚未被惰性删除或者按期删除以前,这个键不会产生任何影响,AOF文件也不会由于这个键而被修改;当过时键被惰性删除或者按期删除以后,程序会向AOF文件发送一条DEL命令,来显式地记录该键已被删除。

      好比客户端使用GET message 命令时,message已通过期,那么服务器将会执行如下3个动做:

        1)从数据库删除message;

        2)追加一条DEL message 命令到AOF文件;

        3)向客户端返回NIL。

    AOF重写

      和RDB文件相似,进行AOF 文件重写时,程序会对键进行检查,过时的键不会被保存到重写后的AOF文件中。因此过时键对重写后的AOF文件没有影响。

    复制  

      当服务器带有附属节点时,过时键的删除由主节点统一控制:

        * 若是服务器是主节点,那么它在删除一个过时键以后,会显式地向附属节点发送一个DEL命令;

        * 若是服务器是附属节点,那么当它在删除一个过时键以后,他会想程序返回键已过时的回复,但并不真正的删除过时键。由于程序只根据键是否过时而不是键是否已经被删除来决定执行流程,因此这种处理并不影响程序的正确执行结果。当接到从主节点传来的DEL命令以后附属节点才会真正的将过时键删除。 附属节点不自主对键进行删除是为了和主节点的数据保持绝对一致,由于这个缘由,当一个过时键还存在于主节点时,这个键在全部附属节点的副本也不会被删除。这种处理机制对那些使用大量附属节点,而且带有大量过时键的应用来讲,可能会形成一部份内存不能当即被释放,可是,由于过时键一般很快会被主节点发现并删除,因此这实际上也算不上什么大问题。

  5.1.13 小结

    * 数据库主要是由dict和expires两个字典构成,起重工dict保存键值对,而expires则保存键的过时时间;

    * 数据库的键老是一个字符串对象,而值能够是任意一种redis数据类型,包括字符串、哈希、集合、列表和有序集;

    * expires的某个键和dict的某个键共同指向同一个字符串对象,而expires键的值则是该键以毫秒计算的Unix过时时间戳;

    * redis使用惰性删除和按期删除两种策略来删除过时键;

    * 更新后的RDB文件和从新后的AOF文件都不会保存已通过期的键;

    * 当一个过时键被删除以后,程序会追加一条新的DEL命令到现有AOF文件末尾;

    * 当主节点删除一个过时键以后,它会显式地发送一条DEL命令道全部附属节点;

    * 附属节点即便发现过时键,也不会自做主张的删除它,而是等待主节点发来DEL命令,这样能够保证主节点和附属节点的数据保持绝对一致;

    * 数据库的dict字典和expires字典的扩展策略和普通字典的同样,他们的收缩策略是:当节点的填充百分比不足10%时,将可用节点数量减小至大于等于当前已用节点数量;

5.2 RDB

  在运行状况下,redis以数据结构的形式将数据存储在内存中,为了将这些数据在redis重启以后任然可使用,redis提供了RDB和AOF两种持久化模式。

  在redis运行时,RDB程序将当前内存中的数据库快照保存到磁盘文件中,在redis从新启动时,RDB程序能够经过载入RDB文件来还原数据库的状态。

  RDB功能最核心的是RDBSave和RDBLoad两个函数,前者用于生成RDB文件到磁盘,然后者则将RDB文件中的数据从新载入到内存中。

    

  5.2.1 保存

    RDBSave函数负责将内存中的数据库数据以RDB格式保存到磁盘中,若是RDB文件已经存在,那么新的RDB文件将替换已有的RDB文件。在保存RDB文件期间,主进程会被阻塞,直到保存完为止。

    SAVE和BGSAVE两个命令都会调用RDBSave函数,但它们的保存方式不一样:

      SAVE直接调用RDBSave,阻塞redis主进程,直到保存完成为止。在主进程阻塞期间,服务器不能处理任何客户端的请求;

      BGSAVE则fork出一个子进程,子进程负责调用RDBSave,并保存完成后想主进程发送信号,通知保存已完成,由于RDBSave在子进程被调用,因此redis服务器在BGSAVE执行期间任然能够处理客户端的请求。

  5.2.2 SAVE、BGSAVE、AOF写入和BGREWRITEAOF

    SAVE
      前面提到过,当SAVE 执行时,Redis 服务器是阻塞的,因此当SAVE 正在执行时,新的SAVE 、BGSAVE 或BGREWRITEAOF 调用都不会产生任何做用。只有在上一个SAVE 执行完毕、Redis 从新开始接受请求以后,新的SAVE 、BGSAVE 或                                BGREWRITEAOF 命令才会被处理。另外,由于AOF 写入由后台线程完成,而BGREWRITEAOF 则由子进程完成,因此在SAVE执行的过程当中,AOF 写入和BGREWRITEAOF 能够同时进行。
    BGSAVE
      在执行SAVE  命令以前,服务器会检查BGSAVE 是否正在执行当中,若是是的话,服务器就不调用rdbSave ,而是向客户端返回一个出错信息,告知在BGSAVE 执行期间,不能执行SAVE 。这样作能够避免SAVE 和BGSAVE 调用的两个rdbSave 交叉执行,                形成竞争条件。另外一方面,当BGSAVE 正在执行时,调用新BGSAVE 命令的客户端会收到一个出错信息,告知BGSAVE 已经在执行当中。
    BGREWRITEAOF 和BGSAVE 不能同时执行:
      • 若是BGSAVE 正在执行,那么BGREWRITEAOF 的重写请求会被延迟到BGSAVE 执行完毕以后进行,执行BGREWRITEAOF 命令的客户端会收到请求被延迟的回复。
      • 若是BGREWRITEAOF 正在执行,那么调用BGSAVE 的客户端将收到出错信息,表示这两个命令不能同时执行。
    BGREWRITEAOF 和BGSAVE 两个命令在操做方面并无什么冲突的地方,不能同时执行它们只是一个性能方面的考虑:并发出两个子进程,而且两个子进程都同时进行大量的磁盘写入操做,这怎么想都不会是一个好主意。
  5.2.3 载入

    当Redis 服务器启动时,rdbLoad 函数就会被执行,它读取RDB 文件,并将文件中的数据库数据载入到内存中。在载入期间,服务器每载入1000 个键就处理一次全部已到达的请求,不过只有PUBLISH 、SUBSCRIBE 、PSUBSCRIBE 、UNSUBSCRIBE 、             PUNSUBSCRIBE 五个命令的请求会被正确地处理,其余命令一概返回错误。等到载入完成以后,服务器才会开始正常处理全部命令。

    注意: 发布与订阅功能和其余数据库功能是彻底隔离的,前者不写入也不读取数据库,因此在服务器载入期间,订阅与发布功能仍然能够正常使用,而没必要担忧对载入数据的完整性产生影响。另外,由于AOF 文件的保存频率一般要高于RDB 文件保存的频率,因此         通常来讲,AOF 文件中的数据会比RDB 文件中的数据要新。所以,若是服务器在启动时,打开了AOF 功能,那么程序优先使用AOF 文件来还原数据。只有在AOF 功能未打开的状况下,Redis 才会使用RDB 文件来还原数据。

  5.2.4 RDB文件结构

    一个RDB文件能够分为如下几个部分:

   

    REDIS:文件的最开头保存着REDIS 五个字符,标识着一个RDB 文件的开始。在读入文件的时候,程序能够经过检查一个文件的前五个字节,来快速地判断该文件是否有多是RDB 文件。

    RDB-VERSION:一个四字节长的以字符表示的整数,记录了该文件所使用的RDB 版本号。目前的RDB 文件版本为0006 。由于不一样版本的RDB 文件互不兼容,因此在读入程序时,须要根据版原本选择不一样的读入方式。

    DB-DATA:这个部分在一个RDB 文件中会出现任意屡次,每一个DB-DATA 部分保存着服务器上一个非空数据库的全部数据。

    SELECT-DB:这域保存着跟在后面的键值对所属的数据库号码。在读入RDB 文件时,程序会根据这个域的值来切换数据库,确保数据被还原到正确的数据库上。

    KEY-VALUE-PAIRS:由于空的数据库不会被保存到RDB 文件,因此这个部分至少会包含一个键值对的数据。每一个键值对的数据使用如下结构来保存:

     

      OPTIONAL-EXPIRE-TIME 域是可选的,若是键没有设置过时时间,那么这个域就不会出现;反之,若是这个域出现的话,那么它记录着键的过时时间,在当前版本的RDB 中,过时时间是一个以毫秒为单位的UNIX 时间戳。KEY 域保存着键,格式和                                     REDIS_ENCODING_RAW 编码的字符串对象同样。TYPE-OF-VALUE 域记录着VALUE 域的值所使用的编码,根据这个域的指示,程序会使用不一样的方式来保存和读取VALUE 的值。

    EOF:标志着数据库内容的结尾(不是文件的结尾)。

    CHECK-SUM:RDB 文件全部内容的校验和,一个uint_64t 类型值。REDIS 在写入RDB 文件时将校验和保存在RDB 文件的末尾,当读取时,根据它的值对内容进行校验。若是这个域的值为0 ,那么表示Redis 关闭了校验和功能。

   5.2.5 小结

    • rdbSave 会将数据库数据保存到RDB 文件,并在保存完成以前阻塞调用者。
    • SAVE 命令直接调用rdbSave ,阻塞Redis 主进程;BGSAVE 用子进程调用rdbSave ,主进程仍可继续处理命令请求。
    • SAVE 执行期间,AOF 写入能够在后台线程进行,BGREWRITEAOF 能够在子进程进行,因此这三种操做能够同时进行。
    • 为了不产生竞争条件,BGSAVE 执行时,SAVE 命令不能执行。
    • 为了不性能问题,BGSAVE 和BGREWRITEAOF 不能同时执行。
    • 调用rdbLoad 函数载入RDB 文件时,不能进行任何和数据库相关的操做,不过订阅与发布方面的命令能够正常执行,由于它们和数据库不相关联。

5.3 AOF

   redis分别提供了RDB和AOF两种持久化机制:

    * RDB将数据库的快照以二进制的方式保存到磁盘中;

    * AOF则以协议文本的方式,将全部对数据库进行过写入的命令(及其参数)记录到AOF文件,以此达到记录数据库状态的目的:

    

  5.3.1 AOF命令同步

    同步命令到AOF 文件的整个过程能够分为三个阶段:
      1. 命令传播:Redis 将执行完的命令、命令的参数、命令的参数个数等信息发送到AOF 程序中。
      2. 缓存追加:AOF 程序根据接收到的命令数据,将命令转换为网络通信协议的格式,而后将协议内容追加到服务器的AOF 缓存中。
      3. 文件写入和保存:AOF 缓存中的内容被写入到AOF 文件末尾,若是设定的AOF 保存条件被知足的话,fsync 函数或者fdatasync 函数会被调用,将写入的内容真正地保存到磁盘中。

  5.3.2 命令传播

    当一个Redis 客户端须要执行命令时,它经过网络链接,将协议文本发送给Redis 服务器。好比说, 要执行命令SET KEY VALUE , 客户端将向服务器发送文本"*3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n" 。服务器在接到客户端的请求以后,它会根据协议           文本的内容,选择适当的命令函数,并将各个参数从字符串文本转换为Redis 字符串对象(StringObject)。好比说,针对上面的SET 命令例子,Redis 将客户端的命令指针指向实现SET 命令的setCommand 函数,并建立三个Redis 字符串对象,分别保存SET 、KEY         和VALUE 三个参数(命令也算做参数)。每当命令函数成功执行以后,命令参数都会被传播到AOF 程序,以及REPLICATION 程序。

  5.3.3 缓存追加

    当命令被传播到AOF 程序以后,程序会根据命令以及命令的参数,将命令从字符串对象转换回原来的协议文本。好比说,若是AOF 程序接受到的三个参数分别保存着SET 、KEY 和VALUE 三个字符串,那么它将生成协议文                                                                       本"*3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n" 。协议文本生成以后,它会被追加到redis.h/redisServer 结构的aof_buf 末尾。redisServer 结构维持着Redis 服务器的状态,aof_buf 域则保存着全部等待写入到AOF 文件的协议文本。

  综合起来,整个缓存追加过程能够分为如下三步:
    1. 接受命令、命令的参数、以及参数的个数、所使用的数据库等信息。
    2. 将命令还原成Redis 网络通信协议。
    3. 将协议文本追加到aof_buf 末尾。

  5.3.4 文件写入和保存

    每当服务器常规任务函数被执行、或者事件处理器被执行时,aof.c/flushAppendOnlyFile 函数都会被调用,这个函数执行如下两个工做:
      WRITE:根据条件,将aof_buf 中的缓存写入到AOF 文件。
      SAVE:根据条件,调用fsync 或fdatasync 函数,将AOF 文件保存到磁盘中。
    两个步骤都须要根据必定的条件来执行,而这些条件由AOF 所使用的保存模式来决定

  5.3.5 AOF保存模式

    Redis 目前支持三种AOF 保存模式,它们分别是:
      1. AOF_FSYNC_NO :不保存。
      2. AOF_FSYNC_EVERYSEC :每一秒钟保存一次。
      3. AOF_FSYNC_ALWAYS :每执行一个命令保存一次。

    AOF_FSYNC_NO

    在这种模式下,每次调用flushAppendOnlyFile 函数,WRITE 都会被执行,但SAVE 会被略过。在这种模式下,SAVE 只会在如下任意一种状况中被执行:
      • Redis 被关闭
      • AOF 功能被关闭
      • 系统的写缓存被刷新(多是缓存已经被写满,或者按期保存操做被执行)

    这三种状况下的SAVE 操做都会引发Redis 主进程阻塞。

    AOF_FSYNC_EVERYSEC:

      在这种模式中,SAVE 原则上每隔一秒钟就会执行一次,由于SAVE 操做是由后台子线程调用的,因此它不会引发服务器主进程阻塞。

      注意,在上一句的说明里面使用了词语“原则上” ,在实际运行中,程序在这种模式下对fsync或fdatasync 的调用并非每秒一次,它和调用flushAppendOnlyFile 函数时Redis 所处的状态有关。
      每当flushAppendOnlyFile 函数被调用时,可能会出现如下四种状况:
        • 子线程正在执行SAVE ,而且:
        1. 这个SAVE 的执行时间未超过2 秒,那么程序直接返回,并不执行WRITE 或新的SAVE 。
        2. 这个SAVE 已经执行超过2 秒,那么程序执行WRITE ,但不执行新的SAVE 。
      注意,由于这时WRITE 的写入必须等待子线程先完成(旧的)SAVE ,所以这里WRITE 会比平时阻塞更长时间。
      • 子线程没有在执行SAVE ,而且:
        3. 上次成功执行SAVE 距今不超过1 秒,那么程序执行WRITE ,但不执行SAVE 。
        4. 上次成功执行SAVE 距今已经超过1 秒,那么程序执行WRITE 和SAVE 。

      

   

    

      根据以上说明能够知道,在“每一秒钟保存一次”模式下,若是在状况1 中发生故障停机,那么用户最多损失小于2 秒内所产生的全部数据。若是在状况2 中发生故障停机,那么用户损失的数据是能够超过2 秒的。Redis 官网上所说的,AOF 在“每一秒钟保存一                   次”时发生故障,只丢失1 秒钟数据的说法,实际上并不许确。

    AOF_FSYNC_ALWAYS

      在这种模式下,每次执行完一个命令以后,WRITE 和SAVE 都会被执行。另外,由于SAVE 是由Redis 主进程执行的,因此在SAVE 执行期间,主进程会被阻塞,不能接受命令请求。

   5.3.6 AOF 保存模式对性能和安全性的影响

    对于三种AOF 保存模式,它们对服务器主进程的阻塞状况以下:
      1. 不保存(AOF_FSYNC_NO):写入和保存都由主进程执行,两个操做都会阻塞主进程。
      2. 每一秒钟保存一次(AOF_FSYNC_EVERYSEC):写入操做由主进程执行,阻塞主进程。保存操做由子线程执行,不直接阻塞主进程,但保存操做完成的快慢会影响写入操做的阻塞时长。
      3. 每执行一个命令保存一次(AOF_FSYNC_ALWAYS):和模式1 同样。
    由于阻塞操做会让Redis 主进程没法持续处理请求,因此通常说来,阻塞操做执行得越少、完成得越快,Redis 的性能就越好。

    模式1 的保存操做只会在AOF 关闭或Redis 关闭时执行,或者由操做系统触发,在通常状况下,这种模式只须要为写入阻塞,所以它的写入性能要比后面两种模式要高,固然,这种性能的提升是以下降安全性为代价的:在这种模式下,若是运行的中途发生停机,         那么丢失数据的数量由操做系统的缓存冲洗策略决定。
    模式2 在性能方面要优于模式3 ,而且在一般状况下,这种模式最多丢失很少于2 秒的数据,因此它的安全性要高于模式1 ,这是一种兼顾性能和安全性的保存方案。
    模式3 的安全性是最高的,但性能也是最差的,由于服务器必须阻塞直到命令信息被写入并保存到磁盘以后,才能继续处理请求。
        综合起来,三种AOF 模式的操做特性能够总结以下:

   

  5.3.7 AOF文件的读取和数据还原

    AOF 文件保存了Redis 的数据库状态,而文件里面包含的都是符合Redis 通信协议格式的命令文本。
    这也就是说,只要根据AOF 文件里的协议,从新执行一遍里面指示的全部命令,就能够还原Redis 的数据库状态了。
    Redis 读取AOF 文件并还原数据库的详细步骤以下:
      1. 建立一个不带网络链接的伪客户端(fake client)。
      2. 读取AOF 所保存的文本,并根据内容还原出命令、命令的参数以及命令的个数。
      3. 根据命令、命令的参数和命令的个数,使用伪客户端执行该命令。
      4. 执行2 和3 ,直到AOF 文件中的全部命令执行完毕。
    完成第4 步以后,AOF 文件所保存的数据库就会被完整地还原出来。
    注意,由于Redis 的命令只能在客户端的上下文中被执行,而AOF 还原时所使用的命令来自于AOF 文件,而不是网络,因此程序使用了一个没有网络链接的伪客户端来执行命令。伪客户端执行命令的效果,和带网络链接的客户端执行命令的效果,彻底同样。

  5.3.8 AOF 重写

    AOF文件经过同步redis服务器所执行的命令,从而实现了数据库状态的记录,可是随着运行时间的流逝,AOF文件将会愈来愈大。因此redis须要对  AOF文件进行重写,建立一个新的AOF文件来代替原来的文件,新的AOF文件和原来保存的AOF文件保存的数据库状态彻底同样,但体积比原来的要小。

  5.3.9 AOF重写的实现

    

    当redis服务器执行上面的的4个命令后,在AOF中将会保存上面4条记录状态的数据记录信息,若如今触发了AOF的重写,那么如今的AOF文件只是记录RPUSH 1 2 3 这一条数据记录信息就好了,而后将原先的AOF文件覆盖,因此,AOF的重写大大缩小了AOF文件的占用空间。

  5.3.10 AOF 后台重写    

    做为一种辅佐性的维护手段,Redis 不但愿AOF 重写形成服务器没法处理请求,因此Redis 决定将AOF 重写程序放到(后台)子进程里执行,这样处理的最大好处是:
      1. 子进程进行AOF 重写期间,主进程能够继续处理命令请求。
      2. 子进程带有主进程的数据副本,使用子进程而不是线程,能够在避免锁的状况下,保证数据的安全性。
    不过,使用子进程也有一个问题须要解决:由于子进程在进行AOF 重写期间,主进程还须要继续处理命令,而新的命令可能对现有的数据进行修改,这会让当前数据库的数据和重写后的AOF 文件中的数据不一致。
    为了解决这个问题,Redis 增长了一个AOF 重写缓存,这个缓存在fork 出子进程以后开始启用,Redis 主进程在接到新的写命令以后,除了会将这个写命令的协议内容追加到现有的AOF文件以外,还会追加到这个缓存中:

      

    换言之,当子进程在执行AOF 重写时,主进程须要执行如下三个工做:
      1. 处理命令请求。
      2. 将写命令追加到现有的AOF 文件中。
      3. 将写命令追加到AOF 重写缓存中。
    这样一来能够保证:
      1. 现有的AOF 功能会继续执行,即便在AOF 重写期间发生停机,也不会有任何数据丢失。
      2. 全部对数据库进行修改的命令都会被记录到AOF 重写缓存中。
    当子进程完成AOF 重写以后,它会向父进程发送一个完成信号,父进程在接到完成信号以后,会调用一个信号处理函数,并完成如下工做:
      1. 将AOF 重写缓存中的内容所有写入到新AOF 文件中。
      2. 对新的AOF 文件进行更名,覆盖原有的AOF 文件。
    当步骤1 执行完毕以后,现有AOF 文件、新AOF 文件和数据库三者的状态就彻底一致了。
    当步骤2 执行完毕以后,程序就完成了新旧两个AOF 文件的交替。
    这个信号处理函数执行完毕以后,主进程就能够继续像往常同样接受命令请求了。在整个AOF后台重写过程当中,只有最后的写入缓存和更名操做会形成主进程阻塞,在其余时候,AOF 后台重写都不会对主进程形成阻塞,这将AOF 重写对性能形成的影响降到了最           低。以上就是AOF 后台重写,也便是BGREWRITEAOF 命令的工做原理。
  5.3.11 小结

    * AOF 文件经过保存全部修改数据库的命令来记录数据库的状态;

    * AOF 文件中的全部命令都以redis通信协议的格式保存;

    * 不一样的AOF 保存模式对数据的安全性、以及redis的性能有很大的影响;

    * AOF 重写的目的是用更小的体积来保存数据库的状态,整个重写的过程基本上不影响redis 主进程处理命令请求;

    * AOF 能够由用户手动触发,也能够由服务器自动触发。

5.4 事件

  事件是redis服务器的核心,主要处理两项重要的任务:

    * 处理文件事件:在多个客户端中实现多路复用,接收它们发来的请求,并将命令的执行结果返回给客户端;

    * 时间事件:实现服务器常规操做。

  5.4.1 文件事件

    Redis 服务器经过在多个客户端之间进行多路复用,从而实现高效的命令请求处理:多个客户端经过套接字链接到Redis 服务器中,但只有在套接字能够无阻塞地进行读或者写时,服务器才会和这些客户端进行交互。
    Redis 将这类由于对套接字进行多路复用而产生的事件称为文件事件(file event),文件事件能够分为读事件和写事件两类。

    读事件:

      当一个新的客户端链接到服务器时,服务器会给为该客户端绑定读事件,直到客户端断开链接以后,这个读事件才会被移除。
      读事件在整个网络链接的生命期内,都会在等待和就绪两种状态之间切换:
        • 当客户端只是链接到服务器,但并无向服务器发送命令时,该客户端的读事件就处于等待状态。
        • 当客户端给服务器发送命令请求,而且请求已到达时(相应的套接字能够无阻塞地执行读操做),该客户端的读事件处于就绪状态。   

      当事件处理器被执行时,就绪的文件事件会被识别到,相应的命令请求会被发送到命令执行器,并对命令进行求值。

    写事件:

      写事件标志着客户端对命令结果的接收状态。和客户端自始至终都关联着读事件不一样,服务器只会在有命令结果要传回给客户端时,才会为客户端关联写事件,而且在命令结果传送完毕以后,客户端和写事件的关联就会被移除。
      一个写事件会在两种状态之间切换:
        • 当服务器有命令结果须要返回给客户端,但客户端还未能执行无阻塞写,那么写事件处于等待状态。
        • 当服务器有命令结果须要返回给客户端,而且客户端能够进行无阻塞写,那么写事件处于就绪状态。
      当客户端向服务器发送命令请求,而且请求被接受并执行以后,服务器就须要将保存在缓存内的命令执行结果返回给客户端,这时服务器就会为客户端关联写事件。

    注意: 读事件只有在客户端断开和服务器的链接时,才会被移除。这也就是说,当客户端关联写事件的时候,实际上它在同时关联读/写两种事件。由于在同一次文件事件处理器的调用中,单个客户端只能执行其中一种事件(要么读,要么写,但不能又读又                       写),当出现读事件和写事件同时就绪的状况时,事件处理器优先处理读事件。这也就是说,当服务器有命令结果要返回客户端,而客户端又有新命令请求进入时,服务器先处理新命令请求。

  5.4.2 时间事件

    时间事件记录着那些要在指定时间点运行的事件,多个时间事件以无序链表的形式保存在服务器状态中。
    每一个时间事件主要由三个属性组成:
      • when :以毫秒格式的UNIX 时间戳为单位,记录了应该在什么时间点执行事件处理函数。
      • timeProc :事件处理函数。
      • next 指向下一个时间事件,造成链表。
    根据timeProc 函数的返回值,能够将时间事件划分为两类:

      • 若是事件处理函数返回ae.h/AE_NOMORE ,那么这个事件为单次执行事件:该事件会在指定的时间被处理一次,以后该事件就会被删除,再也不执行。
      • 若是事件处理函数返回一个非AE_NOMORE 的整数值,那么这个事件为循环执行事件:该事件会在指定的时间被处理,以后它会按照事件处理函数的返回值,更新事件的when 属性,让这个事件在以后的某个时间点再次运行,并以这种方式一直更新并运行                 下去。

  5.4.3 时间事件应用实例:服务器常规操做  

    对于持续运行的服务器来讲,服务器须要按期对自身的资源和状态进行必要的检查和整理,从而让服务器维持在一个健康稳定的状态,这类操做被统称为常规操做(cron job)。在Redis 中,常规操做由redis.c/serverCron 实现,它主要执行如下操做:
      • 更新服务器的各种统计信息,好比时间、内存占用、数据库占用状况等。
      • 清理数据库中的过时键值对。
      • 对不合理的数据库进行大小调整。
      • 关闭和清理链接失效的客户端。
      • 尝试进行AOF 或RDB 持久化操做。
      • 若是服务器是主节点的话,对附属节点进行按期同步。
      • 若是处于集群模式的话,对集群进行按期同步和链接测试。
    Redis 将serverCron 做为时间事件来运行,从而确保它每隔一段时间就会自动运行一次,又由于serverCron 须要在Redis 服务器运行期间一直按期运行,因此它是一个循环时间事件:serverCron 会一直按期执行,直到服务器关闭为止。
    在Redis 2.6 版本中,程序规定serverCron 每隔10 毫秒就会被运行一次。从Redis 2.8 开始,10 毫秒是serverCron 运行的默认间隔,而具体的间隔能够由用户本身调整。

  5.4.4 事件的执行与调度

    既然Redis 里面既有文件事件,又有时间事件,那么如何调度这两种事件就成了一个关键问题。简单地说,Redis 里面的两种事件呈合做关系,它们之间包含如下三种属性:
      1. 一种事件会等待另外一种事件执行完毕以后,才开始执行,事件之间不会出现抢占。
      2. 事件处理器先处理文件事件(处理命令请求),再执行时间事件(调用serverCron)
      3. 文件事件的等待时间(类poll 函数的最大阻塞时间),由距离到达时间最短的时间事件决定。
    这些属性代表,实际处理时间事件的时间,一般会比时间事件所预约的时间要晚,至于延迟的时间有多长,取决于时间事件执行以前,执行文件事件所消耗的时间。

  5.4.5 小结

    * redis分为时间事件和文件事件两类;

    * 文件事件分为读事件和写事件:读事件实现了命令请求的接收,写事件实现了命令结果的返回;

    * 时间事件分为单次执行事件和循环执行事件;

    * 文件事件和时间事件之间是合做关系;通常是服务器先执行文件事件,再执行时间事件;

    * 时间事件的实际执行时间一般比约定的时间要晚一点。

相关文章
相关标签/搜索