【青训营】数据库相关(二)——Redis设计与实现阅读 | 青训营笔记

98 阅读24分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第6篇笔记

第二章 简单动态字符串

  • Redis自己构建了一种简单动态字符串(SDS)的抽象类型,作为其默认字符串表示。

2.1 SDS的定义

 struct sdshdr{
     // 记录buf数组中已经使用的字节的数量,即SDS所保存的字符串的长度
     int len;
     // 记录buf数组中未使用自己的数量
     int free;
     // 字节数组
     char buf[];
 };

2.2 SDS与C字符串的区别

  • C语言使用长度为N+1的字符数组来表示长度为N的字符串,并且字符数组最后一个元素总是空字符'\0';
  • C的字符串不能满足安全性、效率以及功能方面的要求。

2.2.1常数复杂度获取字符串长度

  • C中计算字符串长度操作复杂度为O(N);
  • SDS中获取长度可直接读取len,复杂度为O(1);

2.2.2 杜绝缓冲区溢出

  • C字符串不记录自身长度,容易导致缓冲区溢出。如strcat是假定用户执行这个函数时,已经分配了足够多的内存,容纳后续字符串的内容,一旦假设不成立,就可能会产生缓冲区溢出。
  • SDS的空间分配策略杜绝了缓冲区溢出的可能性。当SDS API要对SDS进行修改时,会先检查SDS的空间是否满足需求,若不满足,则进行孔融。

2.2.3 减少修改字符串时带来的内存重分配次数

  • 对于包含了N个字符的C字符串来说,C字符串的底层总是实现一个N+1个自负床度的数组,每次对其进行增长或缩短时,程序需要对其进行一次内存重分配操作。

    • 对于增长操作,需要进行提前扩容避免缓冲区溢出;
    • 对于缩短操作,需要进行内存释放避免内存泄露。
  • SDS通过未使用空间解除了字符串长度和底层数组长度之间的关联。在SDS中,buf数组的长度不一定就是字符数量+1,可能包含未使用的字节,由free记录。

  • 对于未使用的空间,SDS实现空间与分配和惰性空间释放两种优化策略。

    • 空间预分配:用于优化SDS字符串增长操作。即进行SDS空间扩展时,除了分配必要的空间,还会分配额外的未使用空间。若SDS长度小于1MB,则len和free空间大小一样。若大于1MB,则分配1MB的未使用空间。
    • 惰性空间释放:用于优化SDS的字符串缩短操作:收缩时不立即回收多出来的字节,而是使用free进行记录。

2.2.4 二进制安全

  • C字符串必须符合某种编码,除了字符串末尾外,字符串里不能包含空字符。因此C字符串只能保存文本数据,不适合保存图片、音频、视频、压缩文件等二进制数据。
  • SDS的API是二进制安全的。程序不会对SDS存放在buf数组里的数据进行限制、过滤等操作。SDS使用len属性的值来判断字符串是否结束。

2.2.5 兼容部分C字符串函数


第三章 链表

  • Redis中,列表键的底层、发布与订阅、慢查询、监视器等哦功能都使用了链表。Redis服务器本身还是用链表保存多个客户端的状态信息,以及构建客户端缓冲区等。

3.1 链表与链表节点的实现

 // 链表节点
 typedef struct listNode{
     // 前直节点
     struct listNode *prev;
     // 后置节点
     struct listNode *next;
     // 节点的值
     void *value;
 }listNode;
 ​
 // 链表
 typedef struct list{
     // 表头节点
     listNode *head;
     // 表尾节点
     listNode *tail;
     // 链表所包含的节点数量
     unsigned long len;
     // 节点值复制函数
     void *(*dup)(void *ptr);
     // 节点值是仿函数
     void (*free)(void *ptr);
     // 节点值对比函数
     int (*match)(void *ptr, void *key);
 }list;
  • Redis链表特性:

    • 双端;
    • 无环;
    • 带表头指针和表尾指针;
    • 带链表长度计数器;
    • 多态:链表节点使用void*指针来保存节点值,可通过dup、free、match三个属性为节点值设置特定函数,因此其可以保存不同类型值。

