存储模型
这一章看字符串命令和 stream 命令背后的内存状态容器。
文件边界
src/storage.rssrc/server.rssrc/cmd.rs
两类存储容器
这个仓库没有把所有 Redis 类型都放进一个统一的 value enum。
而是拆成了两个独立容器:
- 普通字符串键值存在
Storage - stream 存在
Server::streams
这就是仓库一个很核心的结构取舍:不做通用对象系统,而是按命令族拆开,让代码更容易读。
字符串键值的内部结构
src/storage.rs 定义的核心结构是:
1
HashMap<String, (String, Option<u128>)>
这个 tuple 表示:
- 存储的字符串值
- 可选的过期时间戳
代码里用 ValueType 别名把这个语义固定了下来。
Storage 的 API 面
它公开的方法很少:
new()get(...)set(...)setx(...)del(...)keys(...)
这比真实 Redis 窄很多。复杂行为仍然留在 cmd.rs,Storage 基本只负责点查和写入。
时间源
now_in_millis() 会把 SystemTime 转成 Unix 毫秒时间戳。
这个 helper 在多个子系统里复用:
Storage里的过期判断cmd.rs里 stream 自动生成 ID
所以“毫秒时间”已经是这个仓库多个模块共享的基本原语了。
过期时间模型
Storage 为每个 key 维护一个 Option<u128> 过期字段。
重点不是“存了过期时间”,而是 setx(...) 期待的输入语义:
1
相对 TTL,单位是毫秒
setx(...) 总是按下面的规则写入:
1
now_in_millis() + expire_ms
这个语义和下面几类写入是匹配的:
SET PX <millis>- 命令层先把秒转成毫秒后的
SET EX <seconds>
get(...) 与惰性过期
Storage::get(...) 不只是简单的 map lookup。
它的控制流是:
- 在
HashMap里查 key - 看是否存在过期时间
- 用
now_in_millis()比较当前时间和过期时间 - 如果已过期,删除 key 并返回
None - 否则 clone 出值
这里没有后台清理线程。过期 key 是在被访问时才被清掉的。
写路径
set(...) 存无过期的字符串。
setx(...) 接收相对 TTL,写成绝对截止时间。
del(...) 直接删 key。
keys(...) 只是返回当前 map 里的 key,不会先做一轮过期清扫。
这也意味着:如果某个 key 已经过期,但一直没人 GET 它,它仍然可能出现在 KEYS * 的结果里。
stream 容器结构
stream 不在 Storage 里。
src/server.rs 里的展开类型实际上是:
1
HashMap<String, BTreeMap<String, Vec<(String, String)>>>
可以读成:
- stream 名 -> stream 本体
- entry ID -> 有序记录
- 记录值 -> 一组 field/value 对
这里最关键的设计选择是 BTreeMap。这样 XRANGE 和 XREAD 都能直接做有序范围查询,不需要额外排序索引。
命令处理器如何访问存储
src/cmd.rs 里的大部分 helper 都遵循同一模式:
1
2
3
lock 共享容器
-> 调一个很小的存储操作
-> 组装 Protocol 响应
比如:
GET->storage.get(...)SET->storage.set(...)SET PX/EX->storage.setx(...)DEL->storage.del(...)TYPE-> 先查storage,再查streams
所以存储层本身才会这么简单:参数解释、角色判断和返回格式都在上层。
需要按源码真实行为写出来的地方
这部分文档不能只写“理想语义”,要写当前实现。
两个点尤其重要:
Storage只存字符串,INCR这种命令依然是先把字符串 parse 成数字,再算完再存回字符串。setx(...)永远把第三个参数当成“相对 TTL”。
第二点会直接影响 RDB 恢复行为:
parse_expire_entry(...)从 snapshot 里解析出来的是绝对过期时间parse_rdb(...)当前会把这个值直接传给storage.setx(...)
因此,RDB 恢复出来的过期时间并不会严格保留原始绝对截止时间,而是会被再次当成相对 TTL,加上一遍当前时间。
这不是文档吹毛求疵,而是当前源码的真实表现。
字符串命令的数据流
普通字符串命令的路径是:
1
2
3
4
5
Cmd::run
-> command helper in cmd.rs
-> lock server.storage
-> Storage method
-> Protocol response
stream 命令走的也是类似模式,只是锁的是 server.streams。
扩展边界
如果后面要支持更多 Redis 类型,当前结构上的阻力点很明确:
Storage只知道字符串- stream 被单独建模在
Storage之外 TYPE只会探测这两个容器- 命令 helper 直接知道要锁哪个容器
所以如果要加 list、set、hash,大概率不是改一个局部 helper 就够,而是要重新整理顶层状态结构。
当前实现限制
- 普通 value 全都是字符串
- 过期清理是惰性的
keys()不会顺手清理过期数据- stream 和字符串存储是完全分开的
setx(...)只天然适配相对 TTL 语义- 这一层没有内存统计、淘汰策略或持久化 hook
存储模型本身很克制,但正因为它足够明确,命令层和 RDB 层的行为才会比较容易顺着看下去。