Reids 源码导读

2022-08-10 14:23:05 浏览数 (1)

导读:本文以 Reids 6.0 版本的 readme 为底本,结合 unstable 分支进行整理。从 Redis 是什么?怎么构建、如何运行、体验,及主要代码文件、数据结构等方面从高纬度视角介绍 Redis,以及如何阅读 Reids 源代码。

本 README 只是一个快速入门文档。你可以在redis.io[1]找到更详细的文档。

什么是 Redis?

Redis 通常被称为数据结构服务器。这意味着 Redis 通过一组命令提供对可变数据结构的访问,这些命令使用带有 TCP 套接字和简单协议的服务器-客户端模型发送。因此不同的进程可以以共享的方式查询和修改相同的数据结构。 Redis 中实现的数据结构有一些特殊的属性:

•虽然 Redis 通常被当做内存数据库,读写操作都是在内存中进行的,但是 Redis 也提供了将数据同步到磁盘的功能。这意味着 Redis 速度很快,但它也是非易失的。

•Redis 数据结构的实现强调内存效率(时间&空间),因此与使用高级编程语言实现的相同数据结构相比,Redis 内部的数据结构可能会使用更少的内存。

•Redis 提供了许多数据库软件常见特性,例如复制、可调整级别的持久性、集群和高可用性。

另一个很好的例子是将 Redis 视为更复杂的 memcached 版本,其中的操作不仅是 SETGET,而且还适用于复杂数据类型(如ListSet、有序数据结构等)的操作。

如果您想了解更多信息,可以参阅下面链接:

•Redis 数据类型简介。 https://redis.io/topics/data-types-intro

•直接在浏览器中试用 Redis。 https://try.redis.io

•Redis 命令的完整列表。 https://redis.io/commands

Redis 官方文档中还有更多内容。 https://redis.io/documentation

构建 Redis

Redis 可以在 Linux、OSX、OpenBSD、NetBSD、FreeBSD 上编译和使用。我们支持大端和小端架构,以及 32 位和 64 位系统。 Redis 可以在 Solaris 派生系统(例如 SmartOS)上编译,但并不保证 Redis 在这些系统上和在 Linux、OSX 、 *BSD 上工作得一样好。

它很简单:

代码语言:javascript复制
% make

要使用 TLS 支持进行构建,您需要 OpenSSL 开发库(例如 Debian/Ubuntu 上的 libssl-dev)并运行:

代码语言:javascript复制
% make BUILD_TLS=yes

要使用 systemd 支持构建,您需要 systemd 开发库(例如 Debian/Ubuntu 上的 libsystemd-dev 或 CentOS 上的 systemd-devel)并运行:

代码语言:javascript复制
% make USE_SYSTEMD=yes

要将后缀附加到 Redis 程序名称,请使用:

代码语言:javascript复制
% make PROG_SUFFIX="-alt"

可以使用以下命令运行 32 位 Redis 二进制文件:

代码语言:javascript复制
% make 32bit

构建 Redis 后,最好使用以下方法对其进行测试:

代码语言:javascript复制
% make test

构建了 TLS,则在启用 TLS 的情况下运行测试(您将需要安装 tcl-tls):

代码语言:javascript复制
% ./utils/gen-test-certs.sh
% ./runtest --tls

修复依赖项或缓存构建选项等构建问题

Redis 有一些依赖项,它们包含在 deps 目录中。即使依赖项的源代码中的某些内容发生更改,make 也不会自动重建依赖项。

当您使用 git pull 更新源代码或以任何其他方式修改依赖关系树中的代码时,请确保使用以下命令来真正清理所有内容并从头开始重建:

代码语言:javascript复制
% make disclean

这将清理:jemallocluahiredislinenoise

此外,如果您强制使用某些构建选项,如 32 位包、无 C 编译器优化(用于调试目的)以及其他类似的构建时间选项,这些选项将无限期缓存,直到您发出 make distclean 命令。

修复构建 32 位二进制文件的问题

如果在使用 32 位目标构建 Redis 后,又需要使用 64 位目标重新构建它,或者相反,您需要在 Redis 发行版的根目录中执行 make distclean

如果在尝试构建 Redis 的 32 位二进制文件时出现构建错误,请尝试以下步骤:

•安装软件包 libc6-dev-i386(也可以尝试 g -multilib)。

尝试使用以下命令行而不是make 32bitmake CFLAGS="-m32 -march=native" LDFLAGS="-m32"

分配器

在构建 Redis 时选择非默认内存分配器是通过设置 MALLOC环境变量来完成的。默认情况下,Redis 是针对libc malloc 编译和链接的,但jemalloc是 Linux 系统上的默认设置。之所以选择此默认值,是因为 jemalloc 已被证明比libc malloc 具有更少的碎片问题。

