gcache 源码学习

引言

Redis, REmote DIctionary Server 因其高效、简单、丰富的数据结构支持、高性能、持久化和集群支持等特性得到了程序员们的青睐,并被广泛部署和应用在众多互联网公司。而因为它采用了比较简单的文本协议,使得客户端实现比较简单,因此也拥有众多编程语言实现的客户端;甚至也有一些其它类型的 K-V 数据库兼容了 Redis 协议!

结合我们目前的业务来看,有非常多的场景使用到了 Redis:

  1. 记录用户通知、短信发送标志,避免重复发送;
  2. 使用 Redis 集合维护一些白名单用户表;
  3. 为支持快速获得用户会员时长等基本信息,将这些信息在 Redis 集群中也维护了一份,并采取相关措施维持与 MySQL 数据库的最终一致性。

当然还有很多场景可以例举,但就我们常使用的 Redis 数据结构来看,主要就是 string, set, zset, list, hash map。虽说 Redis 给我们提供了其它丰富的内存数据结构,但是我们在生产环境用到的并不多。

既然 Redis 这么重要,自然很有必要花时间去研究下 Redis,并阅读它的源码来学习它的一些设计思想,编程风格等。不得不说,Redis 官方文档非常给力,源码注释很充分,代码风格、质量都是非常一流的。

我们已经了解了 Redis 是什么?为什么那么重要?接下来就是怎么来学习它?然后是期望达成什么样的目标?

首先,《Redis 设计与实现》《Redis 5 设计与源码分析》 将作为学习 Redis 源码的主要参考书籍(「站在巨人的肩膀上」);其次是阅读下 Redis 官方文档中比较重要的部分;当然,最后且最重要的是自己要认真阅读源码。

最后一个问题,期望在学习完 Redis 达成的目标,我想主要有如下几点:

  1. 培养阅读知名开源项目源码的耐心,掌握阅读源码的技巧和工具;
  2. 加深对 Redis 的理解,了解它的架构设计;
  3. 掌握 Redis 常用的数据结构设计思想,并能在实际项目中合理运用各种数据结构实现需求;
  4. 吸收精髓,提升自身的技术水平,业余时间还可以尝试折腾个简单的数据库。

Redis 特点

Redis 之所以能够有很高的性能,主要有如下几点原因:

  1. 它是基于内存的数据库,内存的读写速度很快;
  2. 拥有合理设计的内存数据结构,增删改查很简单,并且能够高效利用内存;
  3. 使用了 I/O 多路复用的机制(select, poll, epoll, kqueue),高效处理高并发的网络连接;
  4. 采用了单进程模型(Redis Server 会有多个线程),且只有一个线程专门处理网络请求,避免线程调度带来的上下文切换开销、多线程同步开销(如加锁、释放锁等)。

除此之外,还有如下特点:

  1. <key, value> 中的 value 除了普通的字符串,还支持复杂的数据类型(如集合、字典、位图等);
  2. 支持数据持久化,可在重启后恢复,支持 AOF、RDB 和 AOF + RDB 三种持久化方案;
  3. 支持主从结构,从节点可做数据备份,也可对外提供读服务;
  4. 支持集群。

源码概览

后面的源码学习会基于 Redis 最新的稳定版 5.0.7,参见仓库:https://github.com/iFaceless/redis/tree/5.0,源码注释会推送到该仓库的 comment-src 分支下。

关于 Redis 源码的结构,在 Redis 的 README.md 中有所介绍。具体来说,有如下几个重要的目录:

  • src: Redis 核心实现(C 语言)
  • tests: 单元测试代码(Tcl 语言)
  • deps: Redis 依赖的一些库。其中包含 jemalloc 源码,它是 Redis 在 Linux 下默认的内存分配库,用来替代标准库 malloc,以期减少内存分配碎片

server.h

server.h 中定义了 Redis Server 需要用到的一些数据结构,其中 struct redisServer 维护了 Redis 服务端配置和共享状态,几个比较重要的字段如下:

  • db: 表示 Redis 数据库,用来存储数据
  • commands: 命令表
  • clients: 连接到当前服务器的客户端链表
  • master: replica 节点 master 客户端

另外一个重要的数据结构是 redisClient,用来表示客户端。这里给介绍几个重要的字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct client {
int fd;
// 存放客户端请求
sds querybuf;
int argc;
robj **argv;
redisDb *db;
int flags;
// reply & buf 维护服务端要发给客户端的回复队列,当底层的 fd 可写时,会以渐进地方式发送缓冲区数据
list *reply;
char buf[PROTO_REPLY_CHUNK_BYTES];
... many other fields ...
}

还有一个比较重要的数据结构是 robj,它表示一个 Redis 对象,在 Redis 内部实现中有很多地方在使用,它的定义如下:

1
2
3
4
5
6
7
8
9
10
11
// redisObject 基本上可以表示所有常用的 Redis 数据类型(lists, sets, strings 等)
typedef struct redisObject {
// type 表示具体的数据类型
unsigned type:4;
unsigned encoding:4;
unsigned lru:LRU_BITS; /* lru time (relative to server.lruclock) */
// refcount 表示对象引用次数,借助引用计数的方式,避免为重复对象分配内存
int refcount;
// ptr 指向底层的真正的对象表示,结合 `encoding` 进行解析
void *ptr;
} robj;

server.c

Redis Server 启动的入口定义在此处(参见 main 函数)。下面是 Redis Server 启动时需要进行的重要步骤:

  • initServerConfig() 用来配置 struct server 的默认值
  • initServer() 分配一些必要的数据结构、配置监听的 socket 等
  • aeMain() 启动 Event Loop,监听新的连接

