说实话,Redis 7.0 并没有像很多人传的那样内置一个“内存分段释放”机制。要安全地删除大 Key 而不让主从同步出问题,真正有效的方法是 **UNLINK + 渐进式拆分 + 配置调优** 这三招。少一环,主从复制就可能断连或者延迟飙上去。

说到底,Redis 7.0 并没有所谓的“内存分段释放”。所谓分段释放是误传,靠谱的做法是 UNLINK、渐进式拆分、配置调优三者配合,否则主从同步照样会断连或延迟飙升。
UNLINK 不等于分段,它只是把删除扔给后台线程
很多人以为 UNLINK 能分段释放内存,其实不是那么回事。UNLINK 干的事情很简单:把 key 的元数据(比如 dictEntry、robj 指针)从数据库中摘掉,然后把真正的内存释放任务丢给 `lazyfree-thread` 后台线程去异步执行。但对于大 Key 来说,后台线程仍然要一次性遍历所有元素、归还内存页——这个过程本身并不拆分,只是不卡在主线程上而已。
- String 类型的大 value(比如 50MB):UNLINK 瞬间返回,后台线程释放整块内存,毫无压力。
- Hash/List/Set/ZSet 类型(包含数万 field 或 member):后台线程还得逐个释放每个子项。如果元素数量特别多(比如 200 万个),照样可能压垮 lazyfree 线程,导致未释放内存堆积。
- 主从同步不受影响?错。主节点执行 UNLINK 后,这个操作会以一条 DEL 命令的形式写入 AOF 并同步到从节点——从节点收到后还是要执行一次同步删除,照样阻塞。
主从同步中真正有效的拆分必须在主节点上渐进执行
要避免复制流被一个大 Key 卡死,唯一可靠的做法是:在主节点上用游标命令分批读取、写入新结构、清理旧字段,让每一步都变成小命令进入复制流。
- 对大 Hash:用 `HSCAN myhash 0 COUNT 50` 拉出 50 个 field,然后 `HSET myhash:shard01 ...` 写入新 key,再 `HDEL myhash f1 f2 ...` 删除这 50 个——每轮最多处理 50 个字段,不会撑爆网络包。
- 对大 ZSet:用 `ZSCAN myzset 0 COUNT 100` 配合 `ZADD myzset:part1` 和 `ZREM myzset`,注意 `ZREM` 虽然支持批量删除,但一次别超过 500 个,否则从节点同步时仍可能超时。
- 所有操作必须在主节点执行:从节点只读,无法参与任何写逻辑;`HSET`、`HDEL` 这些命令都会走复制流,保证从节点最终一致。
- 加一点延迟:比如用 `redis-cli --eval` 跑 Lua 脚本时加上 `SLEEP 0.005`,防止连续高密度命令把主节点 CPU 和复制缓冲区打满。
关键配置必须提前调大,否则 UNLINK 也救不了
就算用了 UNLINK,如果主从复制缓冲区太小,一个大 Key 的同步照样会触发断连。这些配置必须在问题出现之前设好:
- `client-output-buffer-limit slave 512mb 128mb 60`:把默认的 256MB/64MB 翻一倍,防止单次 bulk reply 打穿缓冲区。
- `repl-backlog-size 1024mb`:增大复制积压缓冲区,避免从节点重连时因 offset 脱离而触发全量同步——一旦全量同步,大 Key 又要重来一遍。
- `lazyfree-lazy-server-del yes`:开启 rename、flushdb 等隐式删除的异步化,但注意——它不影响主从同步中 DEL 命令的行为。
- 禁用 `CONFIG SET` 动态改 `maxmemory-policy`:这个命令会阻塞主线程,而且切换瞬间可能触发批量淘汰,引发二次带宽洪峰。
删除后内存不降?不是没释放,是没还给 OS
执行完 UNLINK 或拆分清理后,`INFO memory` 里的 `used_memory_rss` 可能长期不下降,这可不是 bug:
- jemalloc 默认不会立即把空闲内存页归还给操作系统,而是缓存起来供后续分配复用。
- 确认是否真释放:要看 `used_memory`(Redis 逻辑内存)有没有下降,而不是看 `rss`。
- 强制归还:仅限低峰期手动执行 `MEMORY PURGE`,但别拿它当日常治理手段。
- 真正要盯的指标是 `mem_fragmentation_ratio`:持续大于 1.5 说明碎片严重,大 Key 拆分后建议重启实例(滚动重启)来彻底整理内存。