3.2 链表和链表节点的API


第四章 字典

  • 字典,用于保存键值对的抽象数据结构,即key-value。
  • 字典中的每个键都是独一无二的。

4.1 字典的实现

  • Redis字典使用哈希表作为i底层实现,一个哈希表里有多个哈希表结点。

4.1.1 哈希表

 typedef struct dictht{
     // 哈希表数组
     dictEntry **table;
     // 哈希表大小
     unsigned long size;
     // 哈希表大小掩码,用于计算索引值,总是等于size-1
     unsigned long sizemask;
     // 该哈希表已有节点数量
     unsigned long used;
 } dictht;

4.1.2 哈希表节点

 typedef struct dictEntry{
     // 键
     void *key;
     // 值,可以是指针或者是整数
     union{
         void *val;
         uint64_t u64;
         int64_t s64;
     } v;
     // 指向下一个哈希表节点,形成链表,解决哈希冲突
     struct dictEntry *next;
 } dictEntry;

4.1.3 字典

 typedef struct dict{
     // 类型特定函数
     dictType *type;
     // 私有数据
     void *privdata;
     // 哈希表,字典只用ht[0],ht[1]哈希表只会在对ht[0]进行rehash时使用
     dictht ht[2];
     // rehash索引,当rehash不在进行时,值为-1
     int rehashidx;
 } dict;
  • type指向一个dictType结构的指针,每个dictType结构保存一簇用于操作特定类型键值对的函数;
  • privdata保存了需要传给特定函数的可选参数。
  • type和privdata属性是针对不同类型的键值对。
 typedef struct dictType{
     // 计算哈希值的函数
     unsigned int (*hashFunction)(const void *key);
     // 复制键的函数
     void *(*keyDup)(void *privdata, const void *key);
     // 复制值的函数
     void *(*valDup)(void *privdata, const void *obj);
     // 对比键的函数
     int (*keyCompare)(void *privdata, const void *key1, vonst void *key2);
     // 销毁键的函数
     void (*keyDestructor)(void *privdata, void *key);
     // 销毁值的函数
     void (*valDestructor)(void *privdata, void *obj);
 } dictType;

4.2 哈希算法

  • 当要将一个新的键值对添加到字典里时,需要根据键值对的键计算出哈希值和索引值,随后根据索引值将包含新键值对的哈希表节点放到哈希表数组的指定索引上面。

     // 使用字典设置的哈希函数,计算key的哈希值
     hash = dict->type->hashFunction(key);
     // 使用哈希表的sizemask属性和哈希值,计算出u松隐之
     index = hash & dict->ht[x].sizemask;
    

4.3 解决键冲突

  • Redis的哈希表使用链地址法来解决键冲突。

4.4 rehash

  • 为了让哈希表的负载因子维持在一个合理的范围内,当哈希表保存的键值对数量太多或太少时,需要对其进行相应的扩展和收缩。

  • rehash步骤:

    • 为字典的ht[1]哈希表分配空间,其大小取决于要执行的操作以及ht[0]当前包含的键值对数量。

      • 若扩展,则ht[1]的大小等于第一个大于等于ht[0].used*2的2的n次方
      • 若收缩,ht[1]大小为对一个大于等于ht[0].used的2的n次方。
    • 将保存在ht[0]的所有键值对rehash到ht[1]上;

    • 当ht[0]包含的所有键值对都迁移到ht[1]后,释放ht[0],将ht[1]设置为ht[0]。

  • 满足以下条件将进行扩展操作:

    • 服务器目前没有在执行BGSAVE或BGREWRITEAOF命令,且哈希表负载因子大于等于1;
    • 服务器目前在执行BGSAVE或BGREWRITEAOF命令,且哈希表负载因子大于等于5。
    • 负载因子计算公式:load_factor = ht[0].used / ht[0].size.
  • 负载因子小于0.1时,进行收缩。

