redis 3.0 源码阅读计划

目录

1 介绍

第 2 部分和第 3 部分为 reids 底层数据结构的实现
第 4 部分为 redis 对外提供的键值对的数据类型
第 5 部分包括数据库的实现,持久化,和一些 redis 独立的功能模块
第 6 部分为 redis 客户端与服务器的实现
第 7 部分为主从复制,redis sentinel,redis 集群的实现

2 底层数据结构实现

2.1 DONE sds

文件:sds.c 和 sds.h
数据结构

typedef char *sds;  // sds 是一个 char* 指针,指向 sdshdr.buf
struct sdshdr {
    int len;    // buf 数组已使用字节数
    int free;   // buf 数组剩余字节数
    char buf[];
};

sds 和 C 字符串区别

  • sds 二进制安全。C 字符串只能存放文本数据,且末尾必须是 '\0',它不能保存图片、音频、视频等二进制数据
  • 获取字符串长度的时间复杂度为 O(1)。C 字符串获取长度时间复杂度为 O(n)
  • sds 的 API 会自动扩容,避免缓冲区溢出。而 C 语言的 strcat(s1, s2) 函数在使用之前就需要检查字符串 s1 分配的空间是否足够
  • 减少频繁修改字符串带来的内存重分配次数
    • 空间预分配。假设修改后的字符串长度为 len,
      • 如果 len < 1MB,会分配 2*len + 1Byte 空间
      • 如果 len >= 1MB,会分配 len + 1MB + 1Byte 空间
    • 惰性空间释放。sds 字符串缩短时,并不会主动释放多余空间,只是记录在 sdshdr.free 中。当然,sds 也提供了相应的 API,真正的释放 sds 的未使用空间

2.2 DONE 双端链表

文件:adlist.c 和 adlist.h

数据结构

#define AL_START_HEAD 0  // 从表头向表尾进行迭代
#define AL_START_TAIL 1  // 从表尾到表头进行迭代

typedef struct listNode {
    struct listNode *prev;
    struct listNode *next;
    void *value;
} listNode;

typedef struct list {
    listNode *head;
    listNode *tail;
    void *(*dup)(void *ptr);             // 节点值复制函数
    void (*free)(void *ptr);             // 节点值释放函数
    int (*match)(void *ptr, void *key);  // 节点值对比函数
    unsigned long len;                   // 链表所包含的节点数量
} list;

typedef struct listIter {
    listNode *next;  // 当前迭代到的节点
    int direction;   // 迭代的方向
} listIter;

redis 双端链表特点

  • redis 的链表是无环链表。因为链表表头结点的前置结点和表尾结点的后置结点都指向 NULL
  • 通过为链表设置不同类型的特定函数(dup、free、match 函数),redis 的链表可以用于保存各种不同类型的值

2.3 DONE 字典

关于 rdb 持久化或 aof 持久化时,哈希表的负载因子:

  • 一般情况下,load-factor >= 1 时,对哈希表执行扩展操作。扩展后 load-factor <= 0.5 附近
  • 在执行持久化操作前,redis 服务器会把执行扩展操作的负载因子提高到 5,load-factor >= 5 时,才执行扩展操作。为什么这样呢?
    • 持久化时,主进程会 fork 一个子进程,由子进程进行持久化操作。操作系统采用 copy-on-write 技术来优化子进程的使用效率。在子进程存在期间,提高执行扩展操作所需的负载因子,尽量避免在子进程存在期间进行 rehash。从而防止操作系统为进程拷贝大量的内存页。换句话说,就是为了让父进程和子进程尽量共享内存页,提高效率

当 dict 中键值对过多时会分多次 rehash,而不是一次性完成。 渐进式 rehash 同时采用如下 2 种方式

  1. 设置本次 rehash 时间为 x 毫秒。每次 rehash 100 步(一步就是对一个非空 bucket 进行 rehash,空 bucket 就跳过),rehash 之后检查用的总时间是否超过 x。如果没有,继续 rehash 100 步;否则,此次 rehash 结束,等待下一次继续 rehash
  2. 对 dict 进行插入、删除、查找、获取一个随机 key 的操作前,执行: 当该 dict 没有安全迭代器,进行单步 rehash 。这样就把该 dict 的 rehash 操作均摊到对字典的插入、删除等操作上了,避免了集中 rehash 带来的庞大计算量

