Redis服务器源码解析:手写极简版搞定版本升级痛点
上周刚把生产环境的 Redis 从 4.0 升到 7.0,结果一堆老代码直接报错。MULTI 命令的行为变了,过期键的处理逻辑也不对劲,改了一下午才搞定。这种“版本升级后 API 全变了”的坑,其实只要懂底层原理,根本不用慌。
很多人只知道 Redis 快,但不知道它为什么快。今天我们就动手写一个极简的 Redis 服务器。不追求功能全,只为了搞懂内存结构、事件循环和协议解析。通过这份源码解析,你会发现那些莫名其妙的 API 行为,其实都是底层逻辑决定的。
项目目标与核心思路
我们的目标不是造轮子去替代 Redis,而是为了“看懂”。
真正的 Redis 有几百万行 C 代码,包含持久化、集群、Lua 脚本等复杂功能。对于理解核心机制来说,这些都是噪音。我们要剥离掉这些,只保留最核心的三块:网络层:如何接收客户端连接?
协议层:如何解析 RESP 协议?
数据层:内存中数据怎么存?怎么查?选 Python 来实现,不是为了性能,而是为了代码可读性。Python 的字典、列表、线程模型,足够模拟 Redis 的核心概念。如果你能用 Python 写出一个能跑通 GET、SET、DEL 的服务,再去读 C 源码时,就不会觉得是天书了。
关键原则:单线程模型:Redis 核心操作是单线程的,我们也模拟这一点,避免并发复杂度干扰思路。
RESP 协议:必须严格遵守 Redis 扩展协议,这样我们可以直接用 redis-cli 测试。
内存哈希表:用 Python 的 dict 模拟 Redis 的 dict.c 中的哈希表。目录结构规划
项目结构保持扁平化,方便阅读。
mini-redis/
├── main.py # 入口文件,启动服务器
├── protocol.py # RESP 协议解析与序列化
├── command.py # 命令处理逻辑
├── storage.py # 内存存储模拟
└── README.md # 说明文档protocol.py 负责和客户端打交道,把字节流变成 Python 对象。
storage.py 负责数据持久化在内存中,模拟 Redis 的内存空间。
command.py 是业务逻辑层,处理 SET、GET 等具体指令。
main.py 是主循环,把三者串联起来。核心代码实现
1. 存储层:模拟 Redis 内存
Redis 的数据结构非常经典,String、List、Hash、Set、ZSet。我们先只实现 String,因为它是基础。
# storage.pyclass RedisStorage:def __init__(self):# 模拟 Redis 的 dict,key 是 bytes,value 是 bytes# 为什么用 bytes?因为网络传输和 Redis 内部都是二进制self.data = {}# 模拟 TTL,key 是 key,value 是过期时间戳self.ttl = {}def set(self, key: bytes, value: bytes, ex: int = None):self.data[key] = valueif ex:import timeself.ttl[key] = time.time() + exelse:self.ttl.pop(key, None)def get(self, key: bytes):if key not in self.data:return None# 检查过期if key in self.ttl:import timeif time.time() self.ttl[key]:self.delete(key)return Nonereturn self.data[key]def delete(self, key: bytes):self.data.pop(key, None)self.ttl.pop(key, None)return 1 if key in self.data else 0解析重点:Key/Value 都是 bytes:这是很多初学者忽略的细节。Redis 内部存储全是二进制,不区分字符串类型。我们在 Python 中也要保持这个习惯,否则序列化时会出 bug。
懒删除策略:注意 get 方法里,我们是在读取时检查过期,而不是后台线程定期删除。这就是 Redis 的“惰性过期”策略。官方文档里提到,这种策略能最大化 CPU 效率,因为只有在访问时才判断,如果键一直没人访问,就不浪费资源去删除它。2. 协议层:解析 RESP
Redis 使用 RESP (REdis Serialization Protocol)。最简单的形式是 *2\r\n$3\r\nSET\r\n$5\r\nhello\r\n$5\r\nworld\r\n。
我们需要把这种二进制流解析成 Python 列表 [[b'SET'], [b'hello'], [b'world']]。
# protocol.pyimport structclass RESPParser:def __init__(self):self.buffer = b''def feed(self, data: bytes):self.buffer += datadef parse(self):解析一条完整的 RESP 命令返回 None 表示数据不完整,需要等待更多数据if not self.buffer:return None# 尝试读取第一行,确定类型# 简单起见,我们假设客户端一次发送完整命令# 实际项目中需要处理半包问题,这里简化处理lines = self.buffer.split(b'\r\n')# 如果最后不是空字符串,说明数据没传完if lines[-1] != b'':return None# 去掉最后的空字符串lines = lines[:-1]if not lines:return None# 清除缓冲区已处理部分(简化版,实际需精确计算长度)self.buffer = b''first_line = lines[0]# 简单判断:如果是简单命令(如 PING),直接返回# 标准 RESP 命令以 * 开头if first_line.startswith(b'*'):try:count = int(first_line[1:])args = []for i in range(count):if lines[1 + i*2] != b'': # 长度行len_str = lines[1 + i*2]val = lines[2 + i*2]args.append(val)return argsexcept (ValueError, IndexError):return Noneelse:# 兼容非标准协议或简单文本return [first_line]避坑指南:半包问题:网络传输是不保证完整性的。上面的 parse 方法做了简化,实际开发中必须维护一个状态机,逐字节解析,直到读取完整长度。
二进制安全:千万不要用 str 处理 key/value,必须用 bytes。因为 Redis 的 key 可以包含 \x00 等控制字符,字符串处理会报错或截断。3. 命令层与主循环
现在把存储和协议连起来。
# command.pyfrom storage import RedisStoragedef execute_command(cmd: list, storage: RedisStorage):if not cmd:return b':-1\r\n' # Errorcommand = cmd[0].upper()args = cmd[1:]if command == b'SET':if len(args) 2:return b':-ERR wrong number of arguments for \'set\' command\r\n'key = args[0]value = args[1]ex = None# 简单解析 EX 参数for i in range(2, len(args), 2):if args[i] == b'EX' and i+1 len(args):ex = int(args[i+1])storage.set(key, value, ex)return b'+OK\r\n'elif command == b'GET':if len(args) != 1:return b':-ERR wrong number of arguments for \'get\' command\r\n'key = args[0]val = storage.get(key)if val is None:return b'$-1\r\n' # Nullelse:# 序列化返回return b'$' + str(len(val)).encode() + b'\r\n' + val + b'\r\n'elif command == b'DEL':if len(args) == 0:return b':-ERR wrong number of arguments for \'del\' command\r\n'count = 0for key in args:if storage.delete(key):count += 1return b':' + str(count).encode() + b'\r\n'elif command == b'PING':return b'+PONG\r\n'return b':-ERR unknown command \'' + command + b'\'\r\n'主入口:
# main.pyimport socket
import threading
from protocol import RESPParser
from command import execute_command
from storage import RedisStoragedef handle_client(conn, storage):parser = RESPParser()while True:try:data = conn.recv(1024)if not data:breakparser.feed(data)cmd = parser.parse()if cmd:response = execute_command(cmd, storage)conn.sendall(response)except Exception as e:print(fClient error: {e})breakconn.close()def start_server(host='127.0.0.1', port=6379):storage = RedisStorage()server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)server.bind((host, port))server.listen(5)print(fMini Redis running on {host}:{port})while True:conn, addr = server.accept()print(fNew connection: {addr})t = threading.Thread(target=handle_client, args=(conn, storage))t.daemon = Truet.start()if __name__ == '__main__':start_server()运行与测试
启动服务后,用 redis-cli 连接测试。
# 终端 1
python main.py# 终端 2
redis-cli -p 6379测试用例:基本读写:
127.0.0.1:6379 SET name zhangsan
OK
127.0.0.1:6379 GET name
zhangsan过期测试:
127.0.0.1:6379 SET temp hello EX 5
OK
127.0.0.1:6379 GET temp
hello
# 等待 6 秒
127.0.0.1:6379 GET temp
(nil)删除测试:
127.0.0.1:6379 DEL name
(integer) 1
127.0.0.1:6379 GET name
(nil)调试技巧:
如果 GET 返回乱码,检查 protocol.py 中的序列化部分。RESP 协议要求返回的 String 必须带长度头 $len\r\ndata\r\n。漏掉长度头,客户端会解析失败。
优化扩展与避坑
这个极简版离生产 Redis 还差得远,但有几个点值得深挖:多线程 vs 单线程:
上面的代码用了 threading,每个连接一个线程。真正的 Redis 核心命令是单线程的,网络 IO 也是多线程(6.0 引入)。为什么 Redis 核心单线程? 避免锁竞争,CPU 瓶颈通常在内存访问而非网络。
Python 的 GIL:Python 有全局解释器锁,多线程并不能真正并行执行 CPU 密集型任务。如果模拟高并发,建议用 asyncio 重写网络层,保持单线程处理命令,这样更接近 Redis 的真实模型。内存碎片:
Redis 在长时间运行后,内存占用会高于实际数据量。这是因为内存分配器(jemalloc)的碎片化。我们的 Python 版由 Python 解释器管理内存,碎片化问题由解释器内部处理,感知不到。但在 C 实现中,这是运维关注的重点。持久化:
当前版本重启数据丢失。进阶练习可以加入 RDB 快照:每隔 N 秒,将 storage.data 序列化保存到文件。
启动时,如果文件存在,加载到内存。
注意:RDB 是二进制格式,不能直接存 Python 的 dict,需要自定义序列化或转储为 JSON(测试用)。大 Key 问题:
如果 SET 一个 10MB 的 Value,上面的代码会阻塞主线程。真实 Redis 中,大 Key 操作会阻塞其他命令。优化方案是将大 Key 拆分,或者在后台线程异步处理(但要注意一致性)。小结
通过手写这个 Mini Redis,我们把黑盒变成了白盒。版本升级 API 变化的本质:往往是底层数据结构或内存管理机制的调整。比如 4.0 到 7.0,集群模式的变化、过期策略的优化,都直接影响命令行为。
源码阅读方法:不要从头读到尾。先找 server.c 的主循环,看事件如何触发;再看 dict.c,看哈希表如何冲突解决;最后看具体命令的实现。
官方文档的价值:Redis 官方文档的“Internals”章节,虽然简短,但指出的方向非常准确。结合代码看,事半功倍。你更常用哪种写法?是喜欢用 Python 这种脚本语言快速模拟逻辑,还是直接啃 C 源码?评论区交流一下,看看大家是怎么入门 Redis 底层的。
企业数字化 ERP 产品动态
相关推荐
学生信息管理系统简单版:一张架构图比代码更值钱 简介:这是一份面向计算机相关专业学生与Java初学者练手的学生信息管理系统简单版源码,配套架构图,适合作为课程设计、毕业设计或SSM入门项目的参考模板。系统围绕学生、班级、院系、课程、成绩等核心业务展开,覆盖信息录入、查询与… · 2026/9/23 19:54:07
合同管理系统方案避坑速查手册:性能优化实战 合同管理系统方案避坑速查手册:性能优化实战 刚写完几百行 CRUD 代码,看着界面能跑就以为大功告成?醒醒吧。很多开发者卡在“学会语法却不知怎么搭项目”这一步,尤其是面对合同这种涉及金额、状态流转、多方签署的复杂业务,系统一上线就卡顿,审批… · 2026/9/23 19:54:07
用C++和SDL2复刻金庸群侠传:2D游戏引擎实战指南 简介:一份基于SDL2的二维游戏引擎源码包,以复刻经典DOS游戏《金庸群侠传》为目标,既适合C学习者作为游戏开发实战范例,也为研究老游戏移植提供了完整参考。整个压缩包共一百八十六个文件,主体由六十九个头文件、五十六… · 2026/9/23 20:27:40
如何用手机号注册微信:新手避坑与底层逻辑解析 如何用手机号注册微信:新手避坑与底层逻辑解析 复制来的代码跑不通,报错信息满屏红字,你盯着屏幕发呆,不知道该从哪下手调?这是绝大多数 新手 在接触开发初期最真实的写照。特别是当你试图通过程序模拟或理解 如何用手机号注册微信… · 2026/9/23 20:27:34
造字工坊版本大改?3个方案完整示例教你快速上手 造字工坊版本大改?3个方案完整示例教你快速上手 版本升级后 API 全变了,是不是让你对着新文档抓耳挠腮?别慌,这种“推倒重来”的更新在造字工坊这类创意工具链中并不罕见,但混乱背后往往藏着更高效的工作流。我花了三天时间,把目前主流的三种实现… · 2026/9/23 20:27:34
如何改文件后缀速查手册:从内存到磁盘的底层逻辑 如何改文件后缀速查手册:从内存到磁盘的底层逻辑 刚学完 Python 语法,却卡在怎么把 .txt 变成 .json ?别急,这正是从“写代码”到“搭项目”的分水岭。很多人以为改后缀就是双击重命名,但在后端开发或数据处理场景中,这往往涉及文… · 2026/9/23 20:27:26
LLaVA 视觉语言助手实战指南:图像对话、VQA 与两阶段指令微调(AI-Research-SKILLs 多模态技能) AI 技能人工智能大模型深度学习 【免费下载链接】AI-Research-SKILLs Comprehensive open-source library of AI research and engineering skills for any AI model. Package the skills and your claude code/codex/gemini agent will be an AI research agent with full hor… · 2026/9/23 20:27:26
【C++入门】面向对象编程 - 07 虚函数调用怎样在运行时找到派生类实现 博主介绍:程序喵大人
35 - 资深C/C/Rust/Android/iOS客户端开发10年大厂工作经验嵌入式/人工智能/自动驾驶/音视频/游戏开发入门级选手《C20高级编程》《C23高级编程》等多本书籍著译者更多原创精品文章,首发gzh,见文末👇&#x… · 2026/9/23 20:27:12
3招搞定手机怎么下载微信面试难题实战项目解析 3招搞定手机怎么下载微信面试难题实战项目解析 面试被问“手机怎么下载微信”背后的原理,90%的人答不上来。别笑,这看似弱智的问题,实则是考察你对移动应用分发机制、安全校验及网络协议理解的试金石。我带过不少校招新人,他们背了八股文,却连一个A… · 2026/9/23 0:00:03
你有新短消息请注意查收:3个新手避坑指南搞定消息系统选型 你有新短消息请注意查收:3个新手避坑指南搞定消息系统选型 面试被问“高并发下如何保证消息不丢失”,你张口就是“用Redis”,结果面试官追问“如果Redis宕机了怎么办”,你瞬间卡壳。这种场景太常见了,很多新手在背八股文时,只记住了技术名词… · 2026/9/23 0:00:29