当前位置: 首页 > news >正文

【Redis从入门到精通】第26篇:Redis过期键机制——TTL的生死时钟是怎么走的

上一篇【第25篇】Redis数据库那些事——16个数据库的选择与键空间管理
下一篇【第27篇】过期键的骨牌效应——AOF/RDB/复制中的过期处理


每一个带TTL的键,从出生那一刻起,脑袋上就悬着一把达摩克利斯之剑。问题是——谁去拉那根绳子?什么时候拉?

引言:为什么需要过期键?

缓存数据总该有个"保质期"。就像冰箱里的酸奶,过了期你还喝,那不是勇敢,那是作死。Redis作为缓存界的扛把子,自然要提供一套完善的过期机制——让键在指定时间后自动"阵亡",释放内存给更需要的数据。

但问题来了:键过期了,谁来负责"收尸"?是到点就杀,还是等到有人用的时候才发现已经过期?这就是过期删除策略要解决的核心问题。

过期字典:Redis的"死亡名单"

Redis在每个数据库中都维护了一个过期字典(expires dict),它和键空间字典(dict)形影不离。

┌─────────────────────────────────────────────────┐ │ Redis DB 结构 │ │ │ │ ┌──────────────┐ ┌──────────────────────┐ │ │ │ dict (键空间) │ │ expires (过期字典) │ │ │ │ │ │ │ │ │ │ key → value │ │ key → expire_time_ms │ │ │ │ │ │ │ │ │ │ "user:1" → │ │ "user:1" → 1687700000│ │ │ │ {name:"Tom"}│ │ "token:abc"→1687700500│ │ │ │ │ │ │ │ │ │ "token:abc"→ │ │ 注意:key指向同一个 │ │ │ │ "secret123" │ │ 键对象(节省内存!) │ │ │ └──────────────┘ └──────────────────────┘ │ │ │ └─────────────────────────────────────────────────┘

过期字典有几个关键细节值得注意:

  1. key指向同一个键对象:过期字典的key和键空间字典的key指向的是同一个对象,不会额外复制一份,这样做的目的是节省内存。

  2. value是long long毫秒级时间戳:过期时间以绝对时间戳的形式存储,精确到毫秒。比如1687700000000代表的是2023年6月25日某个时刻。

  3. 两个字典的关系:一个键只有在过期字典中存在记录,才算设置了过期时间。如果键被删除,过期字典中对应的记录也会被清除。

这种设计的好处是:查询一个键是否过期,只需要在过期字典中查一次,时间复杂度O(1),非常高效。

设置过期时间:四兄弟命令

Redis提供了四个设置过期时间的命令,虽然看起来有四种,但其实内部殊途同归:

# 方式1:设置秒级相对时间EXPIRE key60# 60秒后过期# 方式2:设置毫秒级相对时间PEXPIRE key60000# 60000毫秒后过期(也是60秒)# 方式3:设置秒级绝对时间(Unix时间戳)EXPIREAT key1687700060# 在指定Unix秒时间戳过期# 方式4:设置毫秒级绝对时间(Unix时间戳)PEXPIREAT key1687700060000# 在指定Unix毫秒时间戳过期

内部转换规则:不管你用哪个命令,Redis内部都会将其转换为PEXPIREAT,即毫秒级绝对时间戳,然后存入过期字典。

转换过程如下:

EXPIRE key 60 → PEXPIRE key 60000 # 秒转毫秒 → PEXPIREAT key (now_ms + 60000) # 相对转绝对 → 存入 expires dict

这种统一存储的设计非常优雅——无论用户用什么方式设置过期时间,底层只需要一种存储格式。

当然,最方便的还是SET命令自带的过期参数:

SET key value EX60# 等价于 SET + EXPIRESET key value PX60000# 等价于 SET + PEXPIRESET key value EXAT1687700060# 等价于 SET + EXPIREAT

踩坑提示SET key value EX 60是原子操作,但SET key value+EXPIRE key 60是两条命令,中间如果断线,键就成了"永生"的。所以能用SET EX就别拆开写。

查询剩余时间:TTL和PTTL

想知道一个键还能活多久?用TTL家族命令:

TTL key# 返回剩余秒数PTTL key# 返回剩余毫秒数

返回值的含义需要特别注意:

返回值含义
正整数剩余生存时间(秒/毫秒)
-1键存在,但没有设置过期时间("永生"键)
-2键不存在(已经死了或者从未出生)
127.0.0.1:6379>SET temp_key"hello"EX60OK127.0.0.1:6379>TTL temp_key(integer)58127.0.0.1:6379>PTTL temp_key(integer)57832127.0.0.1:6379>TTL permanent_key(integer)-1# 永生键127.0.0.1:6379>TTL never_existed(integer)-2# 不存在

