一、设置与查询
EXPIRE user:1:session 3600 # 秒
PEXPIRE user:1:session 3600000 # 毫秒
EXPIREAT user:1:session 1767225600 # 到某个时间戳
TTL user:1:session # 剩余秒数,-1 表示永不过期,-2 表示 key 不存在
PERSIST user:1:session # 去掉过期时间
SET cache:k v EX 3600 # 设置值的同时指定过期
SET 时带 EX 是原子操作,比 SET 完再 EXPIRE 更安全——两步之间如果进程挂掉,就会留下一个永不过期的 key。
二、两种删除策略
Redis 不会为每个 key 起一个定时器(那样开销太大),而是两条路配合:
惰性删除:访问某个 key 时先检查是否过期,过期就删掉并返回空。好处是零额外开销,坏处是不访问就永远不删。
定期删除:每 100 毫秒随机抽取一部分设了过期时间的 key 检查,删掉过期的。抽样而非全扫,所以清不完所有过期键。
两者结合的结果:过期键最终会被清理,但时间不确定。这就是「TTL 到了内存没立刻降」的原因。
三、内存为什么没降
几个可能:
- 大量过期键还没被抽样到(尤其是设置了过期时间的 key 特别多时)。
- 内存碎片:删了数据,分配器没把内存还给操作系统。看
INFO memory的mem_fragmentation_ratio,大于 1.5 可考虑activedefrag yes(Redis 4.0+)。 - 客户端输出缓冲区占用:慢客户端积压了大量待发送数据。
- 主从复制缓冲区、AOF 重写缓冲区。
排查顺序:INFO memory → INFO clients → MEMORY DOCTOR。
四、过期键与持久化
- RDB:过期键不会被写入新生成的 RDB;从 RDB 恢复时,已过期的键会被过滤掉。
- AOF:过期键的
DEL会以显式命令追加到 AOF,重放时不会「复活」。 - 主从:从库不主动删除过期键,等主库发来
DEL才删。所以从库读到的可能是「逻辑上已过期」的数据——这是设计如此,保证主从一致。
这意味着:不要从从库读取强依赖过期语义的数据(比如一次性验证码),要么读主库,要么在应用层再校验一次。
五、缓存过期的实践建议
- TTL 加随机抖动:
EX 3600 + rand(0, 300),避免整点大批 key 同时失效(缓存雪崩)。 - 热点 key 用逻辑过期:值里存
expireAt,物理上不设 TTL,读到过期数据时异步刷新,避免击穿。 - 不要给所有 key 设同样的 TTL:同一批写入的缓存会在同一秒集体消失。
- 永不过期的 key 要能枚举:内存排查时至少知道有哪些长期 key。
- 业务上必须有兜底:缓存挂了要能回源数据库,而不是直接报错。
六、清理大量 key 的正确方式
# 错误:KEYS 阻塞主线程
KEYS user:old:*
# 正确:SCAN 渐进遍历 + UNLINK 异步删除
SCAN 0 MATCH "user:old:*" COUNT 200
UNLINK user:old:1001
删除大 key 一定要用 UNLINK,DEL 是同步释放内存,几百万元素的 Hash 能卡住几百毫秒。
七、监控
INFO keyspace # 各库 key 数量与过期比例
INFO stats # expired_keys 计数
CONFIG GET maxmemory-policy
expired_keys 持续高速增长说明 TTL 设得太短或写入量太大,需要重新评估缓存策略。
小结
过期靠「惰性 + 定期抽样」两条路,TTL 到点不等于内存立刻释放;从库不主动删过期键;TTL 要加随机抖动;清大批 key 用 SCAN + UNLINK。