4.5 渐进式rehash

  • rehash动作是多次、渐进式完成的。主要是为了避免rehash对服务器性能产生印象。

  • 步骤:

    • 为ht[1]分配空间,让字典同时拥有ht[0]和ht[1]两个哈希表;
    • 在字典中维持一个索引计数器变量rehashidx,将其设为0,表示rehash工作开始;
    • 在rehash进行期间,每次对字典进行增删改更等操作,程序除了完成指定操作,还会将ht[0]哈希表在rehashidx索引上所有键值对rehash到ht[1],当rehash工作完成后,rehashidx属性增1;
    • 随着字典操作不断执行,在某个时间点上,ht[0]的所有键值对都会被rehash到ht[1],此时将rehashidx设为-1。
    • 在删除、查找、更新操作中,会在两个哈希表内都进行操作,增加操作则直接在ht[1]内。

第五章 跳跃表

  • 跳跃表是一种有序的数据结构,其通过在每个节点中维持多个只想其他节点的指针,从而达到快速访问节点的目的。
  • 其支持平均O(logN)、最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。
  • Redis使用跳跃表作为有序集合键的底层实现之一,若有序集合包含的元素数量较多或其成员是比较长的字符串时,Redis就会使用跳跃表来作为有序集合键的底层实现。
  • Redis只在两个地方用到了跳跃表:实现有序集合键;在集群节点中用作内部数据结构。

5.1 跳跃表的实现

  • zskiplist.header:指向跳跃表的头节点。
  • zskiplist.tail:指向跳跃表的表尾节点。
  • zskiplist.level:记录目前跳跃表内,层数最大的哪个节点的层数。
  • zskiplist.length:记录跳跃表的长度,即目前所包含的节点数量。
  • zskiplistNode.level:标记层,每个层带有前进指针和跨度两个属性。
  • zskiplistNode.backward:后退指针,只想当前节点的前一个节点,
  • zskiplistNode.score:分值,在跳跃表中,节点按各自所保存的分值从小到大排列。
  • zskiplistNode.obj:成员对象。

5.1.1 跳跃表节点

 typedef struct zskiplistNode{
     // 层
     struct zskiplistLevel{
         // 前进指针
         struct zskiplistNode *forward;
         // 跨度
         unsigned int span;
     }level[];
     // 后退指针
     struct zskiplistNode *backward;
     // 分值
     double score;
     // 成员对象
     robj *obj;
 } zskiplistNode;
  • 层:跳跃表节点的level数组可以包含多个元素,每个元素都包含一个只想其他节点的指针,从而通过层来加快访问其他节点的速度。创建新的跳跃表节点时,根据幂次定律,随机生成一个1~32的值作为level数组的大小,即层的高度。

  • 前进指针:每个曾都有指向表尾方向的前进指针,用于从表头向表尾方向访问节点。

  • 跨度:层的跨度用于记录两个节点之间的距离:

    • 跨度越大则相距的越远。
    • 指向NULL的所有前进指针的跨度都为0。
  • 注:遍历操作只是用前进指针即可完成,快读实际上是用来计算排位的。在查找某个节点的过程中,将沿途访问过的所有层的跨度累加起来,就是目标节点在跳跃表中的排位。

  • 后退指针:用于从表为像表头方向访问节点:每个节点只有一个后退指针,每次只能后退至前一个节点。

  • 分值和成员:跳跃表中所有的节点按照分值大小排序。成员指向一个字符串对象,其存放一个SDS值。注:各个节点保存的成员对象必须是唯一地,但分值可以相同,分值相同的按照成员对象的字典序排序。

5.1.2 跳跃表

typedef struct zskiplist{
    // 表头节点和表尾节点
    struct zskiplistNode *header, *tail;
    // 表中节点的数量
    unsigned long length;
    // 表中层数最大的节点的层数(表头节点的层高不算)
    int level;
} zskiplist;

5.2 跳跃表API


第六章 整数集合

  • 当一个集合只包含整数值元素,且元素数量不多时,Redis会使用整数集合作为集合键的底层实现。

6.1 整数集合的实现