要强制针对 libc malloc进行编译,请使用:

代码语言:javascript复制
% make MALLOC=libc

要在 Mac OS X 系统上针对 jemalloc 进行编译,请使用:

代码语言:javascript复制
% make MALLOC=jemalloc

详细构建

默认情况下,Redis 将使用用户友好的彩色输出进行构建。如果要查看更详细的输出,请使用以下命令:

代码语言:javascript复制
% make V=1

运行 Redis

要使用默认配置运行 Redis

只需键入:

代码语言:javascript复制
% cd src
% ./redis-server

如果要提供 redis.conf,则必须使用附加参数(配置文件的路径)运行它:

代码语言:javascript复制
% cd src
% ./redis-server /path/to/redis.conf

可以通过使用命令行直接将参数作为选项传递来更改 Redis 配置。例如:

代码语言:javascript复制
% ./redis-server --port 9999 --replicaof 127.0.0.1 6379
% ./redis-server /etc/redis/6379.conf --loglevel debug

redis.conf 中的所有选项也支持作为使用命令行的选项,名称完全相同。

使用 TLS 运行 Redis

有关如何将 Redis 与 TLS 结合使用的更多信息,请参阅 TLS.md 文件。

体验 Redis

你可以使用 redis-cli 来体验 Redis。启动一个 redis-server 实例,然后在另一个终端中尝试以下操作:

代码语言:javascript复制
% cd src
% ./redis-cli
redis> ping
PONG
redis> set foo bar
OK
redis> get foo
"bar"
redis> incr mycounter
(integer) 1
redis> incr mycounter
(integer) 2
redis>

您可以在 https://redis.io/commands 找到所有可用命令的列表。

安装 Redis

要将 Redis 二进制文件安装到 /usr/local/bin,只需使用:

代码语言:javascript复制
% make install

如果您希望使用不同的目的地,您可以使用make PREFIX=/some/other/directory install

Make install 只会在您的系统中安装二进制文件,但不会在适当的位置配置初始化脚本和配置文件。如果仅仅想体验一下 Redis,则不需要这样做。但是如果想在生存环境正确的安装它,官方为为 Ubuntu 和 Debian 系统提供了一个脚本可以执行此操作:

代码语言:javascript复制
% cd utils
% ./install_server.sh

注意:install_server.sh 不能在 Mac OSX 上运行;它仅适用于 Linux。

该脚本将询问几个问题,并完成相关配置设置,然后作为守护进程启动 Redis。该后台守护程序将在系统重新启动时重新启动。

可以使用名为/etc/init.d/redis_<portnumber>的脚本来停止和启动 Redis,例如 /etc/init.d/redis_6379

代码贡献

注意:通过以任何形式向 Redis 项目贡献代码,包括通过 Github 发送拉取请求、通过私人电子邮件或公共讨论组发送代码片段或补丁,即表示您同意根据 BSD 许可条款(您可以在 Redis 源代码分发包中的 COPYING 文件中找到)发布您的代码。

有关详细信息,请参阅此源分发中的 CONTRIBUTING 文件。

Redis 内部结构

如果您正在阅读此自述文件,您可能在 Github 页面前面,或者您只是解压了 Redis 分发 tar 包。在这两种情况下,你基本上离源代码只有一步之遥,所以在这里我们解释一下 Redis 源代码的布局、每个文件中的大致概念、Redis 服务器内部最重要的功能和结构等等。这里将所有讨论保持在高层次,而不深入细节,因为否则该文档会很大,而且代码库会不断变化,但总体思路应该是了解更多内容的良好起点。此外,大部分代码都经过大量注释并且易于理解。

源代码布局

Redis 根目录只包含这个 README、调用src 目录中真正的 MakefileMakefile 以及 Redis 和 Sentinel 的示例配置。以及一些用于执行 Redis、Redis Cluster 和 Redis Sentinel 单元测试的 shell 脚本,它们在 tests目录中实现。

根目录内有以下重要目录:

src:包含 Redis 实现,用 C 编写。

tests:包含单元测试,在 Tcl 中实现。

deps:包含 Redis 使用的库。编译 Redis 所需的一切依赖都在这个目录中;系统只需要提供 libc、一个 POSIX 兼容的接口和一个 C 编译器。值得注意的是 deps 包含一个jemalloc的副本,它是 Linux 下 Redis 的默认分配器。请注意,在 deps 下还有一些东西是从 Redis 项目开始的,但主存储库不是redis/redis