踩坑提示TTL返回的是秒级取整值,可能看起来已经到期了但实际上还没到期。如果需要精确判断,请用PTTL。比如TTL返回0,但PTTL可能返回999毫秒——键还活着!

三种过期删除策略:哲学之争

过期键的删除策略,本质上是CPU和内存之间的权衡。学术界提出了三种经典策略:

策略一:定时删除(Timer)

为每个设置了过期时间的键创建一个定时器,到点立即删除。

设置过期时间 │ ▼ ┌─────────────────┐ │ 创建定时器 │ │ timer[key] = t │ └────────┬────────┘ │ 时间到! │ ▼ ┌─────────────────┐ │ 执行删除回调 │ ← CPU开销:每个键一个定时器 │ 删除key │ ← 内存友好:过期即释放 └─────────────────┘

优点:内存最友好,过期键立刻被清理,不会占用一秒多余的内存。

缺点:CPU极其不友好。如果同时有10万个键过期,就会同时触发10万个删除操作,CPU直接被打满。而且创建定时器本身也有开销,Redis使用的是单线程模型,定时器的实现和管理非常复杂。

策略二:惰性删除(Lazy)

不主动删除,只在访问键的时候检查是否过期,过期了才删除。

访问key │ ▼ ┌─────────────────┐ │ 检查是否过期 │ └────────┬────────┘ │ ┌────┴─────┐ │ │ 过期了 没过期 │ │ ▼ ▼ 删除并返空 正常返回值

优点:CPU最友好,只在真正需要的时候才执行删除操作,不会有无谓的CPU消耗。

缺点:内存极不友好。如果一个键过期了但从来没人访问它,它就会一直躺在内存里——这就是传说中的内存泄漏。想象一下,你有一堆过期的优惠券,没人来查,它们就永远占着抽屉的空间。

策略三:定期删除(Periodic)

每隔一段时间,随机检查一批设置了过期时间的键,发现过期的就删除。

定时触发(如每100ms) │ ▼ ┌─────────────────┐ │ 随机抽取一批键 │ │ 检查是否过期 │ │ 过期的删除 │ └────────┬────────┘ │ ┌────┴──────┐ │ │ 过期率>25% 过期率≤25% │ │ ▼ ▼ 继续检查 本轮结束

优点:是前两种策略的折中方案,既不会像定时删除那样疯狂消耗CPU,也不会像惰性删除那样浪费内存。

缺点:策略的"火候"很难把握。检查太频繁,CPU压力大;检查太少,内存回收不及时。而且随机抽取的方式无法保证所有过期键都被及时清理。

三种策略对比

维度定时删除惰性删除定期删除
CPU消耗高(到点必删)低(按需删)中(定期抽删)
内存友好好(立即释放)差(可能泄漏)中(基本及时)
实现复杂度高(定时器管理)低(访问时检查)中(需调参)
实时性强(到点即删)弱(取决于访问)中(取决于频率)

Redis的实际选择:惰性 + 定期

Redis没有选择某一种策略,而是采用了惰性删除 + 定期删除的组合拳:

  • 惰性删除:所有对键的读写命令(GET、SET、HGET等)在执行前都会先检查键是否过期,过期则删除。
  • 定期删除:通过serverCron中的activeExpireCycle函数周期性地清理过期键。

这个选择非常符合Redis的设计哲学——在性能和内存之间找到最佳平衡点。

惰性删除的实现

惰性删除的逻辑嵌入在几乎所有的键操作命令中。当你执行GET key时,Redis会先调用expireIfNeeded函数:

// 伪代码robj*lookupKeyRead(redisDb*db,robj*key){robj*val=dictFind(db->dict,key->ptr);if(val!=NULL){// 检查是否过期if(expireIfNeeded(db,key)){// 键已过期,已被删除returnNULL;}// 更新LRU/LFU信息returnval;}returnNULL;}

定期删除的实现细节

定期删除由activeExpireCycle函数实现,它在serverCron中每100毫秒被调用一次。核心逻辑如下:

activeExpireCycle 执行流程 │ ▼ ┌──────────────────────────────────┐ │ 每次遍历若干个DB(默认16个) │ │ 每个DB随机抽取20个键检查 │ │ (ACTIVE_EXPIRE_CYCLE_LOOKUPS │ │ _PER_LOOP = 20) │ └──────────────┬───────────────────┘ │ ▼ ┌──────────────────────────────────┐ │ 如果本轮过期的键占总检查键的比例 │ │ 超过25%,则继续对该DB检查 │ │ 否则切换到下一个DB │ └──────────────┬───────────────────┘ │ ▼ ┌──────────────────────────────────┐ │ 整个周期的时间上限 │ │ 默认为serverCron周期的25% │ │ 超时则强制退出,避免阻塞 │ └──────────────────────────────────┘