typedef struct intset{
    // 编码方式
    uint32_t encoding;
    // 集合中包含的元素数量
    uint32_t length;
    // 保存元素的数组:从小到大排序,且不包含重复项
    // contents真正类型取决于encoding属性的值
    int8_t contents[];
} intset;

6.2 升级

  • 将一个新元素添加到整数集合,且其类型比现有集合中所有类型都要长时,需要进行升级,其步骤为:

    • 根据新元素的类型,扩展底层数组空间大小,并未新元素分配空间。
    • 将底层数组现有的所有元素都转换成新元素相同的类型,并放置元素。
    • 添加新元素。

6.3 升级的好处

  • 提升灵活性。
  • 尽可能解决内存。

6.4 降级

  • 整数集合不支持降级操作。

6.5 整数集合API


第七章 压缩列表

  • 压缩列表是列表键和哈希键的底层实现之一。
  • 若列表键只有少量列表项,且要么是小整数值要么是短字符串时候,利用压缩列表实现。
  • 哈希键只有较少键值对且是短字符串或小整数值时,使用压缩列表实现。

7.1 压缩列表的构成

  • 压缩列表是为了节约内存而开发的,其是由一系列特殊编码的连续内存块组成的顺序性数据结构。

  • 其组成部分如下:

    • zlbytes:表示压缩列表的总长
    • zltail:表示表尾节点的偏移量
    • zllen:表示包含的节点数

7.2 压缩列表节点的构成

previous_entry_length

  • 以字节为单位,记录了压缩列表中前一个节点的长度,可以是1字节或5字节。
  • 基于其可实现从表尾向表头的遍历操作。

encoding

  • 记录了节点的content属性所保存的数据类型以及长度。
  • 1字节、2字节或5字节,值的最高位为00、01、10或者字节数组编码,表示保存字节数组,数组长度由编码出去最高两位的其他位记录。
  • 一字节且最高位是11,表示整数编码。

content

  • 负责保存节点的值,可以是一个字节数组或整数,其类型和长度由encoding属性决定。

7.3 连锁更新

  • 添加新节点和删除节点可能会引起连锁更新。

第八章 对象

  • Redis基于上述基础数据结构构建一个对象系统,每个对象都用到了至少一种上述的结构。
  • Redis可以在执行命令之前,根据对象的类型来判断是否执行给定的命令。
  • 其好处是可以针对不同的使用场景,为对象设置不同的数据结构实现,优化其使用效率。
  • Redis对象系统实现了基于引用计数技术的的内存回收机制,并基于其实现了对象共享机制。
  • Redis的对象带有访问时间记录信息,可用于计算数据库键的空转时长。在服务器启动了maxmemory功能的情况下,空转市场较大的键会优先被删除。

8.1 对象类型与编码

  • Redis使用对象来表示数据库的键和值,每次新建一个键值对时,会至少创建两个对象,分别用作键和值。

  • Redis的每个对象都由一个redisObject结构表示:

    typedef struct redisObject{
        // 类型
        unsigned type : 4;
        // 编码
        unsigned encoding : 4;
        // 只想底层实现数据结构的指针
        void *ptr;
        // ...
    } robj;
    

8.1.1 类型

  • type属性记录了对象的类型。
  • 对于Redis数据库保存的键值对来说,一般键总是一个字符串对象。

8.1.2 编码和底层实现

  • 通过encoding属性来设定对象所使用的编码

8.2 字符串对象

  • 字符串对象的编码可以是int、raw或者embstr。

    • 若是整数型,使用int;
    • 若是大于32字节的字符串,使用raw;
    • 若是短字符串,使用embstr。

8.2.1 编码的转换

  • int和embstr编码的字符串可以转换成raw编码的字符串对象。

8.3 列表对象

  • 其编码可以是ziplist或者linkedlist。

  • 若满足以下两个条件,可使用ziplist编码:

    • 列表对象保存的所有字符串元素长度小于64字节;
    • 列表对象保存的元素数量小于512个。