隐藏难点
关于安全迭代器和非安全迭代器:

  • dict 结构体中存在一个引用计数,当有一个安全迭代器在该 dict 上迭代时,该 dict 的引用计数 +1。
  • 非安全迭代器是迭代一个 dict 时,该 dict 是只读的。不能对 dict 执行导致哈希表 resize 的操作或其它造成 dict 改变的操作,否则可能发生重复迭代。迭代前和迭代后,dict 指纹必须相同。迭代过程中只能调用 dictNext() 接口
  • 安全迭代器在迭代过程中 dict 是可写的。在迭代过程中可以调用字典的 dictAdd(), dictFind() 等接口
  • 安全迭代器能够保证在迭代器未释放之前,字典的哈希表不会进行单步 rehash,在一定程度上保证了不会重复迭代同一元素。rehash 对迭代器的影响很大。假设一个迭代器在 dict 的哈希表 0 上进行迭代,其中一个元素 x 已经被迭代过了。在一次 rehash 后,x 被 rehash 到哈希表 1 中。而哈希表 1 此时还没开始迭代呢。所以最后 x 肯定会被重复迭代一次。所以安全迭代器能够保证访问到的元素不重复
    • 在对该 dict 执行:插入、删除、查找、获取一个随机 key 的操作时,如果该 dict 上没有安全迭代器(当然也肯定不会有非安全迭代器,因为非安全迭代器是只读的),执行单步 rehash
    • 在对该 dict 执行:插入、删除、查找、获取一个随机 key 的操作时,如果该 dict 上存在安全迭代器,不进行 rehash。
  • redis rdb 或 aof 持久化时,会 fork 一个子进程。在子进程中会对整个 redis 服务器中的数据进行迭代,以将所有内容序列化到 rdb 或 aof 文件中。
    • 如果采用安全迭代器进行迭代,被迭代的 dict 的安全迭代器的引用计数 +1。由于进行了写操作,所以操作系统会复制相应的旧的物理内存页的内容到新的物理内存页中,然后设置虚拟内存与物理内存的映射关系,最后把父子进程的物理内存设置可读写,这样父子进程相同的虚拟内存都指向不同的物理内存。
    • 如果采用非安全迭代器,数据被认为是不可变的,所以在子进程中使用不安全的迭代器有助于减少 copy-on-write

难点
关于游标迭代器:字典的遍历dictScan

2.4 DONE skiplist

2.5 DELAY hyperloglog

3 内存编码数据结构实现

3.1 DONE inset 数据结构

3.2 DONE ziplist 数据结构

文件 ziplist.c

  • 压缩列表是为了节约内存而开发的顺序型数据结构。 主要是为了节约内存
    • 如果字符串能转换成整数就转换成整数存储
    • 当字符串不能转换成整数时才存储字符串原串
  • ziplist 占用一整块内存存储整个列表。而不像数据结构中的链表似的,链表每插入一个元素申请一块内存
  • ziplist 不会预先申请多余的内存容量以备将来的存储,它每次插入结点都会使用 realloc 重新分配一整块内存空间(每次插入结点至少使用一次 realloc,且一般情况下使用一次 realloc)
  • ziplist 和 inset 一样,无论在大端机还是小端机,都是以小端字节序存储整数值
  • 在 redis 中,使用 ziplist 作为底层实现的有列表键、哈希键、有序集合键。当列表、哈希表、有序集合中的元素少时,会使用 ziplist 作为底层实现。

4 redis 数据类型实现

4.1 DONE list 键

文件:t_list.c

该文件主要是 列表的实现列表键命令的实现

