上周三凌晨,我们的订单系统突然出现大面积超时报警,核心接口P99从50ms飙升至3s,而当天并没有上线改动。排查后发现,一个同事在清理缓存时,用DEL命令删除了一个存储10万条商品数据的Hash大key——这就是今天要聊的「大key删除引发主线程阻塞」问题。你可能觉得这是个老生常谈的话题,但实际踩坑时才发现,很多细节远比文档里写的更复杂。
现象:一次“安静”的缓存清理引发的血案
那天凌晨1点,运维同学按计划执行了一个缓存清理脚本,逻辑简单到令人放松警惕:
redis-cli -h $HOST -p $PORT DEL "product:detail:hotlist" # 删除热门商品Hash
问题在于:这个看似无害的DEL命令背后,藏着一个1.2MB的Hash(存储了10万条field-value,平均每个field 500B),而Redis的单线程模型让这个操作成了性能杀手。
- 数据说话*:我们事后用
DEBUG OBJECT测量了删除耗时:
- Key大小:1.2MB
- 删除耗时:187ms(通过
redis-cli --latency-history观测) - 影响:这187ms内,所有其他命令排队等待,导致接口超时
根因:你以为的原子操作,其实是同步阻塞
“Redis不是单线程吗?为什么删除大key会阻塞?”这是我当时的第一反应。真正理解后才发现,单线程正是问题的核心:
- 工作线程与IO线程分离不够彻底:即使Redis 6.0引入了多线程IO,但实际命令执行仍由主线程串行处理(这也是为什么
DEL这种命令会阻塞) - 内存释放的不可分割性:删除大key本质是遍历释放内存,操作系统回收大块内存时存在竞争(尤其是jemalloc/ptmalloc的锁竞争)
- UNLINK的“伪异步”陷阱:很多人以为用
UNLINK就万事大吉,但在某些版本中,如果key关联的内存超过阈值(默认64MB),仍会同步释放
- 关键代码验证*:通过Redis源码可以看到,删除逻辑在
dbAsyncDelete函数中仍有同步路径:
// Redis 源码片段(简化版)
if (dictSize(key->dict) > 0) {
if (dictSize(key->dict) > LAZYFREE_THRESHOLD) {
// 超过阈值时退化为同步删除
dictRelease(key->dict);
} else {
// 否则走异步
lazyfreeFreeObject(key);
}
}
解法:从“删库跑路”到优雅处理
- 错误示范*:直接
DEL或用不带控制的UNLINK
// Java代码反例 - 直接暴力删除
jedis.del("product:detail:hotlist");
- 正确姿势*:分批次删除 + 异步组合拳
// 分批次删除Hash的field(每次清理500个)
String key = "product:detail:hotlist";
int batchSize = 500;
Cursor<Map.Entry<String, String>> cursor = jedis.hscan(key, SCAN_POINTER_START);
while (!cursor.isClosed()) {
List<String> fieldsToRemove = cursor.stream()
.limit(batchSize)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
if (!fieldsToRemove.isEmpty()) {
jedis.hdel(key, fieldsToRemove.toArray(new String[0]));
}
Thread.sleep(20); // 控制节奏
}
- 优化效果*:
- 峰值延迟:从187ms降至8ms
- 总耗时:从瞬间阻塞变成渐进式的2分钟
- 服务影响:P99稳定在60ms以内
避坑清单:大key删除的隐藏陷阱
- 版本差异坑:Redis 4.0的
UNLINK对list/zset的大元素无效(必须升级到6.0+) - 内存分配器玄学:jemalloc在释放大内存时表现比ptmalloc好30%(实测数据)
- 监控盲区:
MEMORY USAGE命令在大key扫描时也会阻塞,生产环境慎用 - 连接池雪崩:批量删除时未限制并发,可能导致连接池耗尽(建议用连接池+Semaphore)
- 缓存穿透:删除后未考虑重建缓存策略,可能引发DB查询风暴
终极结论:永远假设你的key比想象的大
现在我们的运维规范里加了一条:任何超过1KB的key都必须用SCAN+HSCAN/ZSCAN/SSCAN分批次删除。你可能觉得小题大做,但当你见过凌晨3点因为一个DEL命令引发的全线瘫痪,就会明白谨慎的价值。
你们团队是怎么处理大key删除的?有没有遇到过更诡异的阻塞场景?欢迎在评论区分享你的实战经验。