8.4 哈希对象

  • 哈希对象的编码可以是ziplist或hashtable。

  • ziplist使用压缩列表作为底层实现,会先保存键的压缩节点到列表结尾,随后保存值的压缩节点。保证键值连在一起。

  • 当哈希对象同时满足以下条件时,可转换成ziplist编码:

    • 所有键值对的键和值都小于64字节;
    • 保存的键值对对象小于512个。

8.5 集合对象

  • 编码可以是intset或者hashtable。

8.6 有序集合对象

  • 编码可以是ziplist或者skiplist。

8.7 类型检查与命令多态

8.7.1 类型检查的实现

  • Redis特定命令所进行的类型检查通过redisObject的type属性来实现:

    • 在执行一个类型特定命令之前,服务器会先检查数据库键的值对象是否为执行梦灵所需要的类型。
    • 若是则执行,否则返回错误。

8.7.2 多态命令的实现

  • 基于类型的多态:一个命令可以同时用于处理多种不同类型的键;
  • 基于编码的多态:一个命令可以同时用于处理多种不同编码。

8.8 内存回收

typedef struct redisObject{
    // ...
    // 引用计数
    int refcount;
    //..
} robj;
  • 子啊创建一个新对象时,引用计数的值被初始化为1;
  • 当对象被一个新程序使用时,引用计数会+1;
  • 若对象不再被一个程序使用时,引用计数-1;
  • 当对象引用计数变为0时,对象所占用的内存会被释放。

8.9 对象共享

  • Redis让多个键共享一个值对象所需要执行的步骤:

    • 将数据库的值指针指向一个现有的值对象;
    • 将被共享的值对象的引用计数+1。

8.10 对象的空转时长

  • redisObject最后有个lru属性,记录了对象最后一次被命令访问的时间;
typedef struct redisObject{
    unsigned lru : 22;
} robj;


第九章 数据库

9.1 服务器中的数据库

  • Redis服务器将所有数据库都保存在服务器状态结构的db数组中,每个redisDb结构代表一个数据库。

    struct redisServer{
        // ...
        // 一个数组,保存着服务器中所有数据库
        redisDb *db;
        // ...
    };
    
  • 在初始化服务器时,程序会根据服务器状态的dbnum属性来决定应该创建多少个数据库:

    struct redisServer{
        // ...
        // 服务器的数据库数量
        int dbnum;
        // ...
    };
    
    // 默认会创建16个数据库,根据配置器的database决定
    

9.2 切换数据库

  • 默认情况下Redis客户的目标数据库为0数据库,客户端可通过SELECT命令切换目标数据库。

  • 服务器内部客户端状态redisClient结构的db属性记录了客户端当前的目标数据库:

    typedef struct redisClient{
        // ...
        // 记录客户端当前正在使用的数据库
        redisDb *db;
        // ...
    } redisClient;
    

9.3 数据库键空间

  • Redis是键值对数据库服务器,其每个数据库都又redisDb结构表示。其中dict字典保存了所有的键值对,可将其称为键空间。

    typedef struct redisDb{
        // ...
        // 数据库键空间,保存着数据库中所有的键值对
        dict *dict;
        // ...
    } redisDb;
    
    • 键空间的键也就是数据库的键,都是以给字符串对象。
    • 键空间的值也就是数据库的值,可以是Redis任意对象。
  • 添加新键值到数据库就是将一个新键值添加到键空间字典里。删除更新同理。

  • 清空整个数据库用FLUSHDB命令,即删除键空间所有键值对。

  • 读取一个键后,服务器会根据键是否存在来更新服务器的键空间命中次数或不命中次数,同时会更新键的LRU时间。

  • 如果客户端使用WATCH监视了某个键,那么服务器在对被监视的键进行修改之后,会将其记为脏。

  • 服务器每次修改一个键之,会对脏键计数器的值增1,这个计数器会触发服务器的持久化以及赋值操作。

9.4 设置键的生存时间或过期时间

  • 通过EXPIRE命令或者PEXPIRE命令,以秒或者毫秒精度为数据库的键设置生存时间。

