首先明确一个核心结论:使用 UNLINK 删除千万级 Hash 大 Key 本身是安全的,但必须确保两个关键条件——Redis 配置和运行时状态,否则它可能静默地退化为同步 DEL,依然会阻塞主线程。许多开发者误以为只要 Redis 版本达到 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,然后重启 Redis 服务 - 注意:此配置仅作用于
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 是否归零,或者增加几秒延迟再验证。否则告警会持续触发,干扰判断。
