第 21 章
源码阅读环境:编译、调试与核心文件地图
第21章 源码阅读环境:编译、调试与核心文件地图
深入理解 Redis 必须直面源码。本章建立完整的源码阅读环境,从编译选项到 GDB 单步调试,从文件地图到启动链路,帮助读者以工程师视角而非用户视角理解 Redis。
21.1 获取源码
Redis 源码托管于 GitHub,主仓库地址为 https://github.com/redis/redis。生产环境建议锁定具体版本标签,避免使用不稳定的 unstable 分支。
git clone https://github.com/redis/redis.git
cd redis
git checkout 7.2.0 # 锁定到 7.2.0 稳定版
git log --oneline -10 # 查看最近提交,了解版本历史
7.x 系列相对 6.x 的主要变化:
- listpack 全面替代 ziplist(6.2 开始逐步,7.0 完成)
- OBJECT ENCODING 输出变化(ziplist → listpack)
- 多线程 I/O 性能优化
- Function(函数)功能正式引入
21.2 编译:保留调试信息
默认 make 使用 -O2 优化,编译器会内联函数、重排代码,调试体验极差。源码阅读时强烈建议关闭优化:
# 关闭优化,保留完整调试符号
make CFLAGS="-g -O0" MALLOC=libc
# 如果使用 jemalloc(Linux 默认)
make CFLAGS="-g -O0" MALLOC=jemalloc
# 并行编译加速
make -j$(nproc) CFLAGS="-g -O0"
# 运行测试套件(需要 Tcl 8.5+)
make test
# 仅运行特定测试
./runtest tests/unit/type/hash.tcl
./runtest tests/unit/expire.tcl
编译产物在 src/ 目录:
src/redis-server:服务端主程序src/redis-cli:命令行客户端src/redis-benchmark:压测工具src/redis-sentinel:哨兵程序(实为 redis-server 的符号链接)src/redis-check-rdb:RDB 文件检查工具src/redis-check-aof:AOF 文件检查工具
启动服务端(调试模式):
src/redis-server --loglevel debug --save "" --appendonly no
# --save "" 禁用 RDB,避免 fork 影响调试
# --appendonly no 禁用 AOF
# --loglevel debug 输出详细日志
21.3 核心文件地图
Redis 7.2 源码约 15 万行 C 代码,分布在 src/ 目录的 150+ 个文件中。以下是最重要的文件分类:
核心框架层
| 文件 | 行数(约) | 作用 |
|---|---|---|
| server.h | 3800 | 全局数据结构定义:redisServer、client、redisObject、dict、listpack 等所有核心结构声明 |
| server.c | 7500 | 服务端主体:main()、initServer()、serverCron()、beforeSleep()、命令表 |
| ae.c | 800 | 事件循环:aeEventLoop、aeProcessEvents、aeCreateFileEvent、aeCreateTimeEvent |
| ae_epoll.c | 150 | epoll 具体实现(Linux) |
| ae_kqueue.c | 150 | kqueue 具体实现(macOS/BSD) |
| ae_select.c | 100 | select 降级实现 |
| ae_evport.c | 150 | Solaris event port 实现 |
网络与协议层
| 文件 | 行数(约) | 作用 |
|---|---|---|
| networking.c | 4200 | 客户端连接管理、RESP 协议解析、readQueryFromClient、addReply 系列函数 |
| resp3.c | 200 | RESP3 协议扩展(Redis 6+) |
| tls.c | 1200 | TLS/SSL 支持(需要 OpenSSL) |
| unix.c | 100 | Unix Domain Socket 支持 |
数据类型命令层
| 文件 | 行数(约) | 作用 |
|---|---|---|
| t_string.c | 900 | String 命令:set/get/incr/append/getrange/setrange 等 |
| t_hash.c | 800 | Hash 命令:hset/hget/hmset/hgetall 等 |
| t_list.c | 700 | List 命令:lpush/rpush/lrange/blpop 等 |
| t_set.c | 900 | Set 命令:sadd/smembers/sinter/sunion 等 |
| t_zset.c | 2800 | ZSet 命令 + 跳表完整实现(zskiplist/zskiplistNode) |
| t_stream.c | 3500 | Stream 命令:xadd/xread/xgroup/xack 等 |
| t_xset.c | 100 | 扩展集合 |
底层数据结构层
| 文件 | 行数(约) | 作用 |
|---|---|---|
| sds.c / sds.h | 1000 | SDS 动态字符串:sdsnew/sdscat/sdsMakeRoomFor |
| dict.c / dict.h | 1500 | 渐进式哈希表:dictCreate/dictAdd/dictFind/dictRehash |
| listpack.c / listpack.h | 1100 | listpack 紧凑列表(7.0 主力编码格式) |
| ziplist.c | 1500 | ziplist(7.0 起逐步废弃,保留兼容性) |
| quicklist.c | 1200 | quicklist:listpack 节点的双向链表 |
| intset.c | 400 | intset:整数集合(小 Set 编码) |
| skiplist(内嵌 t_zset.c) | - | 跳表实现 |
| rax.c / rax.h | 1600 | Radix Tree(前缀树),用于 Stream、集群路由 |
持久化层
| 文件 | 行数(约) | 作用 |
|---|---|---|
| rdb.c | 3500 | RDB 持久化:rdbSave/rdbLoad/rdbSaveObject |
| rdb.h | 200 | RDB 格式常量定义 |
| aof.c | 3000 | AOF 持久化:feedAppendOnlyFile/rewriteAppendOnlyFile |
| rio.c / rio.h | 500 | 流式 I/O 抽象:支持 file/buffer/fd 多种后端 |
复制与集群层
| 文件 | 行数(约) | 作用 |
|---|---|---|
| replication.c | 5500 | 主从复制:PSYNC 协议、backlog、部分同步 |
| sentinel.c | 5000 | 哨兵模式:故障检测、选举、通知 |
| cluster.c | 10000+ | 集群模式(最大文件):Gossip、槽迁移、故障转移 |
| cluster.h | 500 | 集群数据结构定义 |
内存管理层
| 文件 | 行数(约) | 作用 |
|---|---|---|
| object.c | 1200 | redisObject 创建/释放/引用计数/类型检查/编码转换 |
| db.c | 1000 | dbAdd/dbDelete/expireIfNeeded/lookupKey/selectDb |
| expire.c | 300 | 定期删除:activeExpireCycle |
| evict.c | 600 | 内存淘汰:8种策略实现,performEvictions |
| lazyfree.c | 400 | 后台惰性删除:bioCreateLazyFreeJob |
| zmalloc.c | 400 | 内存分配封装:zmalloc/zfree/used_memory 统计 |
| memory.c | 500 | MEMORY 命令、碎片统计、activedefrag |
后台任务层
| 文件 | 行数(约) | 作用 |
|---|---|---|
| bio.c | 400 | 后台 I/O 线程:3类任务(fsync/lazyFree/closeFd) |
| threads_mngr.c | 200 | I/O 多线程管理(Redis 6+) |
21.4 GDB 调试实战
调试 SET 命令
# 终端1:启动 GDB
gdb src/redis-server
(gdb) break setCommand # 在 t_string.c 的 setCommand 设置断点
(gdb) break t_string.c:100 # 按行号设置断点(需先查看行号)
(gdb) run redis.conf # 启动 server
# 终端2:发送命令
redis-cli SET hello world
# 终端1:GDB 停在断点
(gdb) bt # 查看调用栈(backtrace)
(gdb) frame 0 # 切换到第0帧
(gdb) info args # 查看函数参数
(gdb) p c->argc # 查看客户端参数数量(应为3: SET hello world)
(gdb) p c->argv[0]->ptr # 查看第一个参数(命令名)
(gdb) p c->argv[1]->ptr # 查看 key
(gdb) p c->argv[2]->ptr # 查看 value
(gdb) n # 单步执行(next,不进入函数)
(gdb) s # 单步进入函数(step)
(gdb) c # 继续执行(continue)
查看 redisObject 结构
(gdb) p *c->argv[1] # 打印 redisObject(key)
# 输出类似:
# {type = 0, encoding = 8, lru = 12345678, refcount = 1, ptr = 0x7f...}
(gdb) p (char*)c->argv[1]->ptr # 如果是 embstr/raw,直接打印字符串
(gdb) x/10c c->argv[1]->ptr # 以字符格式查看内存(前10字节)
调试时间事件(serverCron)
(gdb) break serverCron
(gdb) commands # 设置断点命中时自动执行命令
> print server.hz
> print server.unixtime
> continue
> end
常用 GDB 命令速查
break func 在函数入口设断点
break file:line 在指定行设断点
watch expr 监视表达式变化(写断点)
rwatch expr 监视表达式被读取(读断点)
info break 列出所有断点
delete 1 删除断点1
print expr 打印表达式值
x/Nfu addr 检查内存(N=数量, f=格式, u=单位)
ptype var 打印变量类型
whatis var 查看变量类型(简版)
set var=value 修改变量值(注入数据)
finish 执行完当前函数并返回
up/down 切换调用栈帧
info locals 显示当前帧所有局部变量
21.5 启动链路全解析
Redis 的启动链路从 main() 开始,理解这个链路对理解 Redis 架构至关重要。
// server.c
int main(int argc, char **argv) {
// 1. 时区初始化
setlocale(LC_COLLATE, "");
tzset();
// 2. 内存分配异常处理器
zmalloc_set_oom_handler(redisOutOfMemoryHandler);
// 3. 随机种子初始化
srand(time(NULL)^getpid());
// 4. 默认配置初始化(填充 server 全局结构)
initServerConfig();
// 5. 模块系统初始化(Module API)
moduleInitModulesSystem();
// 6. 解析命令行参数,加载配置文件
loadServerConfig(configfile, options);
// 7. 哨兵/集群模式检测
if (server.sentinel_mode) initSentinelConfig();
// 8. ACL 系统初始化
ACLInit();
// 9. 正式初始化服务端(创建 eventloop、监听端口等)
initServer();
// 10. 模块加载
moduleLoadFromQueue();
// 11. 从磁盘加载数据(RDB 或 AOF)
loadDataFromDisk();
// 12. 集群/哨兵模式特殊初始化
if (server.cluster_enabled) clusterInit();
// 13. 进入事件循环主体(永不返回)
aeSetBeforeSleepProc(server.el, beforeSleep);
aeSetAfterSleepProc(server.el, afterSleep);
aeMain(server.el);
// 14. 事件循环结束(shutdown时)
aeDeleteEventLoop(server.el);
return 0;
}
initServer() 关键步骤
void initServer(void) {
// 创建事件循环(核心!)
server.el = aeCreateEventLoop(server.maxclients + CONFIG_FDSET_INCR);
// 创建数据库数组(默认16个DB)
server.db = zmalloc(sizeof(redisDb) * server.dbnum);
for (int i = 0; i < server.dbnum; i++) {
server.db[i].dict = dictCreate(&dbDictType);
server.db[i].expires = dictCreate(&dbExpiresDictType);
server.db[i].blocking_keys = dictCreate(&keylistDictType);
server.db[i].watched_keys = dictCreate(&keylistDictType);
server.db[i].id = i;
}
// 监听 TCP 端口
listenToPort(server.port, &server.ipfd);
// 注册 serverCron 时间事件(每1ms检查一次,内部控制实际执行频率)
aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL);
// 注册 accept 文件事件
for (int i = 0; i < server.ipfd.count; i++) {
aeCreateFileEvent(server.el, server.ipfd.fd[i], AE_READABLE,
acceptTcpHandler, NULL);
}
// 初始化 LRU 时钟
server.lruclock = getLRUClock();
}
serverCron 任务调度
serverCron 是 Redis 的心跳函数,每次 aeProcessEvents 循环都会检查是否到期执行:
// 实际执行频率由 server.hz 控制(默认10,即100ms一次)
// 但通过 run_with_period 宏控制不同任务的执行间隔
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
// 每次都执行
server.unixtime = time(NULL);
server.mstime = mstime();
server.ustime = ustime();
updateCachedTime(0);
// 每100ms
run_with_period(100) {
trackInstantaneousMetric(STATS_METRIC_COMMAND, ...);
activeExpireCycle(ACTIVE_EXPIRE_CYCLE_FAST); // 快速过期扫描
}
// 每1000ms(1秒)
run_with_period(1000) {
dbCron(); // DB 相关维护
replicationCron(); // 主从复制心跳
}
// 每5000ms
run_with_period(5000) {
// ... 统计数据更新
}
// BGSAVE / BGREWRITEAOF 子进程检查
if (server.rdb_child_pid != -1 || server.aof_child_pid != -1) {
// waitpid 检查子进程是否完成
}
return 1000 / server.hz; // 返回下次执行的毫秒数
}
21.6 RESP 协议解析源码
理解 networking.c 中的 readQueryFromClient 是理解 Redis 网络模型的基础:
// 客户端发送 "SET hello world\r\n" 后触发
void readQueryFromClient(connection *conn) {
client *c = connGetPrivateData(conn);
// 从连接读取数据到 querybuf
nread = connRead(c->conn, c->querybuf + qblen, readlen);
// 解析 RESP 协议
processInputBuffer(c);
}
void processInputBuffer(client *c) {
while (c->qb_pos < sdslen(c->querybuf)) {
if (c->reqtype == PROTO_REQ_MULTIBULK) {
// 解析 *3\r\n$3\r\nSET\r\n$5\r\nhello\r\n$5\r\nworld\r\n
if (processMultibulkBuffer(c) != C_OK) break;
}
// 命令解析完成后调用 processCommand
if (c->argc > 0) {
processCommand(c);
}
}
}
21.7 阅读源码的方法论
1. 从命令入手:每个 Redis 命令对应一个 C 函数,在 server.c 的 redisCommandTable 数组中定义:
struct redisCommand redisCommandTable[] = {
{"get", getCommand, 2, "read-only fast @string", ...},
{"set", setCommand, -3, "write use-memory @string", ...},
{"hset", hsetCommand, -4, "write use-memory fast @hash", ...},
// ...
};
2. 利用 ctags/cscope:
ctags -R src/ # 生成 tags 文件
# Vim 中:Ctrl+] 跳转定义,Ctrl+T 返回
3. 使用 VSCode + clangd:配置 compile_commands.json,获得精确的跳转和类型推导。
4. 关注 redisObject:几乎所有数据最终存储在 robj(redisObject)中,理解它的 type/encoding/ptr 三元组是理解所有数据结构编码转换的基础。
typedef struct redisObject {
unsigned type:4; // OBJ_STRING/OBJ_LIST/OBJ_SET/OBJ_ZSET/OBJ_HASH
unsigned encoding:4; // OBJ_ENCODING_RAW/EMBSTR/INT/LISTPACK/SKIPLIST...
unsigned lru:24; // LRU 时间戳 或 LFU 计数器
int refcount; // 引用计数(共享对象:0-9999整数)
void *ptr; // 指向实际数据(或直接存整数值)
} robj;
本章小结
- Redis 7.2 源码约15万行,核心文件集中在
src/目录 - 编译时使用
-g -O0保留完整调试信息 - GDB 调试可以单步跟踪任意命令执行路径
- 启动链路:
main()→initServerConfig()→initServer()→loadDataFromDisk()→aeMain()→ 永久事件循环 serverCron是系统的"心跳",集中调度所有定时任务redisObject的 type/encoding/ptr 三元组是理解所有数据编码的钥匙- 从
redisCommandTable入手,每个命令都有对应的 C 函数入口