☰
Redis源码分类指南:按分层与职责拆解核心文件,高效阅读不再迷路
2026/10/12 13:15:10 网站建设 项目流程

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.c(Linux 平台)。ae.c 是跨平台事件循环的抽象层,对外提供了统一的接口;ae_epoll.c 是底层对 epoll 的具体封装。除这两个之外,还有:

  • anet.c:TCP 连接的封装,非阻塞建连、读写等待都在这。
  • 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 和 member
zset 跳表存储t_zset.c / skiplist.c跳表实现有序集合的核心
set 整数存储intset.c全整数集合的有序紧凑数组

这一层的核心数据结构有四个必须啃透:sds、dict、quicklist、跳表。它们各自的使用场景、时间复杂度、退化条件,面试里高频出现,也直接影响你对业务选型的判断。

3.4 命令解析与命令实现层

命令实现层是业务理解最直接的地方。比如你执行set foo bar,到底调到哪个函数、里面做了什么,全在这一层。但实际上命令实现并不是一个庞大的文件,而是一个分散式的文件集。

Redis 的源码里,命令处理函数分布在几个文件:

  • t_string.c:string 类型所有命令(set、get、incr、decr、append 等)。
  • t_list.c:list 类型所有命令(lpush、rpush、lpop、rpop、llen 等)。
  • t_hash.c:hash 类型所有命令(hset、hget、hdel、hlen 等)。
  • t_set.c:set 类型所有命令(sadd、srem、sismember、sinter 等)。
  • t_zset.c:zset 类型所有命令(zadd、zrange、zrank、zscore 等)。
  • db.c:键空间操作,key 的过期、淘汰、查找都在这。

t_ 前缀其实是 "type" 的缩写,代表某种数据类型。看到这些文件,直接就能归类到数据类型命令实现。

另外有两个比较特殊的文件:

  • commands.c / commands.h:命令表定义,把命令名字符串映射到处理函数指针。
  • script_lua.c / script.c:Redis 脚本执行(Lua)的核心。

3.5 持久化、复制与高可用

这一块是 Redis 从"缓存玩具"变成"生产级存储"的关键。文件数量和复杂度都偏高。

  • rdb.c:RDB 快照持久化,全量内存数据序列化到磁盘。
  • aof.c:AOF 日志持久化,每一条写命令记录成日志。
  • replication.c:主从复制协议,全量同步和增量同步都在这里。
  • sentinel.c:哨兵模式,监控主节点、自动切换。
  • cluster.c:集群分片、槽位管理、节点间通信。
  • failover.c / gossip 相关:故障感知和状态传播。

这个模块可以先不精读,但要知道每个文件解决什么问题。等你把主线读通之后再回过头来啃持久化和复制,会觉得简单很多,因为它们的核心还是在操作底层数据结构,只是多了网络交互和状态机。

3.6 内存管理、回收与策略算法

Redis 是内存数据库,所以内存管理策略是它的命门。

  • evict.c:内存淘汰策略的落地实现。LRU、LFU 的近似采样逻辑都在这。
  • expire.c:key 过期机制的实现,惰性删除和定期删除,以及淘汰触发时机。
  • lazyfree.c:异步删除实现,把释放内存的大操作放到后台线程。
  • object.c:Redis 对象的创建、编码转换、引用计数管理。对象共享也在其中。
  • malloc_usable_size 相关的内存统计逻辑,散落在 zmalloc.c。

zmalloc.c 值得单列出来讲一下:它是对系统内存分配函数的一层封装,除了统计内存占用,还做了内存对齐和异常处理。所有 Redis 对象分配内存都过它,所以看内存增长、内存碎片化,离不开这个文件。

3.7 扩展模块与辅助工具

这部分不属于主链路,但在特定场景下很重要。

  • modules.c:Redis 模块系统,允许用 C 写插件扩展命令。
  • multi.c:事务命令 MULTI/EXEC/WATCH 的实现。
  • pubsub.c:发布订阅机制。
  • blocking.c:阻塞命令的实现。比如 BLPOP 怎么让客户端挂起等待。
  • notify.c:键空间通知,把内部操作转发给订阅者。
  • sort.c:SORT 命令的实现。
  • 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为例:

  1. 客户端连接在 networking.c 中被接受,注册可读事件。
  2. 数据到达,事件循环 ae.c 检测到可读事件,调用 readQueryFromClient。
  3. 数据写入输入缓冲区,进入 processInputBuffer,再进 processCommand。
  4. processCommand 在 server.c 中实现,里面做命令查找。
  5. 命令表在 commands.c 中定义,找到 getCommand。
  6. getCommand 在 t_string.c 中实现,内部调用 lookupKeyRead。
  7. 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 多几个环节:

  1. t_string.c 的 setCommand 检查参数。
  2. 对 key 和 value 做编码转换,决定最终用 sds 还是 int 编码。
  3. 写入 db->dict,用 setKey。
  4. signalModifiedKey 通知键空间事件。
  5. 触发 dirty 计数增加,为后续持久化做标记。
  6. 如果 AOF 开启,把命令写入 AOF 缓冲。
  7. 如果复制开启,把命令广播给从节点。

这个链路在 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 内部包含 ziplist,listpack 又在替代 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.c(RDB/AOF 通用的 I/O 封装)获取读写能力。
  • 调用 server.h 里的各种全局状态。

每一个文件都画这样一张图,之后遇到任何问题,你都能迅速判断:这个问题属于模块的对外契约,还是内部实现。排查线上问题的时候,这个判断非常省时间。

8. 分类表之后的下一步:怎么用这份分类做更深入的事情

源码分类不是终点,它是很多实践的起点。我做完这份分类之后,至少这几个方向一下变得好做了:

  • 源码调试:用 gdb 给 processCommand 打断点,凡是执行 Redis 命令都会停在这。配合分类表能很快定位是哪条命令的哪个环节出了问题。
  • 二次开发:想给 Redis 加一个自定义命令,只需要三步——在命令表里注册新命令名,在合适的 t_*.c 文件里写命令函数,在 server.h 里加需要的结构体字段。分类表告诉我该改哪些文件。
  • 性能分析:一条命令慢,是慢在协议解析、数据结构操作还是网络发送?分类表把每一段对应的代码都标出来了,直接按段 profile。
  • 面试准备:被问到"Redis 为什么快",我能从事件循环、数据结构、对象编码、持久化策略四个层次分别展开,每个层次都对应一个源码文件块,回答的深度和结构感都不一样。

9. 最后分享一个小经验

源码分类这事,听起来像个体力活,其实比想象中更考验"抓主线"的能力。每个人分类方式可以不一样,但有一条核心原则不变:你要能够拿着这张分类表,解释任何一条 Redis 命令的全生命周期。如果分类表做不到这一点,那分类就是死的,只是重新抄了一遍文件名而已。

我自己在做分类的时候,最花时间的不是分文件,而是验证每个文件在主链路上的位置。这一步做完之后,后面读代码的效率提升是几何级别的。如果你也拿到了 Redis 源码但一直没读进去,试试我这个方法——先花一个下午把分类表建起来,再花三天精读第一阶段的文件,你会有完全不同的体验。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询