关键的25%阈值设计非常巧妙:

  • 如果过期率很高(>25%),说明有过期键堆积,需要继续清理。
  • 如果过期率低(≤25%),说明当前DB比较干净,可以去检查下一个DB。
  • 这保证了清理过程不会无限循环,也保证了在过期键较多时能加大清理力度。
// 伪代码:activeExpireCycle核心逻辑do{num_expired=0;num_checked=0;while(num_checked<ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP){// 随机抽取一个设置了过期时间的键key=randomKeyFromExpiresDict(current_db);if(isExpired(key)){deleteKey(key);num_expired++;}num_checked++;}// 计算过期比例ratio=num_expired/num_checked;}while(ratio>0.25&&time_not_exceeded());

踩坑提示:定期删除是随机抽取的,这意味着如果你有大量键设置了相同的过期时间(比如半夜12点统一过期),定期删除可能在过期发生时清理不及时,惰性删除又没人访问这些键,导致内存短暂飙升。这就是缓存雪崩的诱因之一。

PERSIST命令:赋予"永生"

如果一个键突然想开了,不想死了怎么办?PERSIST命令可以移除键的过期时间,让它变成永久键:

127.0.0.1:6379>SET temp"I will expire"EX60OK127.0.0.1:6379>TTL temp(integer)58127.0.0.1:6379>PERSIST temp(integer)1127.0.0.1:6379>TTL temp(integer)-1# 变成永生键了!

PERSIST的内部实现就是从过期字典中删除对应的记录:

