脚本能自动清理Redis大Key吗?

wen 实用脚本 3

本文目录导读:

脚本能自动清理Redis大Key吗?

  1. 核心思路:分批删除
  2. 常用脚本方案
  3. 注意事项(非常重要)
  4. 推荐的专业工具

是的,脚本可以自动清理Redis中的大Key,但需要谨慎操作,因为不当的清理可能导致长时间阻塞、数据丢失或集群不稳定。

以下是几种常用的脚本方案及关键注意事项:

核心思路:分批删除

直接使用 DEL 删除一个大Key(如一个包含百万元素的List/Set/Hash)会阻塞Redis主线程,导致服务中断,脚本的核心是分批、延迟、扫描删除


常用脚本方案

针对不同类型的大Key,使用对应的游标式命令

#!/bin/bash
# 示例:自动扫描并删除超过 10KB 的 key(针对 Hash/List/Set/ZSet/SortedSet)
REDIS_CLI="redis-cli -h 127.0.0.1 -p 6379 -a yourpassword --no-auth-warning"
THRESHOLD_BYTES=10240  # 10KB
# 使用 SCAN 命令渐进式遍历所有 key
cursor=0
while true; do
    result=$(redis-cli --no-auth-warning -a yourpassword SCAN $cursor COUNT 100)
    cursor=$(echo "$result" | head -1)
    keys=$(echo "$result" | tail -n +2)
    for key in $keys; do
        # 检查内存大小 (DEBUG OBJECT 会返回 serializedlength)
        size=$(redis-cli --no-auth-warning -a yourpassword DEBUG OBJECT "$key" 2>/dev/null | grep -oP 'serializedlength:\K[0-9]+')
        if [ "$size" -ge "$THRESHOLD_BYTES" ]; then
            type=$(redis-cli --no-auth-warning -a yourpassword TYPE "$key")
            echo "Found Large Key: $key (Type: $type, Size: ${size}bytes)"
            # 根据不同结构分批删除
            case "$type" in
                string)
                    # String 可以直删 (但建议确认业务)
                    redis-cli --no-auth-warning -a yourpassword DEL "$key"
                ;;
                list)
                    # 每次 LTRIM 1000 个元素
                    while true; do
                        result=$(redis-cli --no-auth-warning -a yourpassword LTRIM "$key" 1000 -1)
                        # 如果剩余长度 <= 1000,直接 DEL
                        len=$(redis-cli --no-auth-warning -a yourpassword LLEN "$key")
                        if [ "$len" -le 1000 ]; then
                            redis-cli --no-auth-warning -a yourpassword DEL "$key"
                            break
                        fi
                        sleep 0.1  # 延迟,避免阻塞
                    done
                ;;
                set)
                    # 每次 SPOP 1000
                    while true; do
                        size=$(redis-cli --no-auth-warning -a yourpassword SCARD "$key")
                        if [ "$size" -eq 0 ]; then break; fi
                        redis-cli --no-auth-warning -a yourpassword SPOP "$key" 1000 > /dev/null 2>&1
                        sleep 0.1
                    done
                ;;
                hash)
                    # 每次 HDEL 1000 字段
                    while true; do
                        fields=$(redis-cli --no-auth-warning -a yourpassword HKEYS "$key" | head -1000)
                        if [ -z "$fields" ]; then break; fi
                        redis-cli --no-auth-warning -a yourpassword HDEL "$key" $fields
                        sleep 0.1
                    done
                ;;
                zset)
                    # 每次 ZREMRANGEBYRANK 0 999 (删除前1000个)
                    while true; do
                        count=$(redis-cli --no-auth-warning -a yourpassword ZCARD "$key")
                        if [ "$count" -eq 0 ]; then break; fi
                        redis-cli --no-auth-warning -a yourpassword ZREMRANGEBYRANK "$key" 0 999
                        sleep 0.1
                    done
                ;;
            esac
        fi
    done
    if [ "$cursor" -eq 0 ]; then
        break
    fi
done

使用 Python 脚本(更可控、更适合复杂逻辑)

import redis
import time
r = redis.Redis(host='127.0.0.1', port=6379, password='yourpassword')
THRESHOLD = 10000  # 元素数超过 1 万视为大Key
def delete_large_key(key):
    key_type = r.type(key).decode('utf-8')
    print(f"Deleting large key: {key}, type: {key_type}")
    if key_type == 'string':
        # String 特殊处理:确认是否可删除
        r.delete(key)
    elif key_type == 'list':
        while r.llen(key) > 1000:
            # 保留最后 1000 个,前面的全部清除
            r.ltrim(key, 0, -1001)
            time.sleep(0.1)
        r.delete(key)
    elif key_type == 'set':
        while r.scard(key) > 0:
            # 每次 1000 个
            members = r.spop(key, 1000)
            if not members:
                break
            time.sleep(0.1)
    elif key_type == 'hash':
        while True:
            fields = list(r.hkeys(key)[:1000])
            if not fields:
                break
            r.hdel(key, *fields)
            time.sleep(0.1)
        r.delete(key)  # 清理空Key
    elif key_type == 'zset':
        while r.zcard(key) > 0:
            # 每次删除排序最小的 1000 个
            r.zremrangebyrank(key, 0, 999)
            time.sleep(0.1)
# 扫描所有 key
cursor = 0
while True:
    cursor, keys = r.scan(cursor=cursor, count=100)
    for key in keys:
        # 检查元素数量或内存大小
        key_type = r.type(key).decode('utf-8')
        if key_type in ('list', 'set', 'hash', 'zset'):
            size = eval(f'r.{key_type.lower() if key_type != "zset" else "zcard"}(key)')
            if size > THRESHOLD:
                delete_large_key(key)
        elif key_type == 'string':
            # 通过 STRLEN 或 DEBUG OBJECT 检查大小
            if r.strlen(key) > 10240:  # 大于10KB
                delete_large_key(key)
    if cursor == 0:
        break

注意事项(非常重要)

  1. 绝对避免在高峰期直删大Key:即使使用分批方法,如果主库写入压力大,仍可能造成延迟抖动,建议在低峰期执行。
  2. SCAN 命令弱一致性:它不保证完整遍历所有Key,但对于清理大Key,通常可接受。
  3. 对String类型特殊处理:String没有分批删除接口,如果要删一个非常大的String(如几MB),直接 DEL 会阻塞,这种情况下,可以考虑:
    • 使用 UNLINK 命令(Redis 4.0+):异步删除,不阻塞,这是最安全的方式。
    • GET 设空再删(不推荐,只能减少数据拷贝)。
  4. 集群环境:如果使用Redis Cluster,脚本需要处理节点槽位,可以使用 redis-cli --cluster 模式或自行遍历每个节点。
  5. 权限控制:生产环境建议使用只读账号扫描,以避免误删。
  6. 监控与回滚:执行前记录Key列表,删除后观察主从延迟和内存释放情况。

推荐的专业工具

如果不想自己写脚本,可以使用以下成熟工具:

  • Redis Labs 的 redis-rdb-tools:分析RDB文件找到大Key,再针对性删除。
  • redis-cli --bigkeys:扫描并报告大Key(只读,不删除)。
  • 阿里巴巴开源的 redis-shake:支持过滤和迁移,可配置删除大Key规则。

脚本可以自动清理,但强烈建议

  1. 优先使用 UNLINK 异步删除(如果版本 ≥ 4.0)。
  2. 对于复杂数据类型,使用游标或截断命令分批删除。
  3. 加上睡眠延迟(0.1~0.5s)并发限制,防止阻塞。
  4. 先在测试环境验证,生产环境慎重执行。

抱歉,评论功能暂时关闭!