RDB 解析器
这一章单独看 snapshot 解析器。它既服务本地恢复,也服务主从复制初始化。
文件边界
src/rdb.rs
模块职责
src/rdb.rs 的职责很单一:把一个 RDB 字节流解析成内存写入。
它会在两个地方被复用:
- master 启动时从本地 DB 文件恢复
- follower 复制握手后消费 master 发送过来的 snapshot
这个复用很重要,因为仓库里只有一条 RDB 解码路径,没有“文件恢复版”和“网络复制版”两套实现。
入口函数
当前有两个公开入口:
parse_rdb_file(...)parse_rdb(...)
parse_rdb_file(...) 只是面向文件的包装器。
它做的事只有:
- 把
tokio::fs::File包成BufReader - 转调
parse_rdb(...)
真正的解析逻辑都在 parse_rdb(...)。因为它接收的是 AsyncRead + Unpin,所以既能吃文件,也能吃网络流。
高层解析顺序
parse_rdb(...) 一开始就锁住 server.storage,然后按这个顺序走:
parse_magic(...)parse_version(...)- 进入 opcode dispatch loop
- 遇到
EOF结束
当前支持的 opcode 有:
META(0xFA)DB_SELECT(0xFE)TABLE_SIZE_INFO(0xFB)EOF(0xFF)
遇到其他值就会报错。
头部校验
parse_magic(...) 会精确读取 5 个字节,并要求它等于 REDIS。
parse_version(...) 再读取 4 个字节版本号并原样返回。
这里做的是文件结构级别的校验,但后面没有按不同版本分支处理。
metadata 处理
循环读到 META 时,会连续调用两次 parse_aux(...),然后直接丢掉结果。
也就是说,这个解析器“结构上知道 metadata 的存在”,但“语义上不使用它”。
这很符合这个仓库的风格:
- 尽量保持对真实格式的结构对齐
- 但不为当前运行时用不到的状态额外建模
DB 选择和 table size
DB_SELECT 会被解析,但结果被忽略,因为这个服务基本只当成一个逻辑 DB 来用。
TABLE_SIZE_INFO 则不只是装饰字段,它会驱动后续两段 entry 解析:
- 读取
size_no_expire - 读取
size_expire - 解析对应数量的非过期 entry
- 解析对应数量的过期 entry
也就是说,当前实现默认 snapshot 会按预期的分组顺序组织。
entry 解析函数
每条记录会走两个不同 helper:
parse_no_expire_entry(...)parse_expire_entry(...)
parse_no_expire_entry(...) 会要求下一个字节必须是类型 0,然后通过 parse_aux(...) 读取 key 和 value。
这说明当前解析器只支持字符串 value。
parse_expire_entry(...) 则会先读过期前缀,再回到 parse_no_expire_entry(...) 解析正文。
当前支持两种过期编码:
0xFC-> 8 字节 little-endian 毫秒时间戳0xFD-> 4 字节 little-endian 秒级时间戳
秒级时间戳会立刻转成毫秒。
长度解析
parse_len(...) 是底层长度辅助函数。
它返回:
- 解码后的长度
StringEncoding
当前支持的编码类型有:
RawI8I16I32LZF
parse_string(...) 除了 LZF 之外都能解码;遇到 LZF 会显式报错。
一个需要明确指出的实现细节:长度前缀支持仍然不完整
当前实现显然是在尝试映射 Redis RDB 的长度前缀规则,但分支还没有完全对齐。
例如,本来用于处理 14-bit length 的分支,当前匹配的是 0x04,不是 0x40。
所以这个模块已经把“格式长什么样”表达出来了,但并没有完整覆盖所有前缀。
文档里应该把它当作当前实现限制,而不是写得像已经完整支持。
数据如何流入 storage
这个解析器不会先构建一个中间 snapshot 对象图,而是边读边写。
普通 entry:
1
2
parse_no_expire_entry
-> storage.set(key, value)
过期 entry:
1
2
parse_expire_entry
-> storage.setx(key, value, expire_timestamp)
当前恢复路径里和过期时间有关的注意点
这里必须按源码真实行为来写。
parse_expire_entry(...) 解析出来的是“绝对过期时间戳”。
但 storage.setx(...) 期待的是“相对 TTL 毫秒数”,并且会再加一遍 now_in_millis()。
因此当前实现下,RDB 恢复出来的带过期 key 并不会精确保留原始绝对截止时间,而是会被再次当成相对 TTL 处理。
follower 初始化时的复用
在复制握手阶段,FollowerReplicationClient::recv_rdb_file(...) 最终会调用 rdb::parse_rdb(&mut reader, server)。
也就是说,follower 应用 master snapshot 时,用的还是同一套解析器、同一套写入逻辑,也包括上面提到的过期时间行为。
EOF 和 CRC
读到 EOF 后,解析器会继续读取一个尾部 u64 CRC 字段,但不会做校验。
所以字节流对齐是对的,只是 checksum 还没有真正验证。
当前实现限制
- 只支持字符串 value
- metadata 会解析但会被忽略
- DB index 会解析但会被忽略
LZF编码不支持- checksum 不校验
- 长度前缀支持仍然不完整
- 带过期时间的 snapshot entry 复用的是相对 TTL 写入接口
即使有限制,src/rdb.rs 仍然是仓库里很值得细读的一块,因为它把“本地恢复”和“复制初始化”串进了同一条解码路径里。