volatile-ttl策略未失效,而是因候选键TTL高度集中导致淘汰失效;需通过扫描全库统计TTL分布、监控expired_keys与evicted_keys比值来确认,并采用随机偏移、换用allkeys-lru等策略破局。

volatile-ttl 策略为什么“看起来失效”
volatile-ttl 不是失效了,而是根本没机会生效——它只在内存达到 maxmemory 且需淘汰键时才触发,且只从**已设置过期时间的键(volatile keys)中,挑剩余 TTL 最短的那个**删除。如果所有带 TTL 的 key 剩余时间都集中在几秒内(比如全剩 1~3 秒),Redis 淘汰时随机采样后发现“大家快一起死了”,就无法形成有效区分,结果可能反复淘汰刚写入的新 key,或干脆触发 noeviction 拒绝写入。
怎么确认是不是 TTL 高度集中导致的
别猜,直接查。重点不是看单个 key,而是看整体分布:
- 用
redis-cli --scan --pattern "*"扫描全库 key(注意避开大 key 扫描影响性能) - 对每个 key 调用
TTL命令,收集返回值(-2=不存在,-1=无 TTL,≥0=剩余秒数) - 统计分布:比如跑个简单脚本导出所有
TTL值,再用sort | uniq -c | sort -nr看是否大量集中在某个窄区间(如 3600±5 秒) - 特别关注
INFO stats中的expired_keys(累计过期数)和evicted_keys(累计淘汰数)比值——若前者远大于后者,说明过期机制在跑,但淘汰策略没起作用
为什么 Dify 这类平台容易踩这个坑
Dify 默认配置里 ttl: 3600 是全局硬编码,所有会话、限流计数器、临时任务都统一设 1 小时。一旦服务重启、批量导入或定时刷新缓存,就会批量生成一批「同生共死」的 key。更麻烦的是:volatile-ttl 在这种场景下完全失效——因为所有候选键的 TTL 几乎相等,Redis 随机选一个删,等于没策略。
- 典型表现:内存缓慢上涨,
evicted_keys几乎不增,但used_memory_peak_human持续逼近maxmemory -
CONFIG GET active-expire-effort返回 1(默认值)时,定期删除力度太弱,大量“僵尸过期 key”堆在内存里,进一步压缩了真正需要淘汰的空间 - Spring Data Redis 或 Dify 自己封装的 client 若用了
set(key, value, timeout)但没做时间扰动,问题会放大
实操上怎么破局
核心思路不是调高 active-expire-effort,而是让 TTL 分散开,把“集体死亡”变成“错峰离线”:
- 写入时主动加随机偏移:
EXPIRE key (3600 + rand(60, 300))—— 给 1 小时基础 TTL 加 1~5 分钟抖动 - 避免用
SET key val EX 3600这种确定性写法;改用SETEX或客户端代码显式计算带偏移的秒数 - 对已存在的 key 批量修正:用
SCAN+TTL+EXPIRE脚本重设(注意控制 QPS,别打满) - 如果业务允许,直接换策略:
allkeys-lru或allkeys-lfu不依赖 TTL,反而更稳——前提是能接受无 TTL 的核心数据也被淘汰
最易被忽略的一点:volatile-ttl 只看“还剩多久”,不看“什么时候设的”。哪怕你分散设置了 TTL,只要运行一段时间后所有 key 剩余时间又收拢到同一窗口(比如都剩 10 秒),问题照旧。所以必须持续监控 TTL 分布,不能设完就不管。