需要注意或做到的事情:

  • 列表底层使用两种编码方式:ziplist 和 linkedlist。所以该文件涉及到列表键转码,如何从 ziplist 转码成 linkedlist,以及转码需要满足什么条件
  • 列表的 entry 肯定要对应到 ziplist 和 linkedlist 的 entry。
    • 列表 entry 的迭代器肯定要对应到底层 ziplist 和 linkedlist 的迭代器;
    • 列表 push、pop 一个 entry 肯定要对应到 ziplist 和 linkedlist 的 push、pop;等等
  • 关于对象 rojb 的引用计数的注意事项。ziplist 和 linkedlist 在引用计数上肯定是不同的。
    • 例如在插入时,对于 ziplist,会直接拷贝对象的成员中的值成员到 ziplist 的 entry 中,该对象的引用计数不必变化;而对于 linkedlist,会直接用其 entry 中的一个指针指向这个对象,所以该对象引用计数肯定要自增一的。
  • 删除迭代器指定的 entry 时,要注意删除 entry 后,迭代器的更新。为什么呢?因为 ziplist 每次在删除一个 entry 的时候都会重新为整个 ziplist 分配空间,所以迭代器位置会发生变化;而 linkedlist 需要迭代器指向下一个 entry 位置
  • brpoplpush 是原子操作。例如:从 a 列表弹出表尾元素插入到 b 列表。如果元素插入 b 列表失败时,会重新把元素放入 a 列表的表尾
  • 阻塞相关命令的实现机制。例如:blpop、brpop、brpoplpush。
    阻塞实现机制如下
    • 相关结构体

      typedef struct redisDb {
          dict *blocking_keys;// 键是数据库中 key,值是被该 key 阻塞的 redisClient 链表
          dict *ready_keys;   // 就绪的 key 的集合。该字典只有键没有值,相当于集合。当被阻塞的 key 对应的列表被 push 进数据了,就会把这个 key 添加到该集合中。用于防止发送就绪信号时,重复向 redisServer.ready_keys 添加数据。
      } redisDb;
      
      typedef struct blockingState {
          mstime_t timeout;      // 阻塞超时时间
          dict *keys;            // 造成客户端阻塞的键的集合(值为 NULL 的字典)
          robj *target;          // 在被阻塞的键有新元素进入时,需要将这些新元素添加到哪里的目标键。用于 brpoplpush 命令
      } blockingState;
      
      typedef struct redisClient {
          blockingState bpop;    // 记录客户端使用命令 brpop blpop brpoplpush 阻塞后的阻塞信息
          int flags;             // 可以设置为阻塞状态 REDIS_BLOCKED,来对客户端进行阻塞
          int btype;             // 阻塞类型。当 flags 为 REDIS_BLOCKED,设置该值为 REDIS_BLOCKED_LIST
      } redisClient;
      
      typedef struct readyList {
          redisDb *db;
          robj *key;
      } readyList;
      
      struct redisServer {
          list *ready_keys;      // 链表结点为 readyList 类型。每个结点都记录了一个指定数据库和该数据库上一个就绪的 key
      };
      
    • 调用 bpop 相关命令后,若被阻塞,执行阻塞操作 blockForKeys() :设置 redisClient 的 bpop 成员值;将该客户端添加到 redisDb.blocking_keys;设置 redisClient 阻塞标记 flags 和 btype
    • 调用 push 相关命令后,列表中有数据了。所以发送就绪信号 signalListAsReady() :生成一个 readyList 结构体对象,插入到 redisServer 的 ready_keys 链表中。
    • 解除阻塞操作 handleClientsBlockedOnLists() :遍历 redisServer.ready_keys 链表上的 readyList 元素,在 redisDb.blocking_keys 获取相应被该 key 阻塞的客户端链表。以先阻塞先解除阻塞的原则,从列表中 pop 数据,然后为指定客户端解除阻塞。每解除一个,就将该客户端从客户端链表删除,直到列表中没数据了,没解除阻塞的客户端等待下次列表被 push 数据
      • 为指定客户端解除阻塞 unblockClient() :遍历 redisClient.keys 上的所有 key;在 redisDb.blocking_keys 中获取被该 key 阻塞的 redisClient 链表;遍历该链表,找到该客户端并删除。设置 redisClient 非阻塞标记 flags 和 btyp