还有一些目录,但它们对于我们在这里的目标并不是很重要。我们将主要关注包含 Redis 实现的 src,探索每个文件中的内容。文件展示的顺序是为了逐步揭示不同层次的复杂性而遵循的逻辑顺序。

注意:最近 Redis 被重构了很多。函数名和文件名已更改,因此您可能会发现此文档更接近地反映了不稳定的分支。例如,在 Redis 3.0 中,server.cserver.h 文件被命名为redis.credis.h。但是整体结构是一样的。请记住,所有新的开发和拉取请求都应该针对不稳定的分支执行。

server.h

了解程序如何工作的最简单方法是了解它使用的数据结构。因此,我们将从 Redis 的主头文件 server.h开始。

所有的服务器配置和通常所有的共享状态都定义在一个名为 server 的全局结构中,类型为 struct redisServer。此结构中的几个重要字段是:

server.db 是存储数据的 Redis 数据库数组。

server.commands 是命令表。

server.clients 是连接到服务器的客户端的链表。

server.master 是一个特殊的客户端,如果实例是副本,则为 master。

还有很多其他领域。大多数字段直接在结构定义中进行注释。

另一个重要的 Redis 数据结构是定义客户端的数据结构。过去它被称为redisClient,现在只是client。该结构有很多字段,这里我们只展示主要的:

代码语言:javascript复制
struct client {
  int fd;
  sds querybuf;
  int argc;
  robj **argv;
  redisDb *db;
  int flags;
  list *reply;
  char buf[PROTO_REPLY_CHUNK_BYTES];
  //... many other fields ...
}

客户端结构定义了一个连接的客户端:

fd 字段是客户端套接字文件描述符。

argcargv 填充了客户端正在执行的命令,因此实现给定 Redis 命令的函数可以读取参数。

querybuf 缓存客户端的请求的缓冲区,这些请求由 Redis 服务器根据 Redis 协议进行解析,并通过调用客户端正在执行的命令实现来执行。

replybuf 是动态和静态缓冲区,用于累积服务器发送给客户端的回复。一旦文件描述符可写,这些缓冲区就会增量写入套接字。

正如您在上面的客户端结构中所见,命令中的参数被描述为 robj结构。下面是完整的robj结构,它定义了一个Redis object:

代码语言:javascript复制
typedef struct redisObject {
  unsigned type:4;
  unsigned encoding:4;
  unsigned lru:LRU_BITS; /* lru time (relative to server.lruclock) */
  int refcount;
  void *ptr;
} robj;

基本上,这种结构可以表示所有基本的 Redis 数据类型,如字符串、列表、集合、排序集合等。有趣的是它有一个type字段,因此可以知道给定对象的类型,以及一个refcount,这样可以在多个地方引用同一个对象,而无需多次分配。最后,ptr 字段指向对象的实际表示,即使对于相同的类型,它也可能会有所不同,具体取决于所使用的encoding

Redis 对象在 Redis 内部结构中被广泛使用,但是为了避免间接访问的开销,最近在许多地方我们只使用未包装在 Redis 对象中的普通动态字符串。

server.c

这是 Redis 服务器的入口点,其中定义了 main() 函数。以下是启动 Redis 服务器的最重要步骤。

initServerConfig() 设置服务器结构的默认值。•initServer() 分配操作、设置侦听套接字等所需的数据结构。•aeMain() 启动监听新连接的事件循环。

事件循环会定期调用两个特殊函数:

serverCron() 定期调用(根据 server.hz 频率),并执行必须定期执行的任务,例如检查客户端是否超时。•每次触发事件循环时都会调用 beforeSleep(),Redis 服务了一些请求,然后返回到事件循环中。

server.c 中,您可以找到处理 Redis 服务器其他重要事项的代码:

call() 用于在给定客户端的上下文中调用给定命令。

activeExpireCycle() 通过 EXPIRE 命令处理具有生存时间的键的删除。

freeMemoryIfNeeded() 在执行新的写入命令,但根据 maxmemory指令,判断Redis 内存不足时调用。

•全局变量 redisCommandTable 定义了所有 Redis 命令,指定命令的名称、实现命令的函数、所需的参数数量以及每个命令的其他属性。

network.c

该文件定义了所有带有客户端、主服务器和副本(在 Redis 中只是特殊客户端)的 I/O 功能:

createClient() 分配并初始化一个新客户端。

•命令实现逻辑调用 addReply*()系列函数来将数据附加到客户端结构,这些数据将作为对执行给定命令的回复传输给客户端。

writeToClient()由可写事件处理程序 sendReplyToClient()调用,将输出缓冲区中数据传输到客户端。

readQueryFromClient()是可读的事件处理程序,将从客户端读取的数据缓存到查询缓冲区中。

