likes
comments
collection
share

Redis 源码该怎么读?(译文)

作者站长头像
站长
· 阅读数 15

:本博客由「柏油」翻译自 github.com/redis/redis…,转载请注明出处!

译文如下:

Redis internals (内部结构)

当你阅读 README 文档时,不管是通过 GitHub 页面上还是下载 Redis 分支源码等方式,离源码仅一步之遥,因此,我们在这里对 Redis 源码做粗略讲解,你会看到:

  • 其中一些重要的文件主要做了什么,核心是什么
  • 最重要的一些方法、数据结构等

当然,我们不会深究细节,这样会使文章看起来非常冗长,我们将所有的讨论都集中在一个较高的层次上,以点带面才更容易理解,另外源码中还有很多注释,非常容易跟上节奏。

注:unstable 版本是持续开发的不稳定分支,因此这里选择 6.2 稳定版进行翻译解读。

源码结构

Redis 源码根目录包含了 README、Makefile(这个 Makefile 会调用真正 src 目录下的 Makefile 文件)以及 Redis 或者 哨兵的样例配置。

在 tests 目录下,可以找到很多可执行的 shell 脚本,用于 Redis 单例、哨兵或者集群的单元测试。

Redis 根目录下包含以下几个重要的目录:

  • src: 包含用 C 实现的 redis 源码。
  • tests: 包含用 Tcl 实现的单元测试
  • deps: 包含 redis 依赖的三方库。你的系统只需要提供一个 libc 标准库(兼容 POSIX 的接口的 C 编译器)就可以编译这个目录下的所有依赖库。值得注意的是,deps 包含 jemalloc 的复制版本,在 Linux 环境下,Redis 默认使用 jemalloc 作为内存分配器。另外,deps 目前下的一些依赖库也是从 Redis 项目开始的,当然,其 Github 仓库地址并不是在 redis/redis 目录下。

根目录下还有些不是很重要的目录就不一一列出来了,我们这里主要关注于 src 目录,该目录是 Redis 的主要实现,我们将深入看看其内部文件、结构等。

注:Redis 最近有较大重构,函数名或者文件名可能会有些变更,如果你想看到最新的可以选择 unstable 分支。

比如在 Redis3.0 版本中的文件名是这样:redis.credis.h,而后面版本就更新为 server.cserver.h,当然,整体结构还是相同的。

值得注意的是,所有「新特性」和 「PR请求」都是针对 unstable 分支的(你说它新不新?)

server.h

理解一个应用程序工作机制最简单的方式就是去了解它的数据结构,因此,我们从 Redis 最主要的头文件 server.h 开始吧~

所有的服务端配置、定义的共享状态都定义在名为 server 全局性数据结构中,结构体为 struct redisServer,部分重要的字段:

  • server.db:数组形式的 Redis DB,用来存储数据,默认 16 个 DB。
  • server.commands:服务端提供的命令
  • server.clients:连接到服务端的客户端列表
  • server.master:特殊的客户端,如果当前 Redis 实例是从节点,该节点就要存真实的 master 节点信息。 ...

还有很多字段定义在 server 结构体中,有兴趣可以自行查看。

另外一个重要的数据结构是和客户端相关的,过去叫 redisClient, 现在改名为 client,这个结构也有很多字段,我们也列举重要的几个字段:

struct client {
    int fd;
    sds querybuf;
    int argc;
    robj **argv;
    redisDb *db;
    int flags;
    list *reply;
    // ... many other fields ...
    char buf[PROTO_REPLY_CHUNK_BYTES];
}

一个已建立连接的客户端就会通过该结构体来表示:

  • fd:连接客户端对应的文件描述符
  • argc/argv:命令参数,对应的 Redis command 需要实现方法来读取参数
  • querybuf:查询缓冲区,即 用来装客户端传来参数数据
  • reply/buf:分别是动态和静态的「响应缓冲区」,也就是说,服务端要响应的数据就往这里发。只要文件描述符是可写的,这些缓冲区就会被增量地写入套接字。

以上便是已连接的客户端相关数据结构,命令的最终执行是通过 Redis command 进一步封装,而此时参数就会转换成 robj 数据结构,即 Redis Object:

struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits access time). */
    int refcount;
    void *ptr;
};

基本上,这个结构可以表示所有基本的 Redis 数据类型,如 strings, lists, sets, sorted sets 等。

  • type:通过该字段我们很容易知道当前对象的类型
  • refcount:代表当前对象被引用的次数
  • ptr:指向真实对象的指针
  • encoding:通过 type 可以区分类型,但是一个类型底层可以有不同的数据结构,因此需要 encoding 来区分

robj 在 Redis 内部使用十分广泛,但为避免间接访问带来的开销,最近在很多地方我们直接使用 sds 动态字符串进行处理了。

注:robj 结构体是对请求指令的进一步包装,形成统一的指令风格,可以在任何地方直接引用,兼容性强。

server.c

server.c 是 Redis 服务的主入口,这里定义了 main 方法,以下是 Redis 服务端启动的主要步骤:

  • initServerConfig():给「server 结构」初始化默认值
  • initServer():分配必要的内存空间、结构,启动 socket 端口监听等
  • aeMain():主事件循环,监听新连接、就绪事件等

有两个特殊的方法,在主事件循环中会「周期性」的调用:

  • serverCron():进行周期性调用(由 server.hz 参数控制调用频率),必要的、一些定期性的任务都由该方法进行调用,比如检查客户端超时。
  • beforeSleep():主事件循环中触发调用,在即将进入下一轮阻塞监听新就绪事件前做一些操作。