4.2 DONE hash 键

文件:t_hash.c

该文件主要是 散列键的实现

散列键底层的两种编码方式:ziplist 和 dict

也就是在 ziplist 和 dict 上封装了一层,封装了一些多态操作,将对 hash 的操作根据编码方式转化为对 ziplist 和 dict 的操作。文件内容主要包含有编码转换,迭代器的初始化、迭代、释放等,获取键值对,判断键值对是否存在,设置键值对,删除键值对,获取键值对数量,哈希键命令的实现等。

关于 scan 类命令要注意的事项

scan cursor [match pattern] [count n]
hscan key cursor [match pattern] [count n]
sscan key cursor [match pattern] [count n]
zscan key cursor [match pattern] [count n]
  • 如果底层是 dict 的话
    • 最多取 count 个元素(键值对)(取了 count 个元素,可能会根据 pattern 被过滤掉,所以最多取 count 个元素),如果 dict 中不够 count 个元素就取所有元素
    • 参考另一篇笔记:字典的遍历 dictScan
      • 该算法可能会返回重复元素,但是已经把返回重复元素的可能性降到了最低;
        1. 当 dict 哈希表在两次迭代过程之间发生收缩,原哈希表容量为 x,收缩后容量为 y,则最多会有 x/y – 1 个原 bucket 的节点会被重复迭代;
        2. 当 dict 哈希表在两次迭代过程之间发生扩展,不会存在同一个结点重复迭代的情况;
      • 开始遍历那一刻起,只要 dict 哈希表中的元素在迭代过程期间不被删除,肯定能被遍历到,不管 dict 哈希表扩展还是缩小;
  • 如果底层使用 inset 的话,直接取所有元素,忽略 count 参数
  • 如果底层是 ziplist 的话,直接取所有元素(键值对),忽略 count 参数

如果用 dict 编码作为哈希对象的底层实现,哈希表的一个 entry 存储一对键值对

  • 字典的每个键都是一个字符串对象,而不会是整型对象
  • 字典的每个值都是一个字符串对象,而不会是整型对象

4.3 DONE set 键

文件:t_set.c

该文件主要是 集合的实现

set 底层使用两种编码方式:intset 和 dict

它也就是在 intset 和 dict 上封装了一层,封装了一些多态操作。编码转换,迭代器的初始化、迭代、释放等,set 对象创建,删除、添加集合元素,判断是否是集合元素,随机一个元素,获取集合元素个数,

4.4 DOING zset 键

文件:t_zset.c 中除 zsl 开头的函数之外的所有函数

zset 底层使用两种编码方式:ziplist 和 skiplist + dict。

  • 对于 skiplist + dict 的编码方式。当插入一个元素时,既插入到 skiplist 中又插入到一个 dict 中。其结构体如下:

    typedef struct zset {
        dict *dict;      // 用于支持 O(1) 复杂度的按成员取分值操作
        zskiplist *zsl;  // 用于支持平均复杂度为 O(log N) 的按分值定位成员操作以及范围操作
    } zset;
    
  • 对于第二种编码方式 skiplist + dict,为什么有序集合使用跳表和字典结合的方式来实现呢,而不单独使用跳表或字典实现?
    • 跳表和字典各有其优缺点,例如:dict 能以 O(1) 时间复杂度来查找元素,而 skiplist 查找元素则需要 O(log(n));skiplist 按分值从小到大排列元素,它的优势在于范围型操作,例如:zrank、zrange 等命令就是通过 skiplist 的 API 来实现的。而 dict 中的哈希表保存的元素是乱序的,进行范围型操作时十分麻烦。skiplist + dict 结合的方式能充分利用 skiplist 和 dict 的优点。
    • skiplist 和 dict 一起使用并不会浪费太多内存。有序集合中一个 element 对应一个 score,element 对象使用了引用计数的方式在 skiplist 和 dict 间共享,不会浪费内存;dict 中也不存储 score 值,它通过一个指针指向 skiplist 结点中的 double 类型的 score。
  • 对于 ziplist 的编码方式。使用 2 个 entry 来保存一个有序集合元素。第一个 entry 保存 element,第二个保存 score。使用 ziplist 编码的有序集合的元素是按 score 从小到大顺序排列的

