Redis 6.0 中 UNLINK 对 TB 级 String 大 Key 不绝对安全:虽多为 raw 编码可异步删,但内存碎片、后台积压、分配器延迟归还等问题仍致 P99 毛刺;需结合 MEMORY USAGE、lazyfree_pending_objects、used_memory_rss 等指标综合判断真实释放状态。

UNLINK 对 String 类型大Key 的实际行为你得看版本和编码
Redis 6.0 中 UNLINK 确实能异步删除大部分大 Key,但对 String 类型不绝对安全——它会先检查底层编码是否为 embstr 或 raw,再决定是否移交后台线程。TB 级 String 在 6.0 里大概率是 raw 编码(因为超过 44 字节就不用 embstr),此时 UNLINK 会走异步路径;但如果该 String 是通过 APPEND 或多次 SET 拼出来的,还可能触发内存碎片化,导致后台释放仍需较长时间。
关键点不是“能不能用 UNLINK”,而是它不保证“零延迟”。一次提交一个 TB 的 UNLINK,后台回收线程可能积压,INFO memory 里的 lazyfree_pending_objects 会飙升,P99 延迟照样毛刺。
- 先用
MEMORY USAGE key_name确认真实内存占用,别只信业务侧的“TB”估算 - 执行前检查
CONFIG GET lazyfree-lazy-user-del是否为yes(6.0 默认开启) - 避免在主从同步压力大、AOF rewrite 正在进行时执行
分片拆解比硬删更可控:把一个 TB String 拆成多个小块
String 类型没法像 Hash 那样用 HSCAN 分批删,但可以反向操作:在写入阶段就按固定大小(比如 1MB)切片存成多个 key,比如 log:20261019:000001、log:20261019:000002……这样删除时就能用 SCAN + DEL 或 UNLINK 控制节奏。
如果已经存在单个 TB String,且无法停写重导,只能靠客户端读取后分段落盘再删原 key——但这要求业务能接受短暂不可用,且带宽和磁盘足够承接临时中转。
- 不要用
GET key一次性读取 TB 数据,客户端容易 OOM;改用GETRANGE key start end分段拉取 - 每段拉取后立刻
UNLINK对应子 key(如果已拆),或标记待删 - 原 key 最后用
DEL(此时已空)或直接RENAME下线,避免残留
监控必须盯住三个数字,否则你以为删完了其实没完
UNLINK 返回成功 ≠ 内存已释放。Redis 后台线程释放的是对象本身,但 jemalloc/tcmalloc 这类分配器未必立刻归还给操作系统。你得交叉看:
-
INFO memory中的used_memory(当前 Redis 使用内存) -
used_memory_rss(进程实际驻留内存)——如果后者远高于前者,说明分配器囤着页没还 -
lazyfree_pending_objects—— 必须降到 0 才算真正清理完毕
尤其注意 used_memory_rss 下降滞后性:哪怕 used_memory 已回落,rss 可能卡住几小时。这时候别急着扩容或告警升级,先查 mem_allocator 和系统 cat /proc/PID/status | grep VmRSS 做交叉验证。
别依赖 EXPIRE,它在大Key场景下反而更危险
给 TB 级 String 设 EXPIRE 看似省事,但到期那一刻触发的释放动作仍是同步的——除非你同时开了 lazyfree-lazy-expire yes。而 Redis 6.0 默认只对 DEL 和用户主动 UNLINK 开 lazyfree,EXPIRE 到期清理默认还是主线程干。
更麻烦的是:过期不是准时的。Redis 用概率抽样+惰性删除结合,TB Key 可能长期不被抽中,又或者在某次集中过期时批量触发,瞬间打满主线程。
- 确认
CONFIG GET lazyfree-lazy-expire是yes,否则别设EXPIRE - 设了也要配合
redis-cli --bigkeys定期扫描,防止“以为快过期了,其实压根没被扫到” - 集群环境下,
EXPIRE不保证所有分片同时触发,主从延迟可能让从库先删、主库后删,引发数据不一致
真正难的不是命令怎么敲,而是判断“这个 TB String 到底有没有被业务强依赖”——很多所谓“可删”的大 Key,删到一半发现下游还在轮询 STRLEN 做进度校验,或者某个离线任务正拿它当临时文件系统用。动手前,一定先查慢日志、客户端连接列表、最近一周的 MONITOR 抽样,再决定是删、拆、还是先加监控告警。