Event Loop 中会周期调用的两个特殊函数如下:

  • serverCron() 会被周期调用(参考 server.hz 配置的频率),执行一些周期性的任务,如检查客户端超时等
  • beforeSleep() 会在每次进入事件驱动库主循环时调用,也就是在睡眠等待 ready 的文件描述符之前

server.c 中还有几个函数专门处理其它类型的重要任务:

  • call() 会在指定的客户端上下文中执行指定命令时被使用
  • activeExpireCycle() 用来处理过期的 keys
  • freeMemoryIfNeeded() 当 Redis 内存使用超过 maxmemory 指定的值,且有新的写入进来时,会执行该函数清理内存
  • redisCommandTable 维护了所有 Redis 命令,其中包含每个命令的名称、回调函数、参数个数及其它属性

networking.c

在这个文件中定义了所有的 I/O 函数,用来和客户端、master 及 replicas 交互:

  • createClient() 初始化新的客户端
  • addReply*() 函数族用于给客户端添加响应数据
  • writeToClient() 用于将输出缓冲区的数据发送给客户端,它会被 sendReplyToClient() 调用
  • readQueryFromClient() 用于聚集从客户端读取的数据到查询缓冲区
  • processInputBuffer() 是从客户端查询缓冲区(query buffer)根据 Redis 协议解析查询命令的入口函数。一旦命令可以处理了,就会调用 processCommand() 来真正执行命令
  • freeClient() 释放客户端

aof.c 和 rdb.c

顾名思义,这是 Redis 两种持久化方案的具体实现文件。Redis 的持久化模型比较有趣,它会通过 fork() 系统调用创建一个单独的线程,并能访问主线程共享的内存区域;接下来这个备份线程会将内存内容持久化到磁盘中。rdb.c 在创建快照时会使用这种机制;aof.c 在执行 AOF 重写(避免 Append Only 文件过大)时也会用到这个机制。

db.c

db.c 中定义了一些通用的操作命令,它们都是针对 key 进行的操作,而非对应的数据,比如 DELEXPIRE 等。此外,db.c 中还提供了一些特殊的 API 用于在 Redis 数据集上执行某些操作时,不用访问内部具体的数据结构。

以下是许多命令实现中都会用到的函数:

  • lookupKeyRead()lookupKeyWrite() 用于基于指定 key 得到对应值的指针,如果 key 不存在,则返回 NULL
  • dbAdd() 及更抽象的函数 setKey() 是用来在 Redis 中创建新的 key
  • dbDelete() 移除 key 及关联的 value
  • emptyDb() 移除指定的数据库或者所有的数据库

object.c

struct robj 是 Redis 对象的定义,在 object.c 中有很多应用于 Redis 对象的操作,其中比较关键的函数如下:

  • incrRefcount()decrRefCount() 维护对象的引用计数。当引用值为 0 时才会真正释放对象
  • createObject() 用于分配新的对象。此外,还有一些针对特殊内容分配字符串对象的函数,如 createStringObjectFromLongLong()

replication.c

replication.c 文件中实现了 master 和 replica 角色。但这块内容比较复杂,建议对 Redis 其它部分代码有所了解后再来学习它。

该文件中有个比较重要的函数 replicationFeedSlaves(),它用来将命令发送给从节点,从而保证和主节点数据同步。在该文件中还实现了 SYNCPSYNC 命令,它们用于从节点初次初始化同步,或者在连接断开并重连后继续保持同步。

其它

  • t_hash.c, t_list.c, t_set.c, t_string.ct_zset.c 是 Redis 数据类型的底层实现
  • ae.c Redis 事件循环实现
  • sds.c Redis 动态变长字符串
  • anet.c 对内核提供的网络接口做了封装,从而能够以更加简单的方式使用 POSIX 网络接口
  • dict.c 非阻塞、渐进 rehash 字典实现
  • scripting.c 实现了 Luc 脚本
  • cluster.c Redis 集群实现。在了解这块代码前,记得参考下 Redis 集群说明

思维导图

构建 & 调试

Redis 官方文档中有关于构建和运行它的详细说明,我们也可以使用 gdb 进行调试。但是,作为一个 IDE 死忠党,自然是要在 Clion 中构建和调试 Redis 的。需要注意的是,Clion 使用了 cmake 来管理项目,所以我们需要在 Redis 源码根目录下为它创建好 CMakeLists.txt 才能进行构建。具体可参考 使用 Clion 来调试 Redis 源码 这篇文章~

在完成上一步后,切到 src 目录下,执行 ./mkreleasehdr.sh 脚本生成 src/release.h 文件,否则构建可能会失败。然后在源码目录下执行:

1
cmake .

接下来,我们可以在 Clion 打开 src/server.c ,并找到 main 函数,点击工具栏中的运行或调试按钮,或者点击 main 函数左侧的按钮选择运行或调试。

Redis 服务启动时,默认会使用 6379 端口,也可以在 Clion 中配置参数,使用自定义的端口等:

至此,我们已经可以使用 Clion 来阅读、运行或者调试 Redis 代码啦。有了神器助攻,便于我们通过调试工具追踪调用链,并了解执行步骤中各个中间状态。

参考

  1. Redis README
  2. Clion 调试 Redis 源码
  3. 使用 Clion 来调试 Redis 源码
  4. 《Redis 5 设计与源码分析》
0%