4.5 DELAY hyperloglog 键

5 数据库的实现

5.1 DONE Redis 数据库实现

文件:redis.h 文件中的 redisDb 结构, 以及 db.c 文件

封装了对数据库的一些操作。例如:对键的增删改查,清空数据库,随机返回数据库的一个键,键改名,对过期时间的操作等等。

redis 数据库中使用 redisDb.dict 字典来保存所有键值对。其中 key 是 sds 类型的,value 是 robj 类型的

redis 数据库中使用 redisDb.expires 字典来保存到期时间。其中 key 值是通过指针指向 redisDb.dict 中的 key,它们是共享的,并不会额外增加内存开销;value 是 UNIX 时间戳,是 int64_t 类型的

redis 对过期键的删除策略 。不难想到,过期键的删除策略可以有如下 3 种:(redis 使用了第 2 和第 3 种)

  1. 定时器。在为一个键设置过期时间的时候,创建一个定时器,定时器时间到后执行对键的删除操作。对内存最友好,对 CPU 时间极不友好。并且 redis 的时间事件使用无序链表实现的,查找事件的时间复杂度高达 O(N)。所以不使用该策略;
  2. 惰性删除。每次从键空间获取键时,都检查键是否过期,过期则删除键,未过期则返回键。对内存极不友好,对 CPU 时间友好。它会存在过期键长期不被删除的情况。为解决这些问题,需要该策略和定期删除策略一起使用;
  3. 定期删除。每隔一段时间就遍历一遍数据库中带过期时间的键,过期则删除。在 redis 中,会周期性执行定期删除函数。定期删除函数流程为:在规定的时间内,遍历各个数据库,从每个数据库中随机抽取一部分带过期时间的 key,检查并删除其中的过期键。如果规定时间到,暂停执行,等待下一次调用该函数。

5.2 DONE Redis 数据库通知功能实现

文件:notify.c

当键空间发生变化时,根据键空间的类型向指定频道发出一个通知。如果有客户端订阅了该频道,该客户端就可以收到通知

键空间通知类型 表示关联到该通知类型的配置 代码是否已支持
REDIS_NOTIFY_KEYSPACE K 支持
REDIS_NOTIFY_KEYEVENT E 支持
REDIS_NOTIFY_GENERIC g 不支持
REDIS_NOTIFY_STRING $ 不支持
REDIS_NOTIFY_LIST l 不支持
REDIS_NOTIFY_SET s 不支持
REDIS_NOTIFY_HASH h
REDIS_NOTIFY_ZSET z
REDIS_NOTIFY_EXPIRED x
REDIS_NOTIFY_EVICTED e

5.3 DONE 发布与订阅功能的实现

文件:pubsub.c 和 redis.h 文件的 pubsubPattern 结构

实现了频道订阅发布的 API 和相关命令。API 有:订阅频道/退订频道,订阅频道模式串/退订频道模式串,退订所有频道/退订所有频道模式串,发布消息到指定频道

订阅与发布功能基本结构体如下:

typedef struct pubsubPattern {
    redisClient *client;    // 订阅频道模式的客户端
    robj *pattern;          // 订阅的频道模式
} pubsubPattern;

typedef struct redisClient {
    dict *pubsub_channels;  // 该字典记录了客户端所有订阅的频道。键为频道名字,值为 NULL。也即是一个客户端订阅的频道集合
    list *pubsub_patterns;  // 链表元素为 pattern 对象。记录着该客户端订阅的频道模式。每次都添加到表尾
} redisClient;

struct redisServer {
    dict *pubsub_channels;  // 字典,键为频道,值为链表。链表中保存了所有订阅某个频道的客户端。新客户端总是被添加到链表的表尾
    list *pubsub_patterns;  // 链表元素为 pubsub_patterns。每次都添加到表尾
    int notify_keyspace_events;  // 键空间发生改变时,通知的类型。用于实现通知功能
};

5.4 DONE RDB 持久化