另外,在 server.c 文件中,可以找到 redis 服务端一些关键的处理方法:

  • call():执行当前已连接的客户端的请求命令
  • activeExpireCycle():处理带有 EXPIRE 过期参数的 key
  • performEvictions():内存淘汰策略相关,由 maxmemory 参数决定
  • redisCommandTable:全局变量,定义了 redis 所有命令,指定了命令的名称、实现命令的函数、所需参数的数量以及每个命令的其他属性。

commands.c

此文件被 utils/generate-command-code.py 自动生成,具体内容来自于 src/commands 目录下的 JOSN 文件,这些是关于 Redis 命令和所有关于它们的元数据的唯一来源。

我们可以通过 COMMAND 命令来获取元数据,而不是直接操作 JSON 文件。

networking.c

该文件定义了与客户端、主节点、从节点(在 Redis 中,从节点也是一个特殊的客户端)相关的所有「网络操作」:

  • createClient():创建和初始化一个新的客户端连接
  • addReply*():命令执行完后,通过该类方法将响应数据写回客户端。
  • writeToClient():将缓冲区中待响应的数据发送到客户端,这一具体过程由「写事件handler」sendReplyToClient() 来完成
  • readQueryFromClient():「读事件handler」,将客户端传来的数据累计读入「查询缓冲区」
  • processInputBuffer():从「查询缓冲区」进行数据解析的入口方法,这里需要按照 RESP 协议来解析,另外,一旦命令解析完成,就直接调用 processCommand() 执行命令
  • freeClient():释放内存、断开连接并移除该客户端

aof.c + rdb.c

从名字你应该也能猜得出来,两个文件分别是 Redis 的 AOF 和 RDB 两种「持久化」方式。

主要利用 fork 系统调用来创建子进程,该方式下,主/子进程将空享内存,进而子进程就可以将内存快照写到磁盘。目前有两种方式需要执行 fork 操作:

  • 一种是创建 rdb 文件快照
  • 另一种是 aof 文件重写(只有当 AOF 文件足够大时)

另外,aof.c 文件还提供了一些特殊的 API 支持,以便将客户端执行的命令追加到 AOF 文件中。

server.c 文件中定义的 call() 方法负责将执行后的命令写入 AOF 文件。

db.c

Redis 中大部分操作都是针对特定数据类型而设计,当然也存在一些「通用」的操作,比如 DELEXPIRE,这类操作的特点是针对 key 而不是 value,目前所有的通用操作都在 db.c 中实现。

以下仅列出部分重要的方法名:

  • lookupKeyRead()/lookupKeyWrite():获取给定 key 对应值的指针,如果 key 不存在则返回 NULL
  • dbAdd():在 Redis DB 中创建新 key,作为底层操作使用
  • dbDelete():删除指定 key 及其对应的 value
  • emptyDb():清空单个或者所有 Redis DB

db.c 文件中也实现了暴露给客户端使用的通用操作

object.c

上文中我们提到定义 Redis 对象的 robj 结构体,object.c 中定义了 Redis Object 基本的方法,比如分配新对象的方法、处理应用计数的方法 等等。该文件中的重要函数:

  • incrRefCount()/decrRefCount():对象的引用计数的「增减」操作,当引用值为 0 则表明该对象会被回收。
  • createObject():分配新对象,另外,也有一些特定的方法分配「字符串对象」,比如 createStringObjectFromLongLong() 。

另外,该文件也有 OBJECT 相关命令的实现。

replication.c

副本。这是 Redis 中最复杂的文件之一,建议对 Redis 源码有一定熟悉之后再进行阅读。该文件包含了 masterreplica 两种角色的实现。

其中, replicationFeedSlaves() 是该文件最重要的函数之一,主要作用是将主节点执行后的命令发送到所有从节点,然后从节点再执行,这种方式就可以保证「主从节点数据的一致性」。

另外,该文件也实现了 SYNCPSYNC 命令,分别用于首次「全量同步」和断连之后的「部分同步」。

其他 C 文件

还有很多其他 C 文件,我们可以大致看看:

  • t_hash.c, t_list.c, t_set.c, t_string.c, t_zset.c 以及 t_stream.c 是 Redis 数据类型的实现。它们既实现了访问给定数据类型的API,又实现了这些数据类型的客户端命令实现。

  • ae.c:Redis 事件驱动实现,它是一个简单独立、Redis 自身实现的网络库,易于阅读和理解。

  • sds.c:Redis 字符串库,更多信息点击 github.com/antirez/sds

  • anet.c:一个更加简单的使用 POSIX 网络标准库(相较于内核提供的原生接口)

  • dict.c:字典实现,并且提供了非阻塞的、渐进式 rehash 能力。

  • scripting.c:Lua 能力实现,相对其他 C 文件来说更具独立性,如果你熟悉 Lua API 应该能很快看懂。

  • cluster.c:Redis 分片集群实现,建议你对 Redis 源码有一点熟悉之后再进行阅读,在阅读之前可以先看看这篇文章:Redis Cluster specification

Redis命令的剖析

所有 Redis 命令都如以下方式定义:

void foobarCommand(client *c) {
    printf("%s",c->argv[1]->ptr); /* Do something with the argument. */
    addReply(c,shared.ok); /* Reply something to the client. */
}

然后这些命令会在 server.c 的命令表进行引用:

{"foobar",foobarCommand,2,"rtF",0,NULL,0,0,0,0,0},

该例子中,2 表示参数个数,'rtF' 是命令标志。

当命令执行后,将通过 network.c 中定义的 addReply()addReplyXxx() 类似方法响应客户端。

在 Redis 源代码中有大量的命令实现,可以作为实际命令实现的示例。你可以尝试写几个命令看看,这也是一种不错的了解源码的方式。

当然,还有很多源码文件未全部列出,我们只是将你从 0 引到了 1 这个阶段,剩下的就交给你了。