某同学找到我,问能不能帮他看看简历上那个"基于 Redis 的缓存系统"项目。
我问:Redis 的跳表怎么实现的?
他答:用的 sorted set,底层是跳表。
我问:那跳表的zslInsert是怎么维护 span 的?
沉默。
然后他说:我其实就是在项目里调了几个 Redis 命令,底层是 Redis 帮我做的。
这个场景,我相信很多人都经历过,或者会经历。
会用 Redis,和能讲清楚 Redis,是两个完全不同的段位。
今天这篇文章,就是写给想进第二个段位的人的。
为什么不能直接去啃 Redis 源码?
每次有人问我这个问题,我都会说同一句话:你先去数数 Redis 7.x 有多少行代码。
答案是——约 10 万行。
分布在数百个文件里,充斥着 SDS 的内存布局技巧、dict 的渐进式 rehash 时机、ae 事件循环与 serverCron 之间的协调逻辑,还有主从复制握手时的各种状态机……每一个细节,单独拎出来都能写一篇长文。
更致命的是:你看的是一个"已经完成"的工程。
你看不到它是怎么从第一行代码开始生长出来的。你不知道数据结构层做完之后,为什么下一步是事件循环,而不是先写命令分发。你不知道 RESP 协议解析是在哪个阶段接进去的,为什么要先有 networking 层才能引入命令表。
那个"从0到1"的过程,才是真正值钱的东西。
GitHub 上也有一些"简化版 Redis"——但它们大多参考 Redis 4.x 或 6.x 以前的版本,跳表没有 span,listpack 没有,quicklist 的双编码更别提了。学完之后,你对 Redis 7.x 的理解依然是断层的。
于是我做了这件事
从今年初开始,我花了将近三个月,写了一个叫Mini-Redis的项目。
它基于Redis 7.2.x源码设计,完全由我本人从空文件夹开始,一行一行写出来,核心代码约2 万行,配套测试代码约1.3 万行(用于保证每个模块的正确性)。
这不是对 Redis 的删减版,也不是某个旧版 clone 的翻新——而是我参透 7.x 的设计逻辑之后,重新推演、重新实现的一遍。
先看压测,用数据说话
同等条件,50 并发,10 万请求,Mini-Redis 与 Redis 7.2.11 正面对比:
========== Mini-Redis (port 6379) ========== SET: 164,203 ops/s GET: 159,489 ops/s INCR: 161,290 ops/s LPUSH: 160,771 ops/s HSET: 165,562 ops/s ZADD: 159,235 ops/s MSET(10): 158,730 ops/s ========== Redis 7.2.11 (port 6380) ========== SET: 164,744 ops/s GET: 162,866 ops/s INCR: 163,934 ops/s LPUSH: 159,744 ops/s HSET: 158,478 ops/s ZADD: 162,601 ops/s MSET(10): 158,478 ops/s跨所有命令,Mini-Redis 的吞吐量与官方 Redis 7.2.11 基本持平,p50 延迟相当。
这不是教学玩具,这是一个真实可用的高性能存储引擎。
来看张图:
上图是 Mini-Redis 的完整架构,从客户端接入到持久化与主从复制,每一层都由我从0实现。下面来说说这36天具体走了哪些路。
36 天,这个项目是怎么长出来的
整个教学分为 22 个模块,36 天完成,每天的代码都是对前一天的增量扩展,每天结束都能独立编译运行。你跟着走完一遍,相当于亲历了一个生产级项目的完整生长过程。
上面是架构全貌,下面说说每个阶段在做什么。
第一阶段:地基——数据结构层(Day 1–11)
这一阶段的核心问题是:Redis 为什么不用标准库?
你会手写 zmalloc 内存追踪分配器,然后是 SDS 动态字符串(五种 header 结构、O(1) 长度查询、贪婪扩容策略),adlist 通用链表,然后是重头戏——dict 渐进式哈希表(双表结构、SipHash-2-4、安全与非安全迭代器的差异、dictScan 的倒序位游标算法)。
listpack、quicklist 的双编码逻辑是很多人看 Redis 7.x 时卡住的地方。这两个模块我花了两天,把每一种编码的触发条件、边界处理、节点合并分裂的七条分支决策树,全部拆清楚写出来。
还有跳表(zskiplist),32 层前向指针、span 跨度、按分值范围和字典序范围的区间操作——这是 ZSet 命令能在 O(log N) 内完成排名查询的基础。
最后是整数集合 intset,三种编码的自动升级逻辑。
到 Day 11,你拥有一套完整的、经过测试的底层数据结构库。
第二阶段:心跳——事件驱动层(Day 12–14)
ae 事件循环是 Redis 单线程高性能的秘密所在。
你会实现 epoll 封装与 Channel 抽象,然后是时间事件链表(懒删除 + refcount 保护 + maxId 防递归触发),最后是完整的 aeMain 驱动循环——aeProcessEvents如何把 I/O 超时时间设为最近定时器的倒计时、为什么先处理读事件再处理写事件。
anet 模块封装 TCP 工具函数,非阻塞连接、SO_REUSEADDR、TCP_NODELAY,每一个选项背后都有理由。
第三阶段:骨架——服务器层(Day 15–17)
Day 15 是整个课程的第一个里程碑:服务器第一次能启动了。
redisObject、redisDb、redisServer的核心字段确定,main 函数把 initServer 和 aeMain 串起来,acceptTcpHandler 开始接收连接,serverCron 时间事件跑起来。
Day 16 实现 RESP 协议解析层——processInputBuffer、inline 模式、multibulk 模式、双缓冲回复机制。
Day 17 接入命令表和命令分发,PING/ECHO/SELECT/DBSIZE/FLUSHDB 全部可用,redis-cli 打上去能正常交互了。
第四阶段:血肉——五大数据类型命令(Day 18–26)
String(SET 的 NX/XX/EX/GET 全选项、INCR 的 int encoding 原地修改优化)、List(listpack 与 quicklist 双编码的统一 API 层、LPOS 的 RANK/COUNT/MAXLEN 参数)、Hash、Set(intset → listpack → HT 三级编码升级)、ZSet(跳表 + 新版 ZRANGE 统一接口)。
每种类型的编码升级逻辑,每个命令的边界 case,测试代码全部覆盖。以 ZSet 为例,Day 24–26 三天,86 个测试全部通过。
第五阶段:可靠性——过期、持久化、事务、发布订阅(Day 27–32)
expire 模块:惰性删除 + activeExpireCycle 慢速/快速双模式,EXPIRE 命令的 NX/XX/GT/LT 选项,TTL/PTTL/EXPIRETIME 全部实现。
RDB:rio I/O 抽象、五种数据类型的序列化/反序列化、BGSAVE fork 子进程 + COW 机制、启动时自动加载。
AOF:三种 fsync 策略、fake client 回放、后台 bgrewrite 双缓冲增量同步、父子进程间的原子文件切换。
Multi/EXEC 事务:命令队列、WATCH 的 CAS 语义、脏标记处理。
Pub/Sub:精确匹配 + glob 模式两路投递、客户端状态机限制、keyspace 通知。