考试通知

Redis源码分类指南:按分层与职责拆解核心文件,高效阅读不再迷路

Redis源码分类指南:按分层与职责拆解核心文件,高效阅读不再迷路 1. 项目概述为什么要对 Redis 源码做分类Redis 在服务端开发里的地位基本不用多说。几乎每家互联网公司都在用不只是做缓存还承担了分布式锁、排行榜、消息队列、限流、布隆过滤器这些事。但很多人用 Redis 用得熟练读写命令张嘴就来真到要去看源码的时候却完全不知道从哪下手。我曾经也是这个状态。clone 下来一份 Redis 源码压到本地一看好家伙src 目录下上百个 C 文件有的一眼看得出是干嘛的比如 dict.c、t_string.c有的光看名字根本猜不透比如 object.c、evict.c、replication.c。当时硬着头皮从头读读到第 5 个文件就放弃了。后来换了思路不再按文件顺序读而是先对整个源码做一次分类把文件按它是干嘛的、处于哪个层级、跟核心请求链路有什么关系归好类思路立刻清晰了。这篇文章基于Redis 源码分类这个主题把我自己梳理源码的完整过程写出来。重点解决三类人有需求刚接触 Redis 源码、想找切入点但被文件数量劝退的人。已经在业务里大量使用 Redis、想知道命令到底怎么执行、数据到底怎么存的进阶开发者。准备面试 Redis 方向、需要在有限时间内抓住源码主线的候选人。我会用实际的文件名做例子给出我的分类原则、分类表、阅读顺序建议以及复刻这套方法的具体步骤。这套分类方法不只适用于 Redis任何体量偏大的 C 项目源码你都可以套用同样的思路。2. 分类方法论不要按字母序要按分层 职责两个维度拆2.1 为什么按字母序读源码一定失败很多新手拿到源码第一步是打开目录按文件名一个接一个往下读。这个做法基本必败。原因也很简单源码文件之间的依赖关系不是平铺的而是纵向的。你读 aiocron.c 的时候根本没法理解它调用的 aeMain 是从哪来的读 bio.c 的时候又找不到它跟 lazy free 之间有什么关系。字母序把底层实现、中间调度、上层命令处理完全搅在一起你的脑子里建立不起来调用链条。更致命的是大量文件之间是互相引用的。Redis 的结构体定义集中在 server.h 里几百个开发的人、几十年累积的代码几乎所有文件都 include server.h。如果你按字母序走到第 30 个文件才第一次完整理解 RedisServer 结构体长什么样前面那 30 个文件基本白读了。2.2 我自己的分类维度核心链路优先周边功能分块我用了两个维度同时切。第一个维度叫纵向分层。从最底层的系统抽象到中间的基础数据结构再到上层的命令实现最后到网络协议和事件循环一共四层。这个维度解决的是代码在调用链上的位置问题。第二个维度叫横向模块把同层但职责不同的文件各归各的类。比如同样是基础数据结构dict 管哈希表、ziplist 管紧凑列表、quicklist 管双向链表和压缩列表的组合它们被不同命令以不同方式使用但归类上属于同一层。横向模块和纵向分层叠加之后整个 Redis 源码就能摊成一张表什么文件处于什么位置、服务于什么功能一目了然。后面我读任何一条命令的执行流程先在表里定位涉及的文件再按链路上往下读就不会迷路。2.3 分类过程中需要抓住的一条主线Redis 源码看着散其实有一条绝对主线一条命令从客户端进来最终变成数据结构的读写操作中间经历了多少个环节每个环节对应哪些文件。这条主线是客户端发命令 - TCP 传输 - 事件循环捕获可读事件 - 读取协议数据 - 解析命令参数 - 查找命令表 - 调用对应的命令处理函数 - 参数校验 - 操作底层数据结构 - 完成持久化或通知 - 写回响应 - 事件循环继续下一个事件。源码分类时我把所有文件往这条链路上挂。挂得上的优先理解挂不上的比如配置文件解析、哨兵、集群相关先单独分块后续再补。3. 成品分类表我把 Redis 源码拆成了这几块3.1 入口与核心服务器骨架这一块是 Redis 启动、运行、退出的中枢。没有它其它功能都跑不起来。范畴文件职责一句话主入口server.cRedis 主流程、初始化、cron、命令执行入口核心结构定义server.h全局数据结构、所有核心结构体定义配置解析config.c读取 redis.conf构建服务器配置日志与统计util.c / stats.c通用工具函数、运行时统计信息server.c 是整个程序的心脏main 函数在里面所有初始化步骤的顺序也都在里面。先看 main 能建立全局观。server.h 则是所有文件的头几乎所有结构体、全局变量声明都汇聚在这里。3.2 事件循环与网络抽象层Redis 是单线程处理命令的这个单线程指的就是事件循环。要理解Redis 为什么快这个部分必须读透。核心文件就两个ae.c 和 ae_epoll.cLinux 平台。ae.c 是跨平台事件循环的抽象层对外提供了统一的接口ae_epoll.c 是底层对 epoll 的具体封装。除这两个之外还有anet.cTCP 连接的封装非阻塞建连、读写等待都在这。networking.c在 Redis 里管的是客户端连接对象、缓冲区、输出响应。bio.c后台线程任务主要是异步删除这类耗时的操作丢到后台线程。拿到这个模块重点看两个点。第一epoll 事件如何被检测到并分发到对应回调。第二一个客户端请求从可读事件被触发到读缓冲区再到命令解析这段是怎么串起来的。这决定了整个 Redis 的并发模型基调。3.3 内存编码与基础数据结构实现Redis 之所以被称为数据结构服务器全在这一层。业务里的 string、list、hash、set、zset 五种类型在底层是由多种不同编码方式实现的。这一层的代码质量极高非常值得精读。类型底层编码实现文件说明stringsds.c / sds.h动态字符串所有 key、value 的存储基础list 旧版adlist.c普通双向链表指针list 紧凑存储quicklist.c多个 ziplist 用双向链表串起来的组合结构listpacklistpack.c新版紧凑列表替代 ziplist 的演进方向hash 紧凑存储ziplist.c / listpack.c用紧凑数组实现的小哈希hash 常规存储dict.c哈希表支持渐进式扩容和收缩zset 紧凑存储ziplist.c / listpack.c小 zset 用紧凑结构存 score 和 memberzset 跳表存储t_zset.c / skiplist.c跳表实现有序集合的核心set 整数存储intset.c全整数集合的有序紧凑数组这一层的核心数据结构有四个必须啃透sds、dict、quicklist、跳表。它们各自的使用场景、时间复杂度、退化条件面试里高频出现也直接影响你对业务选型的判断。3.4 命令解析与命令实现层命令实现层是业务理解最直接的地方。比如你执行set foo bar到底调到哪个函数、里面做了什么全在这一层。但实际上命令实现并不是一个庞大的文件而是一个分散式的文件集。Redis 的源码里命令处理函数分布在几个文件t_string.cstring 类型所有命令set、get、incr、decr、append 等。t_list.clist 类型所有命令lpush、rpush、lpop、rpop、llen 等。t_hash.chash 类型所有命令hset、hget、hdel、hlen 等。t_set.cset 类型所有命令sadd、srem、sismember、sinter 等。t_zset.czset 类型所有命令zadd、zrange、zrank、zscore 等。db.c键空间操作key 的过期、淘汰、查找都在这。t_ 前缀其实是 type 的缩写代表某种数据类型。看到这些文件直接就能归类到数据类型命令实现。另外有两个比较特殊的文件commands.c / commands.h命令表定义把命令名字符串映射到处理函数指针。script_lua.c / script.cRedis 脚本执行Lua的核心。3.5 持久化、复制与高可用这一块是 Redis 从缓存玩具变成生产级存储的关键。文件数量和复杂度都偏高。rdb.cRDB 快照持久化全量内存数据序列化到磁盘。aof.cAOF 日志持久化每一条写命令记录成日志。replication.c主从复制协议全量同步和增量同步都在这里。sentinel.c哨兵模式监控主节点、自动切换。cluster.c集群分片、槽位管理、节点间通信。failover.c / gossip 相关故障感知和状态传播。这个模块可以先不精读但要知道每个文件解决什么问题。等你把主线读通之后再回过头来啃持久化和复制会觉得简单很多因为它们的核心还是在操作底层数据结构只是多了网络交互和状态机。3.6 内存管理、回收与策略算法Redis 是内存数据库所以内存管理策略是它的命门。evict.c内存淘汰策略的落地实现。LRU、LFU 的近似采样逻辑都在这。expire.ckey 过期机制的实现惰性删除和定期删除以及淘汰触发时机。lazyfree.c异步删除实现把释放内存的大操作放到后台线程。object.cRedis 对象的创建、编码转换、引用计数管理。对象共享也在其中。malloc_usable_size 相关的内存统计逻辑散落在 zmalloc.c。zmalloc.c 值得单列出来讲一下它是对系统内存分配函数的一层封装除了统计内存占用还做了内存对齐和异常处理。所有 Redis 对象分配内存都过它所以看内存增长、内存碎片化离不开这个文件。3.7 扩展模块与辅助工具这部分不属于主链路但在特定场景下很重要。modules.cRedis 模块系统允许用 C 写插件扩展命令。multi.c事务命令 MULTI/EXEC/WATCH 的实现。pubsub.c发布订阅机制。blocking.c阻塞命令的实现。比如 BLPOP 怎么让客户端挂起等待。notify.c键空间通知把内部操作转发给订阅者。sort.cSORT 命令的实现。slowlog.c慢查询日志。debug.c调试辅助命令。还有一些工具文件比如 sha1.c、crc64.c、rand.c是各种基础算法实现。4. 实操经验我重新组织一遍分类的详细步骤4.1 第一步先把src目录全部过一遍粗分类我的做法是先不要读任何代码纯靠文件名先建一个粗分类的表格。这一遍的目标不是理解代码而是给自己画一张地图。具体做法打开目录把所有.c文件名列出来。然后凭文件名猜功能比如 t_string.c 里应该是 string 类型命令ae.c 里应该是事件循环rdb.c 应该是持久化。猜得准的直接归类猜不出来的先丢进待定区。这一步不需要任何代码基础纯靠命名规律和常识就能完成。别小看它它能帮你在后续阅读过程中随时回到地图上定位。4.2 第二步用git log和头文件建立依赖图粗分类之后第二步是把那些你猜不出功能的文件搞清楚。我最常用的方法先打开文件开头看看它 include 了什么。比如某个文件里 include 了 server.h 和 rdb.h说明它跟服务器核心和持久化都有关系。再辅助一个技巧用git log --oneline -- 文件名看这个文件的历史提交信息。提交说明往往直接写着implement xxx、fix xxx memory leak这样的描述。比如某个文件你完全看不懂看提交历史发现都是关于命令复制的立刻就能知道它属于复制模块。4.3 第三步以命令执行链路为线索做验证分类完成之后需要验证分类是否正确。验证方式不能靠人眼比对而是实际跟踪一条命令的执行过程。以get foo为例客户端连接在 networking.c 中被接受注册可读事件。数据到达事件循环 ae.c 检测到可读事件调用 readQueryFromClient。数据写入输入缓冲区进入 processInputBuffer再进 processCommand。processCommand 在 server.c 中实现里面做命令查找。命令表在 commands.c 中定义找到 getCommand。getCommand 在 t_string.c 中实现内部调用 lookupKeyRead。lookupKeyRead 在 db.c 中最终从 dict 里找到对应的 sds 字符串。你实际跟一遍这条链路之后会发现前面分类表里的所有模块都被串联起来了。这一步同时也是对分类是否正确的终极检验。4.4 第四步给每个文件标记阅读优先级分类完成不代表要通读所有文件。我给自己的文件列表加了三档标记P0主线必读。包括 ae.c、networking.c、server.c、db.c、dict.c、sds.c、t_string.c 中的主要函数。P1功能必读。包括 rdb.c、aof.c、replication.c 的关键路径函数、evict.c 的淘汰逻辑、expire.c 的过期逻辑。P2按需精读。包括 cluster.c、sentinel.c、modules.c、debug.c 和所有工具算法文件。分类表里同时记录这个优先级这样每次开始读之前你就知道该把精力放在哪。5. 带着分类表去读三大核心链路5.1 链路一一条读命令的完整旅程读命令的链路相对简单但必须先读通。入口点在 processCommand 或 processCommandAndResetClient。server.c 里这个函数做的事包括检查命令是否被禁用、是否在事务中、是否需要排队。查找命令表找到对应函数。检查参数个数和权限。调用命令函数。命令函数内部比如 t_string.c 的 getCommand调用 lookupKeyReadWithFlags从 db 里取 key。如果 key 不存在或者已经过期返回空。如果 value 的编码不对走对象类型检查。把对象返回给客户端。这个过程中你会在 db.c 里看到 expireIfNeeded 被调用于是又触达过期模块。在 networking.c 里看到 addReply 被调用于是又触达响应输出模块。整条链路的阅读过程本质就是不断跳转到分类表里其它模块的过程。5.2 链路二一条写命令如何动到持久化以set foo bar为例它比 get 多几个环节t_string.c 的 setCommand 检查参数。对 key 和 value 做编码转换决定最终用 sds 还是 int 编码。写入 db-dict用 setKey。signalModifiedKey 通知键空间事件。触发 dirty 计数增加为后续持久化做标记。如果 AOF 开启把命令写入 AOF 缓冲。如果复制开启把命令广播给从节点。这个链路在 3.4 节的命令实现层之外直接串到了 aof.c 和 replication.c。平时很多人只知道save触发 RDB实际看完这个链路才能理解 RDB 是内存状态的落盘AOF 是命令流的落盘 这句话的分量。5.3 链路三内存淘汰是怎么和读命令耦合的这也是 Redis 源码里隐藏最深的一条逻辑。很多人的认知里内存淘汰只发生在写入时。但看 lookups 系列函数的源码你会发现读命令也可能触发淘汰逻辑。核心在 expire.c 的 expireIfNeeded它做的事是检查 key 是否设置了过期时间。如果过期了在 master 节点上删除 key。如果正好在主从复制场景需要把 DEL 命令传播给从节点。在 db.c 的 lookupKeyReadWithFlags 里每次读 key 都会调用 expireIfNeeded并且可以设置 LUT 标志跳过某些检查用在特别路径里。这一块弄清楚了你才能真正理解Redis 的过期不是靠后台线程统一扫地而是由访问路径上的惰性删除 周期性的 activeExpireCycle 共同完成。6. 阅读顺序建议源码分类表的正确打开方式分类表是地图但有了地图不代表你不会迷路。我推荐的阅读顺序是第一阶段第 1 周只读server.h、sds.h/sds.c、ae.c的事件循环主框架。目标是建立全局结构 字符串实现 事件循环三个坐标系。第二阶段第 2 周读networking.c的命令读取流程结合server.c的 processCommand。目标是把读命令链路跑通。第三阶段第 3 周按数据类型逐一声读t_string.c、t_hash.c、t_zset.c的关键命令结合 dict、quicklist、跳表实现。目标是对每种类型的底层编码和接口复杂度有数。第四阶段第 4 周读db.c、expire.c、evict.c搞清楚 key 空间管理、缓存淘汰、过期删除如何协同。第五阶段最后读rdb.c、aof.c、replication.c的主路径函数理解数据安全如何保证。这个顺序最反直觉的点在于不要把rdb.c、aof.c放最前面读。这两个文件代码量大、分支多一上来就啃容易打击信心而且很多东西依赖前置概念的铺垫。7. 常见问题与实战避坑技巧7.1 读源码时最常见的三个误解误解一Redis 是单线程所以源码里没有并发问题。这个理解不全面。主线程确实单线程处理命令但 Redis 6.0 之后网络 I/O 可以多线程处理。另外还有后台线程做异步删除。真正读代码的时候bio.c、networking.c 里是会看到锁和条件变量的。误解二所有命令都直接走统一入口。大多数命令走 processCommand但有些命令比如 SUBSCRIBE、MONITOR、DEBUG 走的路径完全不同。它们不是普通命令更像控制面操作直接操作客户端状态。误解三数据结构文件之间互相独立。实际上 sds、dict、ziplist 之间有大量耦合。quicklist 内部包含 ziplistlistpack 又在替代 ziplist 的部分场景。分类只是便于理解真实的代码是互相渗透的。7.2 实际操作中值得留意的细节优先看头文件而不是源文件。Redis 的注释质量主要在头文件里函数用途写的很清楚。比如 sds.h 开头关于 sds 内存布局的注释比任何博客都清楚。留意函数命名。Redis 的函数命名非常统一xxxCommand是命令处理函数lookupXxx是查找函数addReplyXxx是响应函数。看懂了命名代码猜一半。用grep定位函数定义别一个个文件翻。比如你想知道setKey在哪直接grep -r int setKey src/一下就有了。源码版本要选对。Redis 7.x 的代码和 5.x 相比很多模块改了名或者新增了文件比如 listpack 替代 ziplist 的过程不同版本差异很大。建议就固定一个 LTS 版本精读别看着 7.0 的教程去翻 6.2 的代码。7.3 一条亲测有效的精读技巧给代码画契约图精读到具体文件时不要一行行看实现先理清楚这个模块对外提供什么能力、内部依赖什么能力。我把这个叫做契约图。以 aof.c 为例对外它提供feedAppendOnlyFile主线程在写命令后调用。后台的 AOF 重写进程逻辑。启动时的 AOF 加载逻辑。对内它依赖从 bio.c 获取后台线程支持。从 rio.cRDB/AOF 通用的 I/O 封装获取读写能力。调用 server.h 里的各种全局状态。每一个文件都画这样一张图之后遇到任何问题你都能迅速判断这个问题属于模块的对外契约还是内部实现。排查线上问题的时候这个判断非常省时间。8. 分类表之后的下一步怎么用这份分类做更深入的事情源码分类不是终点它是很多实践的起点。我做完这份分类之后至少这几个方向一下变得好做了源码调试用 gdb 给 processCommand 打断点凡是执行 Redis 命令都会停在这。配合分类表能很快定位是哪条命令的哪个环节出了问题。二次开发想给 Redis 加一个自定义命令只需要三步——在命令表里注册新命令名在合适的 t_*.c 文件里写命令函数在 server.h 里加需要的结构体字段。分类表告诉我该改哪些文件。性能分析一条命令慢是慢在协议解析、数据结构操作还是网络发送分类表把每一段对应的代码都标出来了直接按段 profile。面试准备被问到Redis 为什么快我能从事件循环、数据结构、对象编码、持久化策略四个层次分别展开每个层次都对应一个源码文件块回答的深度和结构感都不一样。9. 最后分享一个小经验源码分类这事听起来像个体力活其实比想象中更考验抓主线的能力。每个人分类方式可以不一样但有一条核心原则不变你要能够拿着这张分类表解释任何一条 Redis 命令的全生命周期。如果分类表做不到这一点那分类就是死的只是重新抄了一遍文件名而已。我自己在做分类的时候最花时间的不是分文件而是验证每个文件在主链路上的位置。这一步做完之后后面读代码的效率提升是几何级别的。如果你也拿到了 Redis 源码但一直没读进去试试我这个方法——先花一个下午把分类表建起来再花三天精读第一阶段的文件你会有完全不同的体验。
← 返回资讯列表 预约报考咨询 →
NEXT STEP

看完公告,下一步怎么走?

把报考交给靠谱的人:材料预审、批次抢报、考前辅导、复审提醒,全程有人跟。

进入报考专题