事实上,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`:该命令会阻塞主线程,且切换瞬间可能触发批量淘汰,引发二次带宽洪峰。
删除后内存不降?并非未释放,而是未归还操作系统
执行 UNLINK 或拆分清理后,`INFO memory` 中的 `used_memory_rss` 可能长期不下降,这并非 bug:
- jemalloc 默认不会立即将空闲内存页归还给操作系统,而是缓存起来供后续分配复用。
- 确认是否真正释放:应观察 `used_memory`(Redis 逻辑内存)是否下降,而非关注 `rss`。
- 强制归还:仅在低峰期手动执行 `MEMORY PURGE`,但不要将其作为日常治理手段。
- 真正需要关注的指标是 `mem_fragmentation_ratio`:持续大于 1.5 表示碎片严重,大 Key 拆分后建议重启实例(滚动重启)来彻底整理内存。
