Redis 大量 Key 删除慢的根因与系统化解决方案

🔍 为什么 DEL 这么慢?

根本原因是 Redis 是单线程模型

痛点 解释
DEL 是同步阻塞 主线程亲自遍历数据结构、逐块 free() 内存,期间所有其他命令排队等待
逐个 DEL = 海量网络 RTT 100M 个 key 就算每个只花 0.1ms,串行下来也是几小时,还反复跨网络往返
大 Key 放大问题 DEL 一个百万成员的 ZSet/Hash,时间复杂度 O(N),直接卡住几十~几百毫秒甚至数秒
KEYS 命令不能用 KEYS * 会一次性遍历全量 keyspace,直接把 Redis 堵死,生产环境禁用

✅ 方案一:如果是清空整个 DB → FLUSHDB ASYNC(最快)

bash 复制代码
# Redis 4.0+ 支持异步 flush
redis-cli FLUSHDB ASYNC
# 或
redis-cli FLUSHALL ASYNC

ASYNC 会把整个 DB 的 keyspace 替换为一个新空表,旧数据扔给后台线程释放。这是亿级 key 清空的最快路径,几乎瞬时返回。

⚠️ 但注意:这会清掉整个库,不能只删其中一部分。


这是生产环境标准做法 ,三板斧:SCAN 渐进遍历 + UNLINK 异步删 + sleep 限流

命令行一键版(最简单实用)
bash 复制代码
# -i 0.01 每次迭代间 sleep 10ms,保护 Redis 不被打爆
redis-cli --scan --pattern "your:prefix:*" -i 0.01 | \
  xargs -L 500 redis-cli UNLINK

-L 500 表示每批最多 500 个 key 打包成一个 UNLINK 调用,减少调用次数又不过度膨胀单条命令。

Python 版(可控性最好,推荐用于 100M 级别)
python 复制代码
import redis
import time

r = redis.Redis(host="127.0.0.1", port=6379, db=0, decode_responses=True)

def batch_delete_by_pattern(pattern, scan_count=1000, unlink_batch=500, sleep_ms=10):
    """
    pattern:     匹配模式 e.g. "temp:*" 或 "user:*:session"
    scan_count:  SCAN 每次建议遍历量(非精确值,推荐 1000~5000)
    unlink_batch: 每批 UNLINK 的 key 数(推荐 200~1000)
    sleep_ms:    每批之间的休息微秒数,保护主线程
    """
    cursor = 0
    total = 0

    while True:
        cursor, keys = r.scan(cursor=cursor, match=pattern, count=scan_count)
        if keys:
            # 用 pipeline 批量 unlink,减少 RTT
            for i in range(0, len(keys), unlink_batch):
                batch = keys[i:i+unlink_batch]
                r.execute_command("UNLINK", *batch)
                total += len(batch)
                if total % 10000 == 0:
                    print(f"  ...已删除 {total} 个 key")
                if sleep_ms:
                    time.sleep(sleep_ms / 1000.0)

        if cursor == 0:
            break

    print(f"✅ 完成,共删除 {total} 个 key")

# 执行
batch_delete_by_pattern("your:prefix:*", scan_count=2000, unlink_batch=500, sleep_ms=10)

关键调参建议

参数 起步值 调整方向
scan_count 2000~5000 太大→单次SCAN扫太多;太小→SCAN次数爆炸
unlink_batch 200~500 太大→单条UNLINK参数过长;太小→RTT浪费
sleep_ms 5~20ms 业务高峰期取大值,低谷期取小值

如果 key 列表已经在文件里或从别的来源拿到:

bash 复制代码
# key列表每行一个,分批 UNLINK
cat keys_to_delete.txt | xargs -L 1000 redis-cli UNLINK

或用 Python pipeline 提速:

python 复制代码
with open("keys.txt") as f:
    pipe = r.pipeline()
    for i, line in enumerate(f):
        pipe.unlink(line.strip())
        if i % 1000 == 0:
            pipe.execute()
            pipe = r.pipeline()
    pipe.execute()

⚠️ 如果你是 Redis Cluster → 额外注意 CROSSSLOT

Cluster 模式下,UNLINK k1 k2 k3 要求所有 key 落在同一个 hash slot ,否则报 CROSSSLOT 错误。

解法:按 slot / 按 master 节点分别跑 SCAN,或用 cluster-aware 客户端逐 key unlink:

python 复制代码
# redis-py cluster 模式:逐 key unlink(避免 CROSSSLOT)
from redis.cluster import RedisCluster
rc = RedisCluster(startup_nodes=[{"host":"127.0.0.1","port":7000}], decode_responses=True)

cursor = 0
while True:
    cursor, keys = rc.scan(cursor=cursor, match="temp:*", count=2000)
    for k in keys:
        rc.unlink(k)          # ← 逐 key 就不会 CROSSSLOT
    if cursor == 0:
        break

📊 各方案对比速查

场景 推荐方案 阻塞风险 速度 备注
清整个 DB FLUSHDB ASYNC ⭐几乎零 ⚡最快 全清,不可部分筛选
按 prefix/pattern 删 SCAN + UNLINK + 限流 极低 ★生产首选★
已知 key 列表 分批 UNLINK + Pipeline 很快 注意 cluster 的 slot
有大 Key(百万元素的集合) 必须 UNLINK,别用 DEL DEL会卡死 --- DEL 同步释放内存堵主线程
临时救急/测试 KEYS + DEL(不推荐生产) 🔴高 --- KEYS 全量遍历阻塞

🏗️ 长期架构预防(治本)

100M 个 key 本身就是一种设计债务,建议顺手做:

  1. 给临时数据加 TTL --- 让 Redis 自己惰性过期,根本不用手动删
  2. Key 命名加前缀隔离 --- temp:* / cache:* / session:* 分开,脏数据不跟核心数据混一个 DB
  3. 超大 Value 拆小 --- 一个 Hash 放百万元素不如按 hash:{shard_id} 拆 100 个小 Hash
  4. 定期 MEMORY PURGE --- 删完后释放页碎片(Redis 4.0+)

一句话总结

DEL → 换成 UNLINK;遍历 → 用 SCAN 别用 KEYS;操作 → 分批 + sleep 限流。 这是 Redis 亿级 key 清理的铁三角。