
好久没聊 Redis 的底层细节了。前两天同事跑过来问我说生产环境有个缓存 key 忘了设置过期时间结果内存被撑爆直接触发 OOM 挨了顿批评。他问我设置过期时间这种基础操作到底有没有一套标准的、稳妥的写法我当时愣了一下因为这问题看似简单真往深了挖能挖出一大串值得注意的细节。先给结论过期时间不是“设置完就完事”的附加功能而是 Redis 内存管理里必须前置考虑的设计决策。无论你是刚接触 Redis 的新手还是已经在生产环境摸爬滚打多年的老手这篇内容都值得从头到尾捋一遍。我会从底层过期机制讲起再把各种设置命令的细节对比、分布式场景下的坑、监控排查的手段全部展开最后给出一套可以直接抄的实操方案。1. 先从核心问题说起Redis 到底是怎么实现“过期”的1.1 过期键的两种存储方式Redis 里每个键值对本质上是一个字典结构。当对某个 key 设置了过期时间这个 key 并不会立刻从内存里消失而是会被记录到另一个独立的字典中这个字典叫“过期字典”expires dict。这里有一个非常关键的概念过期字典里存的是“key 的指针”和“过期时间戳”的映射关系。也就是说被设置过期时间的 key 本身还在主字典里正常存储只是多了一条过期记录。我见过不少初学者以为“设置过期时间等于定时删除”其实完全不是一回事。这个设计直接影响后面要讲的删除策略。举个例子你执行SET user:10001 supersun EX 300这条命令执行后内存中会发生三件事主字典写入user:10001 - supersun过期字典写入user:10001 - 当前时间戳 300秒返回 OK。注意这个 key 此刻依然是“活着”的能被正常读取也占着内存空间。真正的删除动作要等“删除策略”来触发。1.2 三种过期删除策略的取舍Redis 实际使用的过期键删除策略是“惰性删除 定期删除”的混合方案。你需要先理解这个混合方案的来龙去脉才能真正明白为什么有时候 key 过期了内存却迟迟没降下来。惰性删除的逻辑很直观每次读取 key 时先检查这个 key 在不在过期字典里如果在且已过期就立即删除并返回 nil。这样能保证“读到的键一定不是过期键”但问题也很明显——如果一个 key 过期后再也没人访问它就永远躺在内存里变成“僵尸数据”。定期删除就是为了解决僵尸数据问题。Redis 每隔一段时间默认 100ms 一次随机抽取一批设置了过期时间的 key检查并删除其中已过期的键。这里的关键词是“随机抽取”不是全表扫描。因为如果 key 的数量特别多全表扫描会阻塞主线程后果就是所有请求排队等待延迟飙升。我打个比方惰性删除就像商场门口的保安只在有人进门时验票定期删除就像巡逻队每隔几分钟随机抽查一批人。但这两种方式都不能保证“所有过期票立刻被清走”。所以 Redis 还有一个兜底机制内存淘汰策略。1.3 内存淘汰策略与过期时间的联动当 Redis 内存达到maxmemory限制时会根据配置的淘汰策略主动清理数据。这个机制跟过期时间的关系非常密切allkeys-lru/allkeys-lfu/allkeys-random对所有 key 生效包括没设置过期时间的 keyvolatile-lru/volatile-lfu/volatile-random/volatile-ttl仅对设置了过期时间的 key 生效。如果你只给部分 key 设计了过期时间却把淘汰策略配成allkeys-lru那么那些“永久 key”也有可能被 Redis 主动清掉。这招在某些业务场景下没问题但如果你把 Redis 当数据库用存了不能丢的数据那就得格外小心了。我建议所有使用 Redis 的人都养成一个习惯在配置文件里写明maxmemory-policy不要依赖默认值。我见过太多事故就是因为默认策略 无限内存 忘记过期时间最终导致服务不可用。提示关于内存淘汰策略的详细配置可以直接运行CONFIG GET maxmemory-policy查看当前实例的配置。2. 设置过期时间的四种核心命令命令选择与参数语义2.1 EXPIRE最基础但也最容易踩坑的命令EXPIRE是设置过期时间最原始的命令语法如下EXPIRE key seconds这条命令是“以秒为单位”设置过期时间。我见过有人连续执行两次EXPIRE以为第二次会“追加”时间其实不是第二次会直接覆盖第一次设置的值。EXPIRE user:10001 300 EXPIRE user:10001 600上面两行执行完后这个 key 的剩余生存时间变成 600 秒而不是 900 秒。这个特性有时候蛮好用的比如“续期”操作但如果你没意识到它是覆盖语义就可能出现“本来想延长时间结果反而缩短了”的意外。另外一个坑是EXPIRE对不存在的 key 执行会返回 0。我在脚本里见过有人拿这个返回值做“key 是否存在”的判断其实不完全可靠因为返回 0 只能说明“要么 key 不存在要么设置过期时间失败”。2.2 SETEX 与 SET 命令的 EX 选项原子性与可读性SETEX是“SET EXPIRE”的原子结合体语法SETEX key seconds value它的最大价值是原子性。如果你在旧版本 Redis 上分开执行SET再执行EXPIRE中间一旦发生崩溃或者其他客户端修改了这个 key就可能留下一个“永久 key”。但SETEX一步到位不存在中间状态。在新版本 Redis6.2里我更推荐直接用SET命令自带的可选参数SET user:10001 supersun EX 300或者更精确一点用毫秒级的PXSET user:10001 supersun PX 300000这里有个冷门但实用的选项NX只在 key 不存在时写入和XX只在 key 存在时写入。它们可以和过期时间组合使用组成分布式锁SET lock:order:10001 worker-A EX 30 NX这行命令的语义是当且仅当锁不存在时写入一个 30 秒后自动过期的锁。这一条命令同时实现了“加锁”“防重入”“自动过期”三个能力是 Redis 分布式锁的基石。2.3 EXPIREAT / SETEXAT / PEXPIREAT指定时间戳的场景有些场景下你需要把过期时间设置为“某个未来的精确时间点”而不是相对当前时间的偏移量。比如会员到期时间是 2025-06-30 23:59:59那最稳妥的做法不是计算“距离现在多少秒”而是直接指定时间戳。EXPIREAT user:vip:8888 1751299199 PEXPIREAT user:vip:8888 1751299199000EXPIREAT用秒级时间戳PEXPIREAT用毫秒级时间戳。我强烈建议用毫秒级那个因为有些业务场景对精度有要求而且整数溢出风险更小在 32 位系统上秒级时间戳在 2038 年会有问题虽然 Redis 服务器基本是 64 位但这个习惯值得养成。这里有一个很容易被忽视的语义如果你传一个“已经过去”的时间戳Redis 会立刻删除这个 key。这在某些清理逻辑里可以当“手动立即过期”来用。2.4 TTL / PTTL别忽略返回值“-2”和“-1”的含义设置完成之后怎么确认过期时间生效了答案是TTL命令秒级和PTTL命令毫秒级。TTL user:10001 PTTL user:10001返回值有几种情况必须记清楚正整数剩余存活秒数/毫秒数-1key 存在但没有设置过期时间-2key 不存在或已过期。很多人只记得检查“小于等于 0 就算过期”但其实-1和-2的区别非常有用。在排查问题时看到-1说明“这个 key 是永久 key”看到-2说明“这个 key 已经不存在”。这两个值能帮你在几秒钟内判断出到底是谁的锅。注意在 Redis 6.0 之前TTL对不存在的 key 返回 -2如果 key 存在但无过期时间返回 -1。6.0 之后这个行为没变。但有些客户端库例如老版本的某些中间件会把这两个值都映射成“异常”导致误判。建议直接在命令行用redis-cli验证返回值。3. 底层数据结构解析过期字典、时间戳与内存占用3.1 过期字典的结构我前面已经说过过期字典是独立于主字典的。如果你用redis-cli的INFO命令查看内存详情会发现expires这个字段统计的就是“设置了过期时间的 key 数量”。这个字段非常有用。比如你想知道“当前实例有多少 key 最终会自动消失”直接看INFO里的expires字段就行。从源码角度讲过期字典的值是一个long long类型的时间戳单位是毫秒。每次EXPIRE设置时Redis 会把这个值计算成“当前时间戳 秒数 × 1000”然后存进去。每次访问 key 时Redis 拿当前时间戳跟这个值比较判断是否过期。这个“值就是绝对时间戳”的设计带来一个有意思的推论只要服务器时间不跳变过期时间的计算就是精确的。但如果管理员手动修改了系统时间或者 NTP 同步发生大跨度跳变就可能出现“key 集体提前过期”或“key 集体延迟过期”的现象。我真实遇到过一例某台服务器 NTP 跳变导致一批会话 key 提前失效所有用户集体掉线。排查了半天才发现不是业务代码的问题而是系统时间。3.2 内存占用评估过期时间本身也会占用内存这一点很少人注意到设置过期时间本身是“有成本”的。每个进入过期字典的 key都需要额外存储“key 指针 过期时间戳”这对映射关系。虽然单条开销不大但如果你有几千万个 key 都设置了过期时间这部分内存就不是小数了。我做过一次粗略估算一个平均长度的 key比如 20 字节加上过期字典的指针和时间戳开销大约要多占 40~80 字节。一千万个 key 就是 400MB~800MB 的额外内存。这还没算主字典本身的开销。所以内存规划时不建议“不管有没有必要一律都设过期时间”。正确的姿势是该过期的才过期不该过期的比如长期不变的配置类数据就不设置。如果你担心“忘记设置过期时间导致内存膨胀”更好的办法是配合maxmemory-policy里volatile-*系列的淘汰策略强制让“必须能清理的数据”都进入过期字典管理范围。3.3 过期键的删除时机为什么内存不会立即下降继续深入聊删除时机。前面的混合删除策略决定了你设置了一个 60 秒过期的 key可能在 60 秒后的几毫秒内就被定期删除流程清掉也可能因为一直没被访问、且一直没被抽样抽到而继续停留很久。这就有个实际体验你用DEL删除大 key 时会发现内存曲线陡降但用“等待过期”的方式清数据内存曲线往往是楼梯状缓慢下降。有人觉得这是“内存泄漏”其实是策略本身的特性。想要“控制”删除节奏可以调低hz参数但这会影响定期删除的执行频率也会影响其他后台任务的调度不建议轻易动。还有一个偏门但有效的办法主动访问那些“预期已过期但还没被删”的 key触发惰性删除。4. 从设计角度出发什么时候该用过期时间什么时候不该用4.1 适合设置过期时间的场景最常见的三类场景会话管理。用户登录 token、session 这类数据天然有生命周期。设置过期时间等于自动清理逻辑不用专门写定时任务去扫描删除。缓存提高性能。热点数据、接口响应结果、配置快照等设置一个合理的 TTL让它自动失效后重新加载能保证数据最终一致性。限流和防重。比如“5 分钟内最多允许请求 100 次”会用到INCR EXPIRE组合。这个组合有一个坑如果第一次INCR成功后还没来得及EXPIRE进程就崩溃了这个 key 就变成永久 key。解决方案是用 Lua 脚本把两步操作原子化或者干脆用 Redis 官方推荐的滑动窗口限流脚本。4.2 不适合设置过期时间的场景持久化业务数据。比如订单状态、用户余额、消息记录。如果只用 Redis 存储这些关键数据设置过期时间等于给自己埋雷。万一业务逻辑没及时续期数据就消失得无影无踪。需要长期保持一致性的计数器。比如某个累计统计值过期后重新计数会导致报表断裂。除非你能接受重新计数否则别设置过期时间。共享给多个服务的“元数据”。比如服务发现信息、路由表这些数据如果过期时间设置不合理可能导致某个服务瞬间丢失全局视图引发雪崩。这类数据建议“手动更新 心跳保活”而不是单纯依赖过期。4.3 过期时间长短的设计经验这里没有统一标准但有几个经验值可以借鉴token 类一般 30 分钟到 2 小时具体取决于业务安全性要求验证码类5~10 分钟热点配置类5~30 分钟限流窗口类取决于窗口长度通常 1~60 秒会话类如果用户活跃度高考虑“滑动过期”每次访问就重新EXPIRE。“滑动过期”是一个重要技巧代码如下# 读之前先查 TTL小于某个阈值就续期 if TTL session:user:8888 600 EXPIRE session:user:8888 1800 end这段逻辑如果用 Lua 执行可以避免并发下多个客户端同时续期导致 TTL 被覆盖成小值的隐患。5. 实操实录分布式锁、限流器与缓存穿透处理5.1 分布式锁的标准实现与过期时间设定前面提到的SET key value EX seconds NX是分布式锁的推荐写法但真正落地时还需要考虑锁续期问题。一个常见问题业务执行时间超过锁的过期时间导致锁被自动释放其他客户端趁虚而入。解决办法是“看门狗”机制后台线程每隔一段时间检查锁是否还存在如果存在且还是自己的标识就重新设置过期时间。但我不建议自己写看门狗直接用成熟的客户端库更好。比如某些 Redis 客户端框架内置了“自动续期”的锁实现会周期性执行PEXPIRE。如果你真的自己写核心逻辑是这样的# 伪代码每 10 秒执行一次续期锁过期时间 30 秒 while (holding_lock) { if (GET lock:order:10001 worker-A) { PEXPIRE lock:order:10001 30000 } sleep(10) }5.2 限流器实现INCR EXPIRE 的原子性问题限流器最简版本INCR rate:user:10001:minute EXPIRE rate:user:10001:minute 60这段代码的问题在于如果INCR后EXPIRE前崩溃key 就永不消失。用 Lua 脚本可以规避local current redis.call(INCR, KEYS[1]) if current 1 then redis.call(EXPIRE, KEYS[1], ARGV[1]) end return current这样当计数第一次从 0 变 1 时立即设置过期时间之后只做递增不再重复设置既保证了时间窗口的准确性又避免了永久 key。5.3 缓存穿透与“空值缓存 短过期”缓存穿透是指请求查一个必然不存在的数据导致每次请求都打到数据库。常见解法是把空值也缓存起来但设置一个较短的过期时间。SET cache:user:99999 EX 60 NX这个场景下过期时间不要太长30~60 秒比较合适。太短起不到保护作用太长会导致“数据已经创建但缓存里还是空值”的延迟。我用过很多次这个方案配合布隆过滤器效果更好。不过布隆过滤器又是一套独立的体系这里不展开只提醒一点布隆过滤器本身不支持删除操作如果数据频繁增删建议用增强版结构。5.4 缓存雪崩中的过期时间“错峰”技巧缓存雪崩常见的诱因之一是大量 key 在同一时刻过期。比如你批量写了一批热点数据全部设置成 3600 秒过期那么一小时后它们会集体失效数据库瞬间被打爆。解决办法是在过期时间上加入随机扰动SET cache:hot:1001 value EX 3600 random(0, 300)不同 key 的过期时间分布在 3600~3900 秒区间错开峰值。这个技巧成本极低效果立竿见影。我每次批量写入缓存时都会加这个随机偏移实测能显著降低数据库压力波动。6. 进阶话题用 Lua 脚本保证原子性用 SCAN 清理过期键6.1 为什么需要脚本原子性Redis 的单个命令是原子性的但多个命令组合在一起就不是了。EXPIRE 和 SET、GET 和 EXPIRE 组合都有中间状态窗口。在这个窗口里其他客户端可能读到旧值或者干扰状态。Lua 脚本在 Redis 里是原子执行的执行期间不会插入其他命令整个脚本作为一个整体被处理。所以复杂操作尤其涉及“判断 操作”的多步流程优先考虑 Lua。下面这个脚本实现“如果 key 存在且值等于期望值才更新并设置过期时间”if redis.call(GET, KEYS[1]) ARGV[1] then redis.call(SET, KEYS[1], ARGV[2], EX, ARGV[3]) return 1 end return 0这种场景在“乐观锁更新”“条件续期”里特别常用。6.2 用 SCAN 分批清理“漏网之鱼”虽然 Redis 有定期删除但有些命硬的过期 key 可能会残留较长时间。如果你想主动清理又不想阻塞主线程千万别用KEYS *那个命令在大数据量下会卡死 Redis。应该用SCAN命令分批迭代。SCAN 0 COUNT 1000这会返回一个游标和一批 key。接着你可以在客户端对每个 key 执行TTL如果发现TTL为 -2说明已经不存在不必处理如果为 -1说明是永久 key需要你决定是补设过期时间还是手动删除。示例思路伪代码import redis r redis.Redis(hostlocalhost, port6379, db0) cursor 0 while True: cursor, keys r.scan(cursor, count1000) for key in keys: ttl r.ttl(key) if ttl -1 and key.startswith(cache:): r.expire(key, 300) if cursor 0: break这段代码会把所有cache:开头的永久 key 统一补设一个 300 秒过期时间相当于做一次“逃生舱清理”。6.3 Redis 7.x 的新特性过期时间相关的改进Redis 7.x 在过期处理上做了一些改进比如更高效的后台淘汰机制、对--bigkeys扫描的增强。另外 7.4 引入了一些新的数据结构和命令但与过期时间设置无本质变化核心用法不变。如果你还在用 Redis 5.x / 6.x我也不建议为了“过期时间”这个功能专门升级。但如果你同时想用更完善的访问控制、更好的内存管理、AOF 相关的性能优化升级到 7.x 是值得考虑的。唯一要注意的是兼容性测试尤其是一些老客户端库可能不支持新特性。7. 生产环境实战指南配置、监控与排查7.1 监控指标INFO 命令里的关键字段排查过期时间相关问题时第一时间用redis-cli INFO查看几个关键指标指标含义expires设置了过期时间的 key 数量expired_keys累计被删除的过期 key 数量evicted_keys因内存淘汰被删除的 key 数量keyspace_hits/keyspace_misses缓存命中与未命中次数used_memory/maxmemory当前内存使用与上限把这几个字段接入监控告警比如每分钟采集一次能第一时间发现异常。我习惯关注expired_keys的增速如果某个时间段突然飙升说明大量 key 集中过期要检查是不是设置了相同的 TTL如果它一直是 0说明要么没有设置过期时间要么定期删除根本没跑。mem_fragmentation_ratio这个字段也要瞄一眼它代表内存碎片率。如果过高比如超过 1.5即使过期 key 被删了内存也不一定能马上返还给操作系统因为碎片还在。7.2 生产事故案例分析忘了设置过期时间之后说说我遇到的真实案例细节做了脱敏某个内部系统的缓存层所有 key 统一用一种 KeyGenerator 生成代码里只写SET key value完全没有EXPIRE。上线初期数据量小没事半年后 key 数量累计到千万级内存持续走高最终触发 OOM。排查过程先用INFO keyspace查看当前 key 数量发现异常庞大用MEMORY USAGE key抽样几个 key看单个 key 的内存占用用redis-cli --bigkeys扫描定位大 key最后发现是缓存 key 没有过期时间导致只增不减。修复方案写脚本用SCAN批量扫描对所有cache:前缀的 key 补设过期时间先压到 24 小时逐步缩短到目标值修改业务代码在写入缓存时统一加EX参数配置maxmemory-policy volatile-lru确保即使有漏网之鱼也能被淘汰。整个过程大概花了一个下午。事后复盘最核心的教训是生产环境里应该在缓存层封装一个“统一缓存读写工具类”强制要求任何写缓存操作都必须指定 TTL。宁可把 TTL 设长一点也不能不设。7.3 快速排查命令速查表场景命令查看单个 key 剩余时间TTL key或PTTL key查看某个 key 内存占用MEMORY USAGE key查看全部 key 数量DBSIZE查看当前库所有 key 概况INFO keyspace扫描大 keyredis-cli --bigkeys扫描所有 key分批SCAN 0 COUNT 1000查看淘汰策略CONFIG GET maxmemory-policy查看内存上限CONFIG GET maxmemory手动立刻过期某个 keyEXPIRE key 11 秒后过期或者PEXPIREAT key 1这套命令组合下来绝大多数过期时间相关问题都能在 10 分钟内定位。7.4 常见问题速查表问题现象可能原因解决方案key 一直占内存但 TTL 显示已过期惰性删除还没触发定期删除还没抽到手动访问该 key 触发惰性删除或等待定期删除执行TTL 返回 -1key 存在但没有设置过期时间用EXPIRE补设过期时间TTL 返回 -2key 不存在或已过期删除检查命令是否打错了 key或确认过期策略是否意外删除执行 EXPIRE 返回 0key 不存在先 SET 再 EXPIRE或用 SETEX 原子完成设置了过期时间但内存不降过期 key 删除后内存碎片未返还重建实例或执行MEMORY PURGE慎用大量 key 同一时刻过期统一 TTL 未加随机扰动写缓存时加入随机偏移量设置过期时间失败但返回值正常某些旧客户端库不支持 EX/PX 参数更新客户端库或改用 Lua 脚本8. 命令行与主流语言客户端的落地写法8.1 redis-cli 常用操作命令行直接测试是最快的redis-cli SET token:abc hello EX 60 redis-cli TTL token:abc redis-cli PTTL token:abc如果希望“不存在才设置”且“附带过期时间”redis-cli SET lock:user 1 EX 30 NX返回 OK 表示设置成功返回 nil 表示 key 已存在。检查返回结果时注意redis-cli默认输出格式。SET成功返回OKEXPIRE成功返回(integer) 1失败返回(integer) 0。这些细节在生产脚本里经常被忽略。8.2 Python 客户端redis-py 的推荐写法import redis r redis.Redis(hostlocalhost, port6379, decode_responsesTrue) # 推荐SET EX 一步到位 r.set(user:10001, supersun, ex300) # 检查剩余时间 ttl r.ttl(user:10001) print(ttl) # 设置过期时间对已经存在的 key r.expire(user:10001, 600) # 分布式锁写法 ok r.set(lock:order:10001, worker-A, ex30, nxTrue)重点提示redis-py的set方法里ex单位是秒px单位是毫秒两个参数不能同时传。如果传了nxTrue返回值是True或None不是字符串OK判断逻辑不要搞错。连接池方面建议给不同业务场景配置不同的连接池避免一个慢查询拖垮所有请求。8.3 Java 客户端Jedis 与 Lettuce 的写法Jedis 示例Jedis jedis new Jedis(localhost, 6379); jedis.set(user:10001, supersun, new SetParams().ex(300)); Long ttl jedis.ttl(user:10001); jedis.expire(user:10001, 600); jedis.close();Lettuce 示例Spring Boot 场景经常用stringRedisTemplate.opsForValue().set(user:10001, supersun, Duration.ofSeconds(300)); Long ttl stringRedisTemplate.getExpire(user:10001, TimeUnit.SECONDS); stringRedisTemplate.expire(user:10001, 600, TimeUnit.SECONDS);Java 生态里我特别提醒一点很多团队封装了一个RedisUtil工具类里面写缓存时统一调用一个set(key, value, timeout)方法这个方法内部可能会先SET再EXPIRE。你去看它的实现如果发现是两步操作强烈建议改成SET命令自带的EX参数或者用SETEX。否则在高并发 网络抖动环境下可能出现“设置了值但没设置过期时间”的隐患。8.4 Go 客户端go-redis 的写法import ( context time github.com/redis/go-redis/v9 ) rdb : redis.NewClient(redis.Options{Addr: localhost:6379}) ctx : context.Background() err : rdb.Set(ctx, user:10001, supersun, 300*time.Second).Err() if err ! nil { panic(err) } ttl, err : rdb.TTL(ctx, user:10001).Result() if err ! nil { panic(err) } err rdb.Expire(ctx, user:10001, 600*time.Second).Err()在 Go 里注意Set的过期时间参数类型是time.Duration如果传 0 表示不过期。有些新手误以为传0会立即过期实际效果是完全相反。想让 key 立刻过期应该设置一个极小值或直接Del。8.5 Node.js 客户端node-redis 的写法const { createClient } require(redis); const client createClient({ url: redis://localhost:6379 }); await client.connect(); await client.set(user:10001, supersun, { EX: 300 }); const ttl await client.ttl(user:10001); await client.expire(user:10001, 600); await client.quit();Node.js 版本的 API 里set的第三个参数是选项对象属性名是EX大写和NX这种。不同版本的 node-redis 对EX选项的支持有细微差异升级客户端版本后建议重新跑一遍集成测试。9. 从实践角度总结的注意事项写到这里把这几天沉淀的实操经验浓缩成几条核心清单供你在设计和排查时直接对照。所有写缓存的代码路径必须显式传入过期时间禁止“裸 SET”。可以在封装层拦截比如 key 上没有 TTL 就不让写入。读多写少、允许短时间不一致的数据优先设 TTL强一致、不能丢的数据不要依赖 TTL 清理要用明确的淘汰/归档机制。设置过期时间前先想清楚是“相对时间”还是“绝对时间”。业务上有明确到期日的用EXPIREAT系列只是控制缓存长度的用EX系列。多个命令组合操作时用 Lua 脚本保证原子性尤其涉及“判断当前值再更新”的场景。生产环境监控一定要覆盖expired_keys和evicted_keys。前者异常激增可能是有设计缺陷后者出现通常说明内存已满在淘汰数据。别在生产环境直接执行FLUSHALL或FLUSHDB来清理过期键这个操作是同步阻塞的数据量大时会导致长时间的 Redis 不可用。真要清用脚本分批删除。Redis 的过期时间机制设计得相当精妙它不追求“立即清理”而是用惰性删除、定期删除和内存淘汰三层机制在性能、内存和实现复杂度之间取得了平衡。理解这套机制后你再遇到“为什么内存没有立刻降下来”这类问题就不会慌了。最后分享一个小经验排查任何 Redis 问题时第一步永远是看INFO里的内存和键统计数据再看TTL最后再进代码。这个顺序能帮你把“配置问题、数据问题、代码问题”快速区分开少走很多弯路。希望这篇内容对你有实际帮助。