← 返回文章归档

MiniRedis 学习记录:从编译运行到一条请求的旅程

沿着自己的 C++20 键值服务,跑通编译、测试和客户端,再分清协议、存储、连接与持久化各自负责什么。

先让程序回答一个 PING

学数据库时,我想有一个足够小、能沿着源码看完请求过程的项目。MiniRedis 就是这样的练习:用 C++20 实现字符串键值服务,从终端发出命令,观察数据怎样经过网络进入内存,以及程序重启后还能留下什么。

项目名里有 Redis,但当前使用自己定义的行文本协议。它还不能直接连接 redis-cli,也没有完整的 Redis 数据结构。先把这个边界说清,阅读代码时才不会把没做的能力算进去。

在 Windows 的 MinGW 环境中,可以依次执行:

cmake -S . -B build -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Debug
cmake --build build --parallel 2
ctest --test-dir build --output-on-failure
.\build\mini_redis.exe 6379

-S 指向源码,-B 指定独立构建目录,生成器需要与本机工具链一致。--parallel 2 控制编译任务数量,与服务器的客户端并发数没有关系。测试失败时先看 CTest 输出,再考虑启动服务,能少走不少弯路。

另开终端连接客户端:

python tools/client.py --command "PING"
python tools/client.py

交互客户端里的 :quit 是客户端命令;发给服务器的 QUIT 则属于应用协议。这两个名字相近,但作用位置不同。

用一小段操作建立预期

SET count 41       -> OK
INCR count         -> 42
MGET count missing -> ["42",null]
EXPIRE count 60    -> 1
PERSIST count     -> 1
TTL count         -> -1

这里最值得观察的不是命令数量,而是状态变化:INCR 把读、检查和加一合成一条操作;PERSIST 去掉过期时间;永久键的 TTL 返回值与不存在的键不同。再试一次非法整数递增,应该收到错误,并且旧值保持不变。

协议按换行分帧,键和值不能包含空白,也没有引号转义。拿它保存带空格的句子会遇到语法边界,这正好提醒我:数据模型和传输格式都需要明确约定。

顺着四层代码读下去

从 src/main.cpp 进入后,我会沿着下面的顺序看:

  1. src/server/server.cpp 管理连接、接收缓冲和未发完的响应。
  2. src/protocol/command.cpp 识别命令、检查参数并调用对应操作。
  3. src/storage/kv_store.cpp 维护键、值、截止时间和存储预算。
  4. src/storage/database.cpp 在启用日志时组织准备、记录和提交。

这四层各有自己的失败。例如换行还没收到属于网络分帧;参数缺了一半属于协议;整数溢出属于业务校验;日志无法同步属于持久化。把它们混成一个“执行失败”,后面就很难解释失败后状态是否改变。

数据库对象的生命周期也比一个连接长。客户端断开再连,纯内存数据仍然在;结束服务器进程再启动,未开启持久化的数据才会丢失。

重启实验比背术语更直观

启用追加日志时,先创建目录,再启动:

New-Item -ItemType Directory -Force data
.\build\mini_redis.exe 6379 --aof data/store.aof

写一个永久键和一个短 TTL 键,停掉服务,等到短键应当过期以后,再用同一个日志文件启动。永久键应恢复,短键不能因为重放获得一段全新的有效期。

这里的 AOF 是项目自己的 MR1/MR2 格式,不兼容 Redis AOF。恢复实验能检查进程退出、历史重放和过期时间,却不能替代真实断电与存储设备故障测试。

我在面试研习社里练习“两分钟介绍键值服务”时,会围绕这一段可操作的演示来讲:一个请求经过什么,错误保留什么,重启恢复什么。比连续报出“网络、缓存、持久化”几个词更容易接受追问。

接下来可以分别看连接与背压,以及写入原子性与 AOF。

✳

感谢阅读,下一篇再见。

回到文章开头 ↑