processInputBuffer()是根据 Redis 协议解析客户端查询缓冲区的入口点。一旦准备好处理命令,它就会调用在 server.c中定义的 processCommand() 以实际执行命令。

freeClient() 释放、断开和删除客户端。

aof.c 和 rdb.c

从名称中您可以猜到,这些文件实现了 Redis 的RDBAOF 持久性。 Redis 使用基于 fork() 系统调用的持久性模型,以便创建与主 Redis 线程具有相同(共享)内存内容的线程。此辅助线程将内存内容转储到磁盘。 rdb.c 使用它在磁盘上创建快照,aof.c 使用它来在仅追加文件变得太大时执行 AOF 重写。

aof.c 中的实现具有附加功能,以便实现一个 API,该 API 允许命令在客户端执行它们时将新命令附加到 AOF 文件中。

在 server.c 中定义的 call() 函数负责调用这些函数,这些函数又会将命令写入 AOF。

db.c

某些 Redis 命令对特定数据类型进行操作;其他都是通用命令。通用命令的示例是 DEL 和 EXPIRE。它们作用于键而不是专门作用于它们的值。所有这些通用命令都在 db.c 中定义。

此外,db.c 实现了一个 API,以便在不直接访问内部数据结构的情况下对 Redis 数据集执行某些操作。

在许多命令实现中使用的 db.c 中最重要的函数如下:

lookupKeyRead()lookupKeyWrite() 用于获取指向与给定键关联的值的指针,如果该键不存在,则为 NULL

dbAdd() 及其更高级别的对应setKey()在 Redis 数据库中创建一个新键。

dbDelete() 删除键及其关联值。

emptyDb() 删除整个单个数据库或所有已定义的数据库。

该文件的其余部分实现了向客户端公开的通用命令。

object.c

已经描述了定义 Redis 对象的 robj 结构。在object.c中,有所有在基本层面上对 Redis 对象进行操作的函数,例如分配新对象、处理引用计数等的函数。此文件中的显着功能:

incrRefCount()decrRefCount()用于增加或减少对象引用计数。当它下降到 0时,对象最终被释放。

createObject()分配一个新对象。还有专门的函数来分配具有特定内容的字符串对象,例如 createStringObjectFromLongLong() 和类似函数。

•该文件还实现了 OBJECT 命令。

replication.c

这是 Redis 中最复杂的文件之一,建议在对代码库的其余部分熟悉之后再处理它。在这个文件中,有 Redis 的 masterreplica 角色的实现。

•该文件中最重要的函数之一是 replicationFeedSlaves(),它向代表连接到主节点的副本实例的客户端写入命令,以便副本可以获取客户端执行的写入操作:这样他们的数据集将与master保持一致。

•该文件还实现了 SYNCPSYNC 命令,这些命令用于在主服务器和副本之间执行第一次同步,或者在断开连接后继续复制。

其他 C 文件

t_hash.ct_list.ct_set.ct_string.ct_zset.ct_stream.c 包含 Redis 数据类型的实现。它们既实现了访问给定数据类型的 API,又实现了这些数据类型的客户端命令实现。

ae.c 实现了 Redis 事件循环,它是一个易于阅读和理解的自包含库。

sds.c 是 Redis 字符串库,查看 http://github.com/antirez/sds 了解更多信息。

anet.c 网络库,与内核公开的原始接口相比,它以更简单的方式使用 POSIX 网络。

dict.c 是一个非阻塞哈希表的实现,它以增量方式rehash

scripting.c 实现 Lua 脚本。它是完全独立的,并且与 Redis 实现的其余部分隔离开来,如果您熟悉 Lua API,就很容易理解。

cluster.c 实现了 Redis 集群。可能只有在非常熟悉 Redis 代码库的其余部分之后才能阅读。如果你想阅读 cluster.c 确保阅读 Redis Cluster 规范。

Redis 命令剖析

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

代码语言:javascript复制
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 中引用该命令:

代码语言:javascript复制
{"foobar",foobarCommand,2,"rtF",0,NULL,0,0,0,0,0},

在上面的示例中,2是命令采用的参数数量,而"rtF"是命令标志,如 server.c 内的命令表顶部注释中所述。

命令以某种方式运行后,它会向客户端返回一个回复,通常使用 addReply()networking.c中定义的类似函数。

Redis 源代码中有大量命令实现,可以作为实际命令实现的示例。编写一些玩具命令可能是熟悉代码库的一个很好的练习。

还有很多其他的文件这里没有描述,但是把所有的东西都覆盖了是没有用的。我们只是想帮助您完成第一步。最终你会找到自己的方式支持 Redis 代码库 :-)

Enjoy!

References

[1] redis.io: https://redis.io

0 人点赞