9.5 过期键删除策略

  • 定时删除:在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除操作。
  • 惰性删除:放任键国企不管,但每次从键空间获取键时,检查是否过期,若过期则删除。
  • 定期删除:每隔一段时间,对数据库进行检查,删除里面的过期键。

9.5.1 定时删除

  • 对内存友好但对CPU时间不友好。
  • 现阶段对于大量定时器实现删除策略不够现实。

9.5.2 惰性删除

  • 对CPU时间友好,但是对内存不友好。

## Redis过期键删除策略

  • 使用惰性删除和定期删除两种策略,在合理使用CPU时间和避免浪费内存空间之间取得平衡。
# 定期删除的实现

# 默认每次检查的数据库数量
DEFAULT_DB_NUMBERS = 16
# 默认每个数据库检查的键数量
DEFAULT_KEY_NUMBERS = 20
# 全局变量,记录检查进度
current_db = 0

def activeExpireCycle():
    # 初始化要检查的数据库数量
    if server.dbnum < DEFAULT_DB_NUMBERS:
        db_numbers = server.dunum
    else:
        db_numbers = DEFAULT_DB_NUMBERS
    # 遍历数据库
    for i in range(db_numbers):
        # 如果current_db的值等于服务器的数据库数量,则说明已经便利了一次
        # 此时将current_db重置为0,开始新的遍历
        if current_db == server_dbnum:
            current_db = 0
        ## 获取当前要处理的数据库
        redisDb = server.db[current_db]
        # 将数据库索引增1,表示下一个要处理的数据库
        current_db += 1
        # 检查数据库键
        for j in range(DEFAULT_KEY_NUMBERS):
            # 如果没有一个键带有过期时间,跳过这个数据库
            if redisDb.expires.size() == 0:
                break
            # 随机获取一个带有过期时间的键
            key_with_ttl = redisDb.expires.get_random_key()
            # 检查是否过期,若过期则删除
            if is_expired(key_with_ttl):
                delete_key(key_whit_ttl)
            # 已经达到hi箭上弦,停职处理
            if reach_time_limit():
                return

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

9.7.1 生成RDB文件

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

9.7.2载入RDB文件

  • 如果服务器以主服务器模式运行,那么在载入RDB文件时,程序会对文件中保存的键进行检查,过期的将被忽略;
  • 如果服务器以从服务器运行,则无论是否过期都会载入到数据库中。

9.7.3 AOF文件写入时

  • 当服务器以AOF持久化模式运行时,一个过期键若没有被删除,AOF文件不会因为过期键而受到影响。
  • 当被删除后,程序会向AOF追加一个DEL命令,来显式记录该键已经被删除。

9.7.4 AOF重写

  • 在AOF重写过程中,程序会对数据库中的键进行检查,已经过期的不会被保存到重写后的AOF文件中。

9.7.5 复制

  • 当服务器运行在复制模式下,从服务器的过期键删除动作由主服务器控制,从而保证主从服务器的一致性。

9.8 数据库通知

  • 服务器配置的notify-keyspace-events选项决定了服务器所发送通知的类型:
  • 想让服务器发送所有类型的键空间通知和键事件通知,可以将选项的值设置为AKE。
  • 想让服务器发送所有类型的键空间通知,可以将选项的值设置为AK。
  • 想让服务器发送所有类型的键事件通知,可以将选项的值设置为AE。
  • 想让服务器只发送和字符串键有关的键空间通知,可以将选项的值设置为K$。
  • 想让服务器只发送和列表键有关的键事件通知,可以将选项的值设置为El。

9.8.1 发送通知

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


第十章 RDB持久化

  • Redis是一个键值对数据库服务器,其包含任意个非空数据库,而每个非空数据库又可以包含人一个键值对。将非空数据库以及键值对称为数据库状态。
  • Redis是内存数据库,将数据库状态存储在内存里面,若部将其存储在磁盘里,一旦服务器进程退出,则数据库状态也会消失。
  • 为解决数据库状态消失问题,提供了RDB持久化,可将Redis在内存中的数据库状态保存在磁盘里。
  • RDB持久化可以手动执行也可以配置定期执行。
  • RDB持久化所生成的RDB文件是一个经过压缩的二进制文件。

