引言
Redis, REmote DIctionary Server 因其高效、简单、丰富的数据结构支持、高性能、持久化和集群支持等特性得到了程序员们的青睐,并被广泛部署和应用在众多互联网公司。而因为它采用了比较简单的文本协议,使得客户端实现比较简单,因此也拥有众多编程语言实现的客户端;甚至也有一些其它类型的 K-V 数据库兼容了 Redis 协议!
结合我们目前的业务来看,有非常多的场景使用到了 Redis:
- 记录用户通知、短信发送标志,避免重复发送;
- 使用 Redis 集合维护一些白名单用户表;
- 为支持快速获得用户会员时长等基本信息,将这些信息在 Redis 集群中也维护了一份,并采取相关措施维持与 MySQL 数据库的最终一致性。
当然还有很多场景可以例举,但就我们常使用的 Redis 数据结构来看,主要就是 string
, set
, zset
, list
, hash map
。虽说 Redis 给我们提供了其它丰富的内存数据结构,但是我们在生产环境用到的并不多。
既然 Redis 这么重要,自然很有必要花时间去研究下 Redis,并阅读它的源码来学习它的一些设计思想,编程风格等。不得不说,Redis 官方文档非常给力,源码注释很充分,代码风格、质量都是非常一流的。
我们已经了解了 Redis 是什么?为什么那么重要?接下来就是怎么来学习它?然后是期望达成什么样的目标?
首先,《Redis 设计与实现》 和 《Redis 5 设计与源码分析》 将作为学习 Redis 源码的主要参考书籍(「站在巨人的肩膀上」);其次是阅读下 Redis 官方文档中比较重要的部分;当然,最后且最重要的是自己要认真阅读源码。
最后一个问题,期望在学习完 Redis 达成的目标,我想主要有如下几点:
- 培养阅读知名开源项目源码的耐心,掌握阅读源码的技巧和工具;
- 加深对 Redis 的理解,了解它的架构设计;
- 掌握 Redis 常用的数据结构设计思想,并能在实际项目中合理运用各种数据结构实现需求;
- 吸收精髓,提升自身的技术水平,业余时间还可以尝试折腾个简单的数据库。
Redis 特点
Redis 之所以能够有很高的性能,主要有如下几点原因:
- 它是基于内存的数据库,内存的读写速度很快;
- 拥有合理设计的内存数据结构,增删改查很简单,并且能够高效利用内存;
- 使用了 I/O 多路复用的机制(select, poll, epoll, kqueue),高效处理高并发的网络连接;
- 采用了单进程模型(Redis Server 会有多个线程),且只有一个线程专门处理网络请求,避免线程调度带来的上下文切换开销、多线程同步开销(如加锁、释放锁等)。
除此之外,还有如下特点:
<key, value>
中的 value 除了普通的字符串,还支持复杂的数据类型(如集合、字典、位图等);- 支持数据持久化,可在重启后恢复,支持 AOF、RDB 和 AOF + RDB 三种持久化方案;
- 支持主从结构,从节点可做数据备份,也可对外提供读服务;
- 支持集群。
源码概览
后面的源码学习会基于 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 | struct client { |
还有一个比较重要的数据结构是 robj
,它表示一个 Redis 对象,在 Redis 内部实现中有很多地方在使用,它的定义如下:
1 | // redisObject 基本上可以表示所有常用的 Redis 数据类型(lists, sets, strings 等) |
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()
用来处理过期的 keysfreeMemoryIfNeeded()
当 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 进行的操作,而非对应的数据,比如 DEL
和 EXPIRE
等。此外,db.c
中还提供了一些特殊的 API 用于在 Redis 数据集上执行某些操作时,不用访问内部具体的数据结构。
以下是许多命令实现中都会用到的函数:
lookupKeyRead()
和lookupKeyWrite()
用于基于指定 key 得到对应值的指针,如果 key 不存在,则返回NULL
dbAdd()
及更抽象的函数setKey()
是用来在 Redis 中创建新的 keydbDelete()
移除 key 及关联的 valueemptyDb()
移除指定的数据库或者所有的数据库
object.c
struct robj
是 Redis 对象的定义,在 object.c
中有很多应用于 Redis 对象的操作,其中比较关键的函数如下:
incrRefcount()
和decrRefCount()
维护对象的引用计数。当引用值为 0 时才会真正释放对象createObject()
用于分配新的对象。此外,还有一些针对特殊内容分配字符串对象的函数,如createStringObjectFromLongLong()
等
replication.c
replication.c
文件中实现了 master 和 replica 角色。但这块内容比较复杂,建议对 Redis 其它部分代码有所了解后再来学习它。
该文件中有个比较重要的函数 replicationFeedSlaves()
,它用来将命令发送给从节点,从而保证和主节点数据同步。在该文件中还实现了 SYNC
和 PSYNC
命令,它们用于从节点初次初始化同步,或者在连接断开并重连后继续保持同步。
其它
t_hash.c
,t_list.c
,t_set.c
,t_string.c
和t_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 代码啦。有了神器助攻,便于我们通过调试工具追踪调用链,并了解执行步骤中各个中间状态。