文件 rdb.h rdb.c

rdb 持久化是指将 redis 中的所有非空数据库以及它们的所有键值对序列化后保存到磁盘。

为节省磁盘空间,redis 持久化时,

  • 如果一个字符串能转化成整数值,就转化成整数值保存;
  • 如果能进行压缩(配置文件中允许 RDB 压缩功能 且 字符串长度大于 20 byte),就使用压缩算法压缩一下再保存到 rdb 文件中
  • 如果以上 2 种情况都不行,才会原样保存字符串

redis 对文件流的读写(fread 和 fwrite)进行了封装,实现文件 rio.c 和 rio.h。读数据时直接从文件中读。写数据时先写入一个缓冲区,写入后,根据缓冲区已有数据的大小来判断是否需要把缓冲区的数据同步到文件中。当然,这里的同步是指把 redis 缓冲区的数据写入到 C 库的缓冲区中。在 rdb 文件保存结束时,需要调用 fflush 把 C 库的缓冲区中的数据同步到内核的缓冲区;然后调用 fsysc 把内核缓冲区的数据同步到磁盘

5.5 DONE AOF 持久化

文件 aof.c

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

aof 重写过程:

  1. redis 父进程创建一个子进程
    • 子进程带有父进程的 redis 数据副本。它会遍历该 redis 数据副本在临时文件中对 AOF 文件进行重写。
    • 父进程继续处理客户端命令请求。处理请求时,它会把写命令追加到 AOF 缓冲区(sds)AOF 重写缓冲区 (重写缓冲区是一个链表,链表元素是一个 10MB 的缓存块)
  2. 父进程收到子进程的退出信号后,如果子进程正常退出的话,父进程会把 AOF 重写缓冲区的数据追加到临时文件(此时主进程调用了 write,会被阻塞一会儿,阻塞时不能处理客户端命令请求)。然后对临时文件 rename(2),替换旧的 AOF 文件。重写完成

AOF 缓冲区追加内容与 AOF文件的写入和同步:
redis 执行写命令时会把命令追加到 AOF 缓冲区 aof_buf 。把 aof_buf 的内容写入并同步到磁盘的方式有 3 种:

  1. AOF_FSYNC_ALWAYS 总是将 aof_buf 的所有内容写入并同步到 AOF 文件
  2. AOF_FSYNC_EVERYSEC 默认采用这种方式。将 aof_buf 的所有内容写入到 AOF 文件。如果距上次同步时间超过 1 秒,就使用一个线程把缓冲区内容同步到 AOF 文件
  3. AOF_FSYNC_NO 将 aof_buf 的所有内容写入到 AOF 文件。并不显式同步数据。何时同步由操作系统自己决定

关于 sync 和 sdatasync:

  • rdb 使用 fsync 把内核缓冲区数据同步到磁盘。
  • 在 linux 上 aof 使用函数 fdatasync 把内核缓冲区数据同步到磁盘,每写入 32M 就显式调用一次 fdatasync,防止缓存累积过多造成 I/O 阻塞时间过长。
  • fsync 一般至少需要 2 次 I/O 操作,一次是同步文件修改的内容,另外一次是同步文件元数据(比如文件大小,访问时间等)
  • fdatasync 一般情况下 1 次 I/O 操作就够了,它会同步文件修改的内容,一般不会同步元数据,只有在需要元数据才能正确处理后续的数据检索的时候才会同步元数据(例如:使用 ftruncate 函数修改了文件大小时,fdatasync 会需要 2 次 I/O)
  • 根据 Wikipedia 的数据,当前硬盘驱动的平均寻道时间(Average seek time)大约是 3~15ms,7200RPM 硬盘的平均旋转延迟(Average rotational latency)大约为 4ms,因此一次 IO 操作的耗时大约为 10ms 左右

6 客户端和服务器的实现

6.1 DONE 事件处理器实现

文件 ae.c ae.h ae_epoll.c 等

redis 需要处理两种事件:时间事件、文件事件