10.1 RDB文件的创建与载入

  • SAVE和BGSAVE都可以用于生成RDB文件。但是SAVE命令会阻塞Redis服务器进程,BGSAVE会派生出一个子进程。

    def SAVE():
        # 创建RDB文件
        rdbSave()
    
    def BGSAVE():
        # 创建子进程
        pid = fork()
        if pid == 0:
            # 子进程负责创建RDB文件
            rdbSave()
            # 完成后给父进程发送信号
            signal_parent()
        elif pid > 0:
            # 父进程继续处理命令请求,轮询等待子进程信号
            handle_request_and_wait_signal()
        else:
            # 处理错误
            handle_fork_error()
    
  • RDB文件的载入工作是在服务器启动时自动执行的,只要Redis服务器启动时检测到RDB文件存在,就会自动载入RDB文件。

  • AOF文件的更新频率通常比RDB文件高,所以:

    • 若服务器开启了AOF持久化功能,则服务器会优先使用AOF文件还原;
    • 只有在AOF持久化功能关闭时,服务器才会用RDB文件还原。
  • 在BGSAVE命令执行期间,服务器处理SAVE、BGSAVE、BGREWRITEAOF三个命令的方式会有比那花:

    • 客户端发送的SAVE会被服务器拒绝,避免发生竞态条件;
    • 客户端发送的BGSAVE也会被服务器拒绝;
    • BGREWRITEAOF和BGSAVE两个命令不能同时执行,BGREWRITEAOF期间执行BGSAVE会被拒绝,BGSAVE执行期间BGREWRITEAOF会被延迟。

10.2 自动间隔性保存

struct redisServer{
    // ...
    // 记录了保存条件的数组
    struct saveparam *saveparams;
    // 修改计数器
    long long dirty;
    // 上一次执行保存的时间
    time_t lastsave;
    // ...
};

// 属性数组,保存了一个save选项设置的保存条件
struct saveparam{
    // 秒数
    time_t seconds;
    // 修改数
    int changes;
};
# 检查保存条件的过程
def serverCron():
    # ...
    # 遍历所有保存条件
    for saveparam in server.saveparams:
        # 计算距离上次执行保存操作有多少秒
        save_interval = unixtime_now() - server.lastsave;
        
        # 若修改次数超过设置次数,且保存时间超过设置时间,则保存
        if server.dirty >= saveparam.changes and save_interval > saveparam.seconds:
            BGSAVE()
    # ...

10.3 RDB文件结构


第十一章 AOF持久化

  • AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库的状态。
  • 被写入AOF文件的所有命令都是以Redis命令请求协议格式保存的,因为Redis的命令请求协议是纯文本格式。

11.1 AOF持久化的实现

  • 命令追加:AOF功能打开时,写命令之后会议协议的格式将其追加到服务器状态aof_buf缓冲区末尾。
  • 文件写入:服务器结束一个事件循环之前,回调用flushAppendOnlyFile函数,考虑是否将aof_buf缓冲区中的内容写入和保存到AOF文件里。
  • 文件同步

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

  • Redis读取AOF文件并还原数据库状态的详细步骤如下:

    • 创建一个不带网络连接的伪客户端:因为Redis的命令只能在客户端上下文执行,而载入AOF文件使用的命令来源于AOF文件而不是网络连接,利用伪客户端执行命令;
    • 从AOF文件中分析并读取出一条写命令;
    • 使用为客户端执行被读出的写命令;
    • 一直执行上面两个步骤,直到AOF文件中的所有写命令被处理完。

11.3 AOF重写

  • 为了解决AOF文件体积膨胀的问题,Redis提供了AOF文件重写功能。通过该功能,Redis服务器可以创建一个新的AOF文件来提到现有的AOF文件,新旧两个AOF文件保存的数据状态相同,且没有任何浪费空间的冗余命令。
  • AOF文件重写不需要对现有的AOF文件进行任何读取、分析或者写作操作,这个功能通过读取服务器当前数据库的状态来实现。
