redis过期键清理不是删不掉,而是被动扫描与同步删除导致cpu周期性打满;需优化清理节奏(调hz)、删除方式(unlink替代del)和过期分布(打散ttl)。

直接结论:不是“删不掉”,而是Redis在被动清理时反复扫描、同步删除大Key或集中过期Key,导致CPU被周期性打满。核心要改的是清理节奏、删除方式和过期分布。
为什么TTL返回-2或-1不代表没过期Key?
用TTL key单点查只能验证个别Key状态,无法反映全局压力。真正的问题藏在INFO keyspace里的expires字段(当前有到期时间的key总数)和expired_keys累计值——后者持续上涨说明过期Key正在堆积;而INFO cpu中used_cpu_sys和used_cpu_user同步飙升,基本可锁定是定期删除任务在高频扫描。
-
expires值极大(比如百万级),但db0:keys=1000,expires=950000,说明95%的Key带过期时间,风险极高 -
expired_keys每秒增长数百甚至上千,远超业务写入节奏,说明清理已严重滞后 - 别信
KEYS *——它会阻塞主线程,且结果不可靠;改用SCAN配合TTL脚本批量探查
hz参数调到多少才算合理?
默认hz 10太保守,尤其当expired_stale_perc(过期键占比)长期>25%时,扫描效率跟不上。但盲目拉高会吃光CPU——关键看服务器资源余量和过期Key密度。
Redis 8.2.3 是一款安全优先的高性能键值存储系统。该版本紧急修复了可能引发远程代码执行(RCE)的高危漏洞(CVE-2025-62507),并解决了 HyperLogLog 及 Cuckoo Filter 等数据结构在特定场景下的崩溃问题。建议所有用户立即升级,以保障生产环境的系统稳定与数据安全。
- 观察
INFO stats中的instantaneous_ops_per_sec和used_cpu_sys比值:若OPS<5k但CPU sys持续>70%,说明扫描占满调度周期 - 小规模集群(≤4核)、QPS<1k:先试
config set hz 30,再看expired_stale_perc是否降至10%以下 - 中大型集群(≥8核)、QPS>5k:可设为
hz 100,但必须同步开启lazyfree-lazy-expire yes,否则同步删大Hash/ZSet仍会卡住 - 注意:
CONFIG SET hz在Redis 6.0+才支持热生效;旧版本必须改redis.conf并重启
为什么DEL命令反而让CPU更烫?
对大Key(如含10万成员的ZSET)执行DEL,Redis会同步遍历释放内存,主线程卡死几十毫秒甚至秒级——这期间所有请求排队,定时任务也停摆,形成恶性循环。真正的解法是绕过同步删除。
- 确认大Key存在:用
MEMORY USAGE key测体积,或DEBUG OBJECT key看编码细节 - 立即停用
DEL,改用UNLINK key(Redis 4.0+)——它把释放动作移交后台线程,主线程只做逻辑删除 - 生产环境批量清理必须走Lua:
EVAL "for i=1,#ARGV do redis.call('UNLINK', ARGV[i]) end" 0 key1 key2...,避免网络往返放大延迟 - 如果Redis版本<4.0,只能靠
RENAME+EXPIRE打散删除压力,但效果有限
过期时间打散到底怎么写才安全?
硬编码EXPIREAT key 1747574400等于埋雷。真正有效的打散不是加个随机数就完事,得结合数据生命周期和访问模式。
- 对批量写入场景(如商品列表缓存),按分片哈希而非时间戳:比如
SET category:123:page:0 data EX 3600→ 改成SET category:123:page:0:shard:001 data EX 3600,再用CRC32(key) % 100决定shard编号 - 对时效强的数据(如短信验证码),TTL本身不宜长,但可错开基础时间:原
EX 300→EX 300 + math.random(0, 60)(Lua里生成) - 绝对禁止用
EXPIREAT统一设整点时间(如所有Key设为整点过期),这是引发“过期风暴”的最常见操作 - 上线前用
SCAN抽样检查:SCAN 0 MATCH "category:*" COUNT 1000,再对结果批量TTL,确认分布离散度
最易被忽略的点:lazyfree-lazy-expire开启后,INFO memory里的mem_clients_normal可能短期反升——因为内存释放异步化,统计延迟导致。此时不能误判为无效,要看evicted_keys和expired_keys是否同步下降,这才是真实清理进度。










