尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

【系列:MiniKV 原理剖析 · 第 3 篇】

【系列:MiniKV 原理剖析 · 第 3 篇】 导读上一篇我们拆解了 LRU 淘汰策略这次聚焦 Entry 里的expires_at字段——TTL 过期时间机制。从数据结构到六条命令的统一调用从惰性删除的取舍到与 LRU 的协作用源码实测带你吃透 MiniKV 的过期设计。从一道面试题说起「Redis 的过期键是怎么删除的」如果你背过八股文大概率会脱口而出惰性删除 定期删除。但当你真正打开一个 KV 存储的源码会发现「惰性删除」这四个字背后藏着不少值得推敲的细节。MiniKV 作为我们手写的轻量级 KV 存储它的 TTL 实现选择了最纯粹的惰性删除——不主动扫描操作到哪个键才检查哪个键。这个选择是偷懒还是智慧今天我们结合源码把 TTL 的来龙去脉一次讲透。一、Entry 里的 expires_at一个 optional 的妙用先看数据结构。在store.h中每个 Entry 长这样structEntry{std::string value;std::optionalClock::time_pointexpires_at;// ⭐ TTL 核心过期时间点std::liststd::string::iterator lru_position;};注意std::optional这个类型。它表达了一个关键语义一个键可以没有过期时间。有 TTL 的键expires_at存一个具体的时间点无 TTL 的键expires_at是std::nullopt也就是持久键这个设计比用一个「魔法值」表示「永不过期」要优雅得多——类型系统直接帮你区分了两种状态编译器就能拦截「把持久键当过期键处理」的 bug。另一个细节Clock::now()用的是steady_clock单调时钟。这意味着什么系统时间被人为调整不会影响 TTL 的判断。你手动把系统时间往前拨一小时已存键不会因此提前过期——这对分布式场景下的时间一致性至关重要。二、remove_if_expired_locked惰性删除的心脏核心逻辑在store.cpp:19的remove_if_expired_lockedboolStore::remove_if_expired_locked(conststd::stringkey){constautofoundentries_.find(key);if(foundentries_.end()||!found-second.expires_at.has_value()||Clock::now()*found-second.expires_at){returnfalse;// 不存在 / 无TTL / 未过期 → 不动}lru_.erase(found-second.lru_position);// 从LRU链表删除entries_.erase(found);// 从哈希表删除stats_.expired;returntrue;// 已过期并删除}这段代码只有 8 行但信息密度很高。三个提前返回的条件对应三种「不需要删除」的情况键不存在——没什么可删的无 TTL——持久键永远不过期未过期——时间还没到放行只有三个条件全部不满足才真正执行删除从 LRU 链表和哈希表同时移除并更新统计计数。注意lru_.erase和entries_.erase的顺序——先删 LRU 链表再删哈希表。这个顺序保证了两个结构的一致性如果先删哈希表一旦 LRU 删除失败比如迭代器失效就会出现「哈希表没了但 LRU 还挂着」的脏数据。三、六条命令统一调用惰性的体现remove_if_expired_locked被哪些命令调用答案是六条getseteraseexistsexpirettl每条命令在操作键之前都会先调用这个函数检查过期状态。这就是「惰性」的精髓——不主动扫描全表操作到哪个键才检查哪个键。这个设计的优点很直接零后台开销没有定时器没有扫描线程CPU 占用极低实现简单一个函数六处调用逻辑统一时机精确每次操作都是「最新状态」不会出现「明明过期了但还能读到」的窗口但缺点也同样明显如果某个过期键一直不被访问它会一直占着内存。这就是惰性删除的经典取舍——用空间换时间。四、TTL 语义与 Redis 对齐的 -2 / -1 / 秒数TTL 的返回值语义MiniKV 完全对齐 Redis情况返回值键不存在或已过期被删-2键存在但无 TTL持久键-1键存在且有 TTL剩余秒数≥0expire()的实现也很直白expires_at Clock::now() ttl_value设置成功返回 1键不存在返回 0。这里有个容易踩坑的点expire一个已经过期的键会先触发惰性删除然后返回 0。因为expire命令开头就调用了remove_if_expired_locked键已经被删了自然无法再设置 TTL。从测试代码可以验证这个行为TEST(StoreExpiresKeys){minikv::Storestore(2);store.set(short,value);store.expire(short,std::chrono::seconds(0));// 0秒后过期EXPECT_FALSE(store.get(short).has_value());// 惰性删除触发EXPECT_EQ(store.ttl(short),-2LL);// 不存在 → -2EXPECT_EQ(store.stats().expired,std::uint64_t{1});}expire设置 0 秒过期get时触发删除ttl返回 -2stats().expired计数为 1——整个链路严丝合缝。五、keys() 的顺带清理遍历时的意外收获除了六条命令keys()方法在遍历时也会顺手删除已过期的键store.cpp:127-131。这个设计很有意思——keys()本来只是「列出所有键」但在遍历过程中发现过期键就顺手删掉。一次遍历同时完成了「枚举」和「清理」两件事。不过这里有个值得思考的点keys()的顺带清理是有边界的。如果过期键在遍历中途才到期它可能已经被遍历过了如果过期键在遍历之后才到期它又不会被这次遍历覆盖。所以keys()的清理是尽力而为不是精确保证。六、与 LRU 的协作两个结构必须同步TTL 和 LRU 不是两套独立机制它们共享同一个 Entry必须保持同步。回顾remove_if_expired_locked的删除逻辑同时删lru_和entries_。如果只删哈希表不删 LRU 链表LRU 里就会留下悬空迭代器后续 LRU 淘汰时可能访问野指针——这是 C 里最经典的崩溃源之一。另一个协作点是set已存在的键时expires_at.reset();// 重置 TTL重新变成持久键这个语义很明确对一个已存在的键重新set会清除它的 TTL让它变成持久键。这符合直觉——「我重新赋值了之前的过期设置当然作废」。但这里也暴露了一个设计选择MiniKV 没有「set 时保留原 TTL」的选项。Redis 的SET命令默认也是清除 TTL除非用KEEPTTL选项MiniKV 选择了最简单的语义。七、惰性 vs 主动架构文档的明确取舍MiniKV 的架构文档明确写了一句后台清理器是下一阶段。这意味着作者清楚知道惰性删除的局限——过期键堆积会浪费内存——但选择先把核心功能做对再考虑优化。惰性删除和主动清理的对比维度惰性删除主动清理后台线程内存占用过期键可能堆积及时回收CPU 开销几乎为零需要定时扫描实现复杂度低一个函数高线程、锁、调度访问延迟首次访问过期键稍慢无影响适用场景过期键少、访问频繁过期键多、内存敏感MiniKV 的选择是先用最简单的方案跑通把复杂度留给真正需要的场景。这个思路值得学习——很多系统不是死于功能缺失而是死于过度设计。测试也验证了持久键的行为TEST(StoreReportsPersistentTtl){minikv::Storestore(2);store.set(persistent,value);EXPECT_EQ(store.ttl(persistent),-1LL);// 无TTL → -1}没有设置 TTL 的键ttl返回 -1语义清晰。八、TTL 生命周期全景图把整个流程串起来看set(key, value)→ 创建 Entryexpires_at为nulloptexpire(key, 60)→expires_at now 60s60 秒内get(key)→ 未过期正常返回60 秒后get(key)→ 触发remove_if_expired_locked删除键返回空ttl(key)→ 返回 -2键已不存在这个生命周期里第 4 步是关键——过期键不是「到点就消失」而是「到点后被访问才消失」。这个语义差异是理解 TTL 机制的核心。小结MiniKV 的 TTL 实现用最少的代码完成了核心功能一个optional字段表达过期状态一个函数承担删除逻辑六条命令统一调用配合 LRU 保持双结构同步。它的取舍也很清晰先用惰性删除跑通后台清理器留给下一阶段。这提醒我们——在系统设计里「不做」和「做」同样重要关键是知道当前阶段什么才是最重要的。下一篇预告《线程安全键值存储单互斥锁的正确性优先》——MiniKV 的所有 Store 方法都用std::lock_guardstd::mutex保护连const方法也不例外靠mutable mutex。这篇讲清楚为什么第一阶段选择单把大锁而非更精细的锁策略以及这个正确性优先的设计哲学如何为后续分片锁演进铺路。参考文献与引用cppreference - std::optionalen.cppreference.com/w/cpp/utility/optional——optional 表达可能没有值的语义Redis 官方文档 - EXPIRE/TTLredis.io/docs/latest/commands/expire——-2/-1/秒数的返回值语义对照下载完整源码如需整个工程的源码请在下面的链接下载https://download.csdn.net/download/ganxin7932508/93241722觉得有用点个关注持续获取 MiniKV 系列干货。你在自己的项目里是怎么处理过期键的欢迎留言讨论。
返回列表