先让程序回答一个 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 进入后,我会沿着下面的顺序看:
src/server/server.cpp管理连接、接收缓冲和未发完的响应。src/protocol/command.cpp识别命令、检查参数并调用对应操作。src/storage/kv_store.cpp维护键、值、截止时间和存储预算。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。