Redis 4.0+安全删除千万级Hash大Key:unlink+淘汰策略
在Redis4.0+中删除千万级Hash大Key时,UNLINK需将lazyfree-lazy-user-del设为yes才能异步执行,否则退化为同步DEL阻塞主线程。运行时需避免WATCH监控、refcount大于1或内存紧张等降级场景。UNLINK不替代EXPIRE,适用于主动清理过期冷数据,且删除后内存不会立即下降,应监控lazyfree_pendin
先说一个核心判断:UNLINK 删除千万级 Hash 本身是安全的,前提是你得确认两个关键点——Redis 配置和运行时状态,否则它可能会悄悄退化回同步 DEL,照样卡住主线程。很多人以为版本到了 4.0 就自动异步了,其实没那么简单。

无情的事实是:如果 lazyfree-lazy-user-del 没配成 yes,UNLINK 的效果跟 DEL 没区别——阻塞依旧。
UNLINK 真正异步的前提:lazyfree-lazy-user-del 必须为 yes
即使 Redis 版本 ≥ 4.0,UNLINK 默认仍然是同步操作。它只在 lazyfree-lazy-user-del 配置开启时才启动后台线程去释放内存。这个配置默认是 no,不手动改等于没开。
- 检查当前值:
CONFIG GET lazyfree-lazy-user-del返回["lazyfree-lazy-user-del","yes"]才有效 - 临时生效:
CONFIG SET lazyfree-lazy-user-del yes(重启后失效) - 永久生效:在
redis.conf中添加或修改为lazyfree-lazy-user-del yes,然后重启 - 注意:该配置只影响
UNLINK、FLUSHDB、FLUSHALL等命令,不影响DEL
也就是说,这第一步没做好,后面所有的“异步删除”都是幻觉。
千万级 Hash 删除前,先确认它不会被 WATCH 或 refcount > 1 拦截
即使配置对了,UNLINK 也可能在运行时遇到一些约束,然后悄无声息地降级为同步删除。它不会报错,也不会给你提示——你看到返回 (integer) 1,但主线程其实已经被阻塞了一把。
- 被
WATCH监控的 Key:事务未提交前调用UNLINK,会立刻同步执行 refcount > 1的 Key:例如刚被OBJECT REFCOUNT查出是 2,或者正在参与RENAME、RESTORE等操作- 内存极度紧张时:
INFO memory中mem_not_counted_for_lazyfree明显上升,说明后台线程已经拒收新任务 - 验证是否真异步:删除前后快速执行
INFO memory,观察used_memory_human是否缓慢下降,同时lazyfree_pending_objects先升后降
这几点很容易被忽略——尤其是生产环境里,没人会特意去查 Key 的 refcount 是不是 1。
配合淘汰策略时,UNLINK 不替代 EXPIRE,而是补位清理
设置过期时间(EXPIRE / PEXPIRE)是预防大Key堆积的第一道防线,但过期不是即时清理。Redis 用的是惰性+定期双策略,冷数据可能滞留数分钟才被真正删除。此时 UNLINK 是主动清场的补位动作,而不是替代方案。
- 高频写入+短 TTL 场景(比如秒杀令牌):优先靠
EXPIRE自动回收,一般不需要手动UNLINK - 低频写入+长周期冷数据(比如用户画像 Hash 存半年):TTL 到期后若监控发现
expired_keys持续增长,可以定时SCAN+UNLINK主动收割 - 不要在 Lua 脚本里调
UNLINK:redis.call("UNLINK", key)会报错ERR unknown command;正确的做法是脚本外判断,外部调用 - 批量删前缀示例:
redis-cli --scan --pattern "profile:hash:*" | head -500 | xargs redis-cli UNLINK(控制批次,别把 BIO 线程压垮)
真正容易被忽略的是:UNLINK 后 used_memory 不会立刻下降,而很多监控告警依赖这个指标做“内存已释放”判断。如果你的运维流程里有“删完立刻检查内存回落”的断言,那得改成查 lazyfree_pending_objects 是否归零,或者加几秒延迟再验。否则告警会一直响,干扰判断。