intremoveExpire(redisDb*db,robj*key){// 从过期字典中删除该key的过期时间记录returndictDelete(db->expires,key->ptr)==DICT_OK;}

返回值:1表示成功移除过期时间,0表示键不存在或本来就没有过期时间。

过期键被删除时发生了什么

当过期键被删除(无论是惰性删除还是定期删除),Redis并不是简单地把键从字典中删掉就完事了。它还会触发一系列后续操作:

过期键被删除 │ ├──► 从键空间字典(dict)中删除键值对 │ ├──► 从过期字典(expires)中删除过期记录 │ ├──► 如果开启了AOF持久化: │ 追加一条DEL命令到AOF缓冲区 │ ├──► 如果开启了键空间通知: │ 发送expired事件通知 │ ├──► 更新键空间统计信息 │ (keyspace_hits / keyspace_misses等) │ └──► 触发内存回收相关逻辑 (如果配置了maxmemory策略)

其中最值得关注的两个副作用:

  1. AOF追加:过期键被删除后,AOF文件中会追加一条DEL key命令。这保证了AOF重放时也能正确删除过期键。

  2. 键空间通知:如果客户端订阅了过期事件(我们将在下一篇文章详细讨论),Redis会发送一个expired通知。

生产实践:合理设置TTL

避免批量键同时过期

这是最常见的缓存雪崩场景。假设你有一个活动,所有优惠券的过期时间都设为活动结束的那一秒:

# 危险做法:所有键同时过期forcouponincoupons: EXPIRE coupon:{id}86400# 统一24小时后过期

当86400秒到来时,大量键同时过期,如果此时又有大量新请求涌入,所有请求都会穿透到数据库,造成缓存雪崩。

正确做法:在过期时间上加随机偏移:

# 安全做法:过期时间加随机偏移importrandomforcouponincoupons: ttl=86400+ random.randint(-3600,3600)# ±1小时随机偏移EXPIRE coupon:{id}$ttl

为所有缓存键设置TTL

即使你不确定一个键该活多久,也给它设一个合理的上限(比如7天)。没有TTL的键就像冰箱里没有标注日期的剩饭——你永远不知道它什么时候开始变质,但它一定会变质。

# 即使是长期缓存,也设个兜底TTLSET user:10086"{data}"EX604800# 7天兜底

监控过期键情况

使用INFO keyspace命令查看各数据库的键空间统计:

127.0.0.1:6379>INFO keyspace# Keyspacedb0:keys=100000,expires=80000,avg_ttl=3456789
  • keys:总键数
  • expires:设置了过期时间的键数
  • avg_ttl:平均剩余TTL(毫秒)

如果expires/keys的比例很低,说明大部分键没有过期时间,需要警惕内存泄漏风险。

OBJECT IDLETIME的局限

OBJECT IDLETIME命令可以查看一个键的空闲时间(多久没被访问),常用于实现类似LRU的淘汰逻辑:

127.0.0.1:6379>OBJECT IDLETIME mykey(integer)3600# 空闲了3600秒

但这个命令有一个重要局限:LRU时钟精度有限。Redis的LRU时钟默认每100毫秒更新一次(由server.hz控制),所以OBJECT IDLETIME返回的值可能有最多100毫秒的误差。对于绝大多数场景来说这无所谓,但如果你需要精确到毫秒的空闲时间,那就没法靠这个命令了。

此外,OBJECT IDLETIME本身也会更新键的访问时间吗?不会!这是它和GET等命令的区别——OBJECT IDLETIME读取空闲时间但不更新LRU时钟,否则它本身就变成了一个"观察者效应"的bug。

总结

Redis的过期键机制是一个精巧的设计:

  1. 过期字典以O(1)的复杂度管理所有过期时间,key共享节省内存
  2. 四个过期命令内部统一转换为PEXPIREAT,用毫秒绝对时间戳存储
  3. 惰性删除+定期删除的组合策略在CPU和内存之间取得了良好平衡
  4. 定期删除的25%阈值设计让清理力度自适应
  5. 过期键的删除会触发AOF追加和键空间通知等副作用

理解过期键机制,不仅能帮你写出更合理的TTL策略,还是理解Redis持久化和主从复制中过期键行为的基础——这是我们下一篇文章要讨论的内容。


上一篇【第25篇】Redis数据库那些事——16个数据库的选择与键空间管理
下一篇【第27篇】过期键的骨牌效应——AOF/RDB/复制中的过期处理


http://www.cnnetsun.cn/news/2696229.html

相关文章:

  • 【Redis从入门到精通】第28篇:数据库通知——Redis的事件订阅机制
  • 终极指南:3个秘诀让你成为虚幻引擎游戏修改大师
  • GetQzonehistory:3分钟永久备份QQ空间说说的免费高效方案
  • 超越官方Demo:如何微调YOLOv8模型让BotSORT在体育视频中跟踪得更准更稳?
  • 别再微调CLIP了!Sora 2原生支持“战术意图编码器”,3步接入教练战术板(含英超某队真实部署案例+JSON Schema模板)
  • ExtractorSharp:一站式游戏资源编辑解决方案,让NPK和IMG文件处理变得简单高效
  • Pearcleaner:macOS应用清理革命,告别数字垃圾的一站式解决方案
  • OBS StreamFX完整指南:免费打造专业级直播特效的终极教程
  • CCS12.2配置避坑:手把手教你为DSP28335生成可烧录的.bin文件(解决‘C:’报错)
  • 全球仅12家机构掌握的Sora 2物理锚定技术:如何让虚拟物体在真实视频中承受真实反作用力?
  • Oni-Duplicity深度解析:基于TypeScript与Redux的《缺氧》存档编辑器架构设计与实现原理
  • 51单片机四则运算计算器完整Keil工程:矩阵键盘输入+数码管显示(含源码与HEX)
  • 终极解决方案:如何一键安装所有Visual C++运行库,告别“缺少dll文件“错误
  • 如何5分钟掌握FF14智能钓鱼:渔人的直感终极指南
  • Arduino与3D打印打造万圣节互动糖果机:从硬件到软件的完整DIY指南
  • 基于Django搭建的药房库存后台系统(含MySQL建库脚本与一键部署指南)
  • 基于STM32F103的T12焊台温控主板方案:含多版原理图、Arduino源码与OLED图形化菜单
  • 如何快速掌握LaTeX公式转Word:面向学术工作者的终极解决方案
  • MATLAB版NSGA-II多目标优化工具包:含完整源码、逐函数文档与可运行示例
  • SteamShutdown终极指南:如何让电脑在Steam下载完成后自动关机
  • 打造智能电视专属媒体中心:Jellyfin Android TV客户端完整指南
  • 趣味电路入门:用铜胶带与筷子制作帽子LED开关
  • 从零开始HTML:构建网页骨架的完整指南与实战
  • 生成式AI如何重塑新闻生产:从自动化写作到人机协同的未来
  • PHP 完全指南:从入门到现代 Web 开发
  • 终极指南:5分钟用ImageToSTL将图片转换为3D打印模型
  • Sora 2信息图表动画效能跃迁:实测对比传统工具提速3.7倍,关键帧压缩率提升62%(内部压测报告首曝)
  • 2025-2026年ai写小说软件测评推荐:五大口碑产品评测沉浸创作提速注意事项
  • Sora 2生成视频色彩崩坏?3步精准校色流程曝光:LUT映射+时序一致性补偿+光流遮罩修复
  • Sora 2编码参数设置全解析(附官方未公开的rate_control_mode隐式优先级规则)