def aof_rewrite(new_aof_file_name):
    # 创建新的AOF文件
    f = create_file(new_aof_file_name)
    # 遍历数据库
    for db in redisServer.db:
        # 忽略空数据库
        if db.is_empty():
            continue
        # 写入SELECT指令,指定数据库号码
        f.write_command("SELECT" + db.id)
        # 遍历数据库中所有键
        for key in db:
            # 忽略已过期的键
            if key.is_expried():
                continue
            if key.type == String:
                rewrite_string(key)
            if key.type == List:
                rewrite_list(key)
            if key.type == Hash:
                rewrite_hash(key)
            if key.type == Set:
                rewrite_set(key)
            if key.type == SortedSet:
                rewrite_sorted_set(key)
            # 如果键有过期时间,那么过期时间也要被重写
            if key.have_expire_time():
                rewrite_expire_time(key)
    # 关闭文件
    f.close()
    
def rewrite_string(key):
    # get获取value
    value = GET(key)
    # set重写
    f.write_command(SET, key, value)
def rewrite_list(key):
    # 使用LRANGE获得列表键所有元素
    item1, item2, ..., itemN = LRANGE(key, 0, -1)
    # rpush重写
    f.write_command(RPUSH, key,  item1, item2, ..., itemN)
def rewrite_hash(key):
    field1, value1, field2, value2, ..., fieldN, valueN = HGETALL(key)
    f.write_command(HMSET, key, field1, value1, field2, value2, ..., fieldN, valueN)
def rewrite_set(key):
    elem1, elem2, ..., elemN = SMEMBRES(key)
    f.write_command(SADD, key, elem1, elem2, ..., elemN)
def rewrite_sorted_set(key):
    member1, score1, member2, score2, ..., memberN, scoreN = ZRANGE(key, 0, -1, "WITHSCORES")
    f.write_command(ZADD, key, member1, score1, member2, score2, ..., memberN, scoreN)
def rewrite_expire_time(key):
    timestamm = get_expire_time_in_unixstamp(key)
    f.write_command(PEXPIREAT, key, timestamp)
  • 为了解决AOF后台重写中,数据值不一致的问题,Redis服务器设置了一个AOF重写缓冲区。这个缓冲区在服务器创建子进程之后开始使用。Redis执行完一个写命令时候,会同时将这个写命令发送给AOF缓冲区和AOF重写缓冲区。即子进程进行AOF重写时,服务器工作:

    • 执行客户端命令;
    • 将命令追加到AOF缓冲区;
    • 将命令追加到AOF重写缓冲区。
  • 这样可以保证:

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

第十二章 事件

  • 文件事件:Redis服务器通过套接字与客户端进行连接,文件事件就是服务器对套接字操作的抽象。
  • 时间事件:Redis服务器中的一些操作需要在特定的时间点执行。

12.1 文件事件

  • 使用Reactor模式实现I/O多路复用;
  • 单线程方式运行。
  • 组成:套接字、I/O多路复用程序、文件事件分派器、事件处理器。

12.2 时间事件

  • 时间事件可分为两类:

    • 定时事件
    • 周期事件
  • 时间事件的三个属性:

    • ID
    • WHEN:时间戳
    • TIMEPROC:事件处理器
  • 实现:

    • 将所有的时间事件放到一个无序链表中,当时间事件执行器运行时,就便利整个链表,查找所有已到达的时间事件,并调用相应的处理器。

第十三章 客户端

第十四章 服务器



第十五章 复制

  • 在Redis中,用户可以通过执行SLACEOF命令或者设置slaveof选项,让一个服务器去复制另一个服务器,我们称呼被赋值的服务器为主服务器,而对祝福其进行复制的服务器则被称为从服务器。
  • Redis的复制功能分为同步和命令传播两个操作。

第十六章 Sentinel

16.1 启动并初始化Sentinel


第十七章 集群