时间事件使用链表来实现的,所有的时间事件都存放在一个链表里。查找下一个超时的事件的时间复杂度为 O(n)。可以使用小根堆来优化,堆顶元素就是下一个超时的时间事件,查找时间复杂度为 O(1)

时间事件是单次定时事件还是循环定时事件取决于时间事件的回调函数的返回值。返回值 ret = -1 表示是单次定时事件;非 -1 表示 ret 毫秒后再次处理该事件

关于人为调整系统时间导致的定时器时间混乱问题:

  • 系统时间被用户改小了,redis 选择的策略是立即执行所有时间事件。这样事件可能会被提前处理
  • 系统时间被用户改大了,redis 不做处理。这样时间事件会提前处理

所以只要人为调整操作系统的时间,已注册的时间事件一般都会提前处理。

6.2 DONE Redis 客户端

文件 networking.c

通过使用由 I/O 多路复用技术实现的文件事件处理器,redis 服务器使用单线程单进程的方式来处理命令请求,并与多个客户端进行网络通信

对于每个和服务器进行连接的客户端,服务器使用 redisClient 结构来保存客户端的状态信息。服务器使用一个链表来保存所有和该服务器连接的客户端状态结构

redis 客户端分两种,这两种客户端的套接字描述符 fd 有区别:

  • 伪客户端(fake client) fd 值为 -1。伪客户端处理的命令请求来源于 AOF 文件 或者 Lua 脚本,而非网络
  • 普通客户端 fd 值为非 -1。普通客户端使用套接字来与服务器进行网络通信

输入缓冲区 命令和命令参数

  • typedef struct redisClient {
        // ...
        sds querybuf;
        robj **argv;
        int argc;
        // ...
    } redisClient;
    
  • 例如命令:

    SET key value
    

    querybuf 的 sds 值为

    *3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n
    

    服务器将客户端发送的命令请求保存到 querybuf 中后,服务器会对命令内容进行解析,相关信息保存到 argc 和 argv 属性中。如:argc 的值为 3。argv[0] 指向的 robj 类型的字符串对象为 "SET";argv[1] 对应 "key";argv[2] 对应 "value"

  • 输入缓冲区的大小会根据输入内容动态地缩小或扩大,但它的大小最大不能超过 1GB,否则服务器会关闭这个客户端

回复缓冲区

  • 每个客户端都有 2 个回复缓冲区可用。一个是固定大小的缓冲区,16k;另外一个是可变大小的缓冲区,是一个列表
    • typedef struct redisClient {
          // ...
          char buf[REDIS_REPLY_CHUNK_BYTES];  // 固定大小的回复缓冲区
          int bufpos;  // 回复缓冲区偏移量
          // ...
      } redisClient;
      
    • typedef struct redisClient {
          // ...
          list *reply;  // 可变大小的回复缓冲区。它是一个字符串对象列表
          // ...
      } redisClient;
      
  • 服务器会首先尝试使用固定大小的缓冲区。当 buf 数组已经用完,或回复内容过大而无法放进 buf 数组中时,服务器就开始使用可变大小缓冲区。此时,固定大小的回复缓冲区和可变大小的回复缓冲区中的数据加一块就是要回复给客户端的内容
  • 每次向回复缓冲区添加内容后都会检查回复缓冲区的大小。
    1. 如果回复缓冲区大小 >= 硬性限制(hard limit)所设置的大小,那么服务器会执行异步关闭客户端操作(redisServer.clients_to_close 链表保存了所有待关闭的客户端。异步关闭客户端就是把待关闭客户端添加到 redisServer.clients_to_close 链表尾部,服务器下一次执行 serverCron 函数时会关闭这个客户端)。否则执行步骤 2;
    2. 如果回复缓冲区大小 >= 软性限制(soft limit)所设置的大小,且其持续时间超过服务器设置的时长,那么服务器会执行异步关闭客户端操作。

6.3 DOING 单机 Redis 服务器的实现

7 多机功能的实现

7.1 TODO redis 主从复制

7.2 TODO redis sentinel

7.3 TODO redis 集群

本笔记系统由 时中贺 搭建和维护,使用 Emacs Org mode 编辑和构建
email: shi_zhonghe@163.com