Redis Key数量过多:完整清理指南与最佳实践

Redis中Key数量过多会导致内存压力增大、性能下降、备份和恢复变慢等问题。以下是完整的清理方案。

1. 分析和诊断阶段

1.1 查看当前Key数量

```bash
# 查看所有数据库的key数量
redis-cli info keyspace

# 查看当前数据库key总数
redis-cli dbsize

# 查看每个数据库的详细信息
redis-cli --bigkeys
```

1.2 分析Key分布

```bash
# 安装redis-stat工具进行实时监控
git clone https://github.com/junegunn/redis-stat.git
cd redis-stat
./redis-stat --server

# 或者使用redis-rdb-tools分析RDB文件
pip install rdbtools
rdb --command memory dump.rdb --bytes 128 --largest 20 > memory_report.csv
```

2. 渐进式清理策略

2.1 使用SCAN命令(推荐)

```bash
#!/bin/bash
# safe_clean.sh - 安全渐进式清理

REDIS_HOST="127.0.0.1"
REDIS_PORT=6379
REDIS_PASS=""
PATTERN="temp:*"  # 要清理的模式
BATCH_SIZE=1000   # 每批处理数量
SLEEP_TIME=0.1    # 批次间隔
MAX_ITERATIONS=1000000  # 最大迭代次数

# 构建redis-cli命令
REDIS_CMD="redis-cli -h $REDIS_HOST -p $REDIS_PORT"
[ -n "$REDIS_PASS" ] && REDIS_CMD="$REDIS_CMD -a $REDIS_PASS"

echo "开始渐进式清理模式: $PATTERN"

cursor="0"
iteration=0
total_deleted=0

while [ "$cursor" != "0" ] || [ $iteration -eq 0 ]; do
    if [ $iteration -ge $MAX_ITERATIONS ]; then
        echo "达到最大迭代次数"
        break
    fi
    
    # 获取一批key
    result=$($REDIS_CMD SCAN $cursor MATCH "$PATTERN" COUNT $BATCH_SIZE 2>/dev/null)
    cursor=$(echo "$result" | head -1)
    keys=$(echo "$result" | tail -1)
    
    if [ -n "$keys" ] && [ "$keys" != "nil" ]; then
        # 统计key数量
        key_count=$(echo $keys | wc -w)
        
        if [ $key_count -gt 0 ]; then
            echo "迭代 $iteration: 找到 $key_count 个key,准备删除..."
            
            # 使用UNLINK(非阻塞删除,Redis 4.0+)
            $REDIS_CMD UNLINK $keys 2>/dev/null
            
            total_deleted=$((total_deleted + key_count))
            echo "已删除 $total_deleted 个key"
        fi
    fi
    
    iteration=$((iteration + 1))
    
    # 小延迟避免阻塞
    sleep $SLEEP_TIME
    
    # 每10000个key输出一次进度
    if [ $((iteration % 10)) -eq 0 ]; then
        echo "进度: 迭代 $iteration, 已删除 $total_deleted 个key"
    fi
done

echo "清理完成,总共删除 $total_deleted 个key"
```

2.2 分批次清理不同模式

```bash
#!/bin/bash
# multi_pattern_clean.sh - 多模式批量清理

PATTERNS=(
    "temp:*"
    "cache:temp:*"
    "session:*"
    "test:*"
    "debug:*"
    "lock:*"
    "tmp:*"
)

for pattern in "${PATTERNS[@]}"; do
    echo "开始清理模式: $pattern"
    
    # 先查看有多少个key
    count=$(redis-cli --scan --pattern "$pattern" | wc -l)
    
    if [ $count -gt 0 ]; then
        echo "找到 $count 个key,开始删除..."
        
        # 分批次删除
        redis-cli --scan --pattern "$pattern" | \
        while read key; do
            echo "DEL $key" | redis-cli --pipe
        done
        
        echo "模式 $pattern 清理完成"
    else
        echo "模式 $pattern 没有匹配的key"
    fi
    
    echo "等待5秒..."
    sleep 5
done
```

3. 按条件清理

3.1 按TTL清理

```bash
#!/bin/bash
# clean_by_ttl.sh - 按过期时间清理

# 清理已过期的key
redis-cli --scan --pattern "*" | while read key; do
    ttl=$(redis-cli ttl "$key")
    
    # ttl = -2 表示key不存在
    # ttl = -1 表示没有设置过期时间
    if [ "$ttl" -eq -2 ]; then
        # key已不存在,跳过
        continue
    elif [ "$ttl" -eq -1 ]; then
        # 没有过期时间的key,可以按业务逻辑处理
        # 例如:删除7天未访问的key
        idle_time=$(redis-cli object idletime "$key" 2>/dev/null || echo 0)
        if [ "$idle_time" -gt 604800 ]; then  # 7天 = 604800秒
            echo "删除闲置key: $key (空闲 $idle_time 秒)"
            redis-cli unlink "$key"
        fi
    fi
done
```

3.2 按内存大小清理

```bash
#!/bin/bash
# clean_large_keys.sh - 清理大Key

THRESHOLD=1048576  # 1MB = 1048576字节

echo "查找大于 ${THRESHOLD} 字节的key..."

redis-cli --scan --pattern "*" | while read key; do
    # 获取key的内存使用情况(Redis 4.0+)
    memory=$(redis-cli memory usage "$key" 2>/dev/null)
    
    if [ -n "$memory" ] && [ "$memory" -gt "$THRESHOLD" ]; then
        key_type=$(redis-cli type "$key")
        echo "发现大key: $key (类型: $key_type, 大小: $memory 字节)"
        
        # 根据类型采取不同策略
        case $key_type in
            "string")
                # 对于字符串,可以直接删除或分割
                redis-cli unlink "$key"
                ;;
            "hash")
                # 对于Hash,可以逐步删除字段
                # redis-cli hscan "$key" 0 count 100 | while read field; do
                #     redis-cli hdel "$key" "$field"
                # done
                redis-cli unlink "$key"
                ;;
            "list"|"set"|"zset")
                redis-cli unlink "$key"
                ;;
        esac
    fi
done
```

4. 使用Lua脚本进行原子清理

4.1 Lua批量删除脚本

```lua
-- delete_keys.lua
-- 使用SCAN迭代删除,避免阻塞

local cursor = '0'
local pattern = ARGV[1] or '*'
local batch_size = tonumber(ARGV[2]) or 1000
local limit = tonumber(ARGV[3]) or 10000  -- 最多删除数量
local deleted = 0

repeat
    -- 扫描一批key
    local result = redis.call('SCAN', cursor, 'MATCH', pattern, 'COUNT', batch_size)
    cursor = result[1]
    local keys = result[2]
    
    -- 删除这批key
    if #keys > 0 then
        -- 使用UNLINK避免阻塞
        redis.call('UNLINK', unpack(keys))
        deleted = deleted + #keys
        
        -- 输出进度
        redis.log(redis.LOG_NOTICE, "已删除 " .. deleted .. " 个key")
        
        -- 如果达到限制,停止
        if deleted >= limit then
            break
        end
    end
    
    -- 如果游标为0,扫描完成
    if cursor == '0' or tonumber(cursor) == 0 then
        break
    end
    
    -- 小延迟,避免阻塞
    redis.call('TIME')
until false

return deleted
```

执行脚本:

```bash
# 加载并执行Lua脚本
redis-cli --eval delete_keys.lua , "temp:*" 1000 10000
```

4.2 复杂条件清理脚本

```lua
-- conditional_clean.lua
-- 按条件清理key

local pattern = ARGV[1] or '*'
local min_idle_seconds = tonumber(ARGV[2]) or 86400  -- 默认24小时
local max_memory = tonumber(ARGV[3]) or 1048576  -- 默认1MB
local action = ARGV[4] or 'delete'  -- delete|expire|log

local cursor = '0'
local stats = {
    deleted = 0,
    expired = 0,
    skipped = 0,
    large_keys = 0
}

repeat
    local result = redis.call('SCAN', cursor, 'MATCH', pattern, 'COUNT', 500)
    cursor = result[1]
    local keys = result[2]
    
    for i, key in ipairs(keys) do
        local should_process = false
        
        -- 检查空闲时间
        local idle = redis.call('OBJECT', 'IDLETIME', key)
        if idle >= min_idle_seconds then
            should_process = true
        end
        
        -- 检查内存大小(Redis 4.0+)
        local memory = 0
        if redis.pcall('MEMORY', 'USAGE', key) then
            memory = redis.call('MEMORY', 'USAGE', key)
            if memory >= max_memory then
                stats.large_keys = stats.large_keys + 1
                should_process = true
            end
        end
        
        if should_process then
            if action == 'delete' then
                redis.call('UNLINK', key)
                stats.deleted = stats.deleted + 1
            elseif action == 'expire' then
                redis.call('EXPIRE', key, 60)  -- 设置为60秒后过期
                stats.expired = stats.expired + 1
            elseif action == 'log' then
                -- 仅记录,不执行操作
                redis.log(redis.LOG_WARNING, "候选key: " .. key .. 
                    " (空闲: " .. idle .. "秒, 大小: " .. memory .. "字节)")
            end
        else
            stats.skipped = stats.skipped + 1
        end
    end
    
    -- 每批处理完稍作休息
    if #keys > 0 then
        redis.call('TIME')
    end
until cursor == '0'

return stats
```

5. 自动化和定时清理

5.1 Crontab定时任务

```bash
# 编辑crontab
crontab -e

# 添加以下内容
# 每天凌晨3点清理临时key
0 3 * * * /usr/local/bin/clean_redis_temp.sh

# 每小时清理过期的session
0 * * * * /usr/local/bin/clean_expired_sessions.sh

# 每周日凌晨2点执行深度清理
0 2 * * 0 /usr/local/bin/deep_clean_redis.sh

# 每月1日凌晨1点清理测试数据
0 1 1 * * /usr/local/bin/clean_test_data.sh
```

5.2 Systemd定时服务

```ini
# /etc/systemd/system/redis-cleanup.service
[Unit]
Description=Redis Key Cleanup Service
After=network.target redis.service
Requires=redis.service

[Service]
Type=oneshot
User=redis
ExecStart=/usr/local/bin/redis-cleanup.sh
StandardOutput=journal

# /etc/systemd/system/redis-cleanup.timer
[Unit]
Description=Run Redis cleanup daily

[Timer]
OnCalendar=daily
Persistent=true

[Install]
WantedBy=timers.target
```

```bash
# 启用定时器
sudo systemctl daemon-reload
sudo systemctl enable redis-cleanup.timer
sudo systemctl start redis-cleanup.timer
```

6. 监控和告警

6.1 监控脚本

```bash
#!/bin/bash
# monitor_redis_keys.sh

THRESHOLD_WARNING=1000000  # 警告阈值
THRESHOLD_CRITICAL=2000000 # 严重阈值
ALERT_EMAIL="admin@example.com"

# 获取key数量
KEY_COUNT=$(redis-cli dbsize)

# 获取内存使用
MEMORY_INFO=$(redis-cli info memory)
USED_MEMORY=$(echo "$MEMORY_INFO" | grep "used_memory:" | cut -d: -f2)

echo "Redis Key监控报告"
echo "================="
echo "当前时间: $(date)"
echo "Key总数: $KEY_COUNT"
echo "内存使用: $USED_MEMORY 字节"

# 检查阈值
if [ $KEY_COUNT -ge $THRESHOLD_CRITICAL ]; then
    SUBJECT="[CRITICAL] Redis Key数量严重超标"
    MESSAGE="Redis Key数量已达到 $KEY_COUNT,超过严重阈值 $THRESHOLD_CRITICAL"
    
    echo "$SUBJECT"
    echo "$MESSAGE"
    
    # 发送邮件告警
    echo "$MESSAGE" | mail -s "$SUBJECT" $ALERT_EMAIL
    
    # 触发自动清理
    /usr/local/bin/emergency_cleanup.sh
    
elif [ $KEY_COUNT -ge $THRESHOLD_WARNING ]; then
    SUBJECT="[WARNING] Redis Key数量警告"
    MESSAGE="Redis Key数量已达到 $KEY_COUNT,超过警告阈值 $THRESHOLD_WARNING"
    
    echo "$SUBJECT"
    echo "$MESSAGE"
    
    echo "$MESSAGE" | mail -s "$SUBJECT" $ALERT_EMAIL
fi

# 生成详细报告
echo ""
echo "Key模式分析:"
echo "------------"

PATTERNS=(
    "user:*"
    "session:*"
    "cache:*"
    "temp:*"
    "product:*"
    "order:*"
)

for pattern in "${PATTERNS[@]}"; do
    count=$(redis-cli --scan --pattern "$pattern" | wc -l)
    if [ $count -gt 0 ]; then
        percentage=$(echo "scale=2; $count * 100 / $KEY_COUNT" | bc)
        echo "$pattern: $count 个 ($percentage%)"
    fi
done

# 记录到日志文件
LOG_FILE="/var/log/redis_monitor.log"
echo "[$(date)] Keys: $KEY_COUNT, Memory: $USED_MEMORY" >> $LOG_FILE
```

6.2 使用Redis自带的监控

```bash
# 启用Redis监控
redis-cli config set notify-keyspace-events Ex

# 监控过期key事件
redis-cli --csv psubscribe '__keyevent@0__:expired'

# 查看慢查询
redis-cli slowlog get 10

# 查看客户端信息
redis-cli client list
```

7. 最佳清理策略

7.1 分级清理策略

```bash
#!/bin/bash
# tiered_cleanup.sh - 分级清理策略

echo "=== Redis分级清理策略 ==="
echo ""

# 第一级:清理明显无用的key
echo "1. 清理临时和测试key..."
redis-cli --scan --pattern "temp:*" | xargs -r redis-cli unlink
redis-cli --scan --pattern "test:*" | xargs -r redis-cli unlink
redis-cli --scan --pattern "debug:*" | xargs -r redis-cli unlink

sleep 2

# 第二级:清理过期但未删除的key
echo "2. 清理过期key..."
redis-cli --scan --pattern "*" | while read key; do
    ttl=$(redis-cli ttl "$key")
    if [ "$ttl" -eq -2 ]; then
        # key已过期不存在,Redis会自动清理
        continue
    fi
done

sleep 2

# 第三级:清理长期未访问的key
echo "3. 清理闲置key(30天未访问)..."
redis-cli --scan --pattern "*" | while read key; do
    idle_time=$(redis-cli object idletime "$key" 2>/dev/null || echo 0)
    if [ "$idle_time" -gt 2592000 ]; then  # 30天
        # 检查key类型,避免删除重要数据
        key_type=$(redis-cli type "$key")
        if [ "$key_type" = "string" ] && [[ "$key" == cache:* ]]; then
            echo "删除闲置缓存: $key"
            redis-cli unlink "$key"
        fi
    fi
done

sleep 2

# 第四级:清理大key
echo "4. 清理大key(>1MB)..."
redis-cli --scan --pattern "*" | while read key; do
    memory=$(redis-cli memory usage "$key" 2>/dev/null || echo 0)
    if [ "$memory" -gt 1048576 ]; then
        echo "发现大key: $key ($memory 字节)"
        # 这里可以根据业务决定是否删除
    fi
done

echo "清理完成"
```

7.2 预防措施

```bash
#!/bin/bash
# redis_prevention.sh - 预防Key过多的措施

# 1. 设置内存淘汰策略
redis-cli config set maxmemory-policy allkeys-lru

# 2. 启用key过期事件通知
redis-cli config set notify-keyspace-events Ex

# 3. 设置合理的过期时间
# 对于不同类型的key设置不同过期时间

# 4. 使用Hash等数据结构合并小key
# 将多个相关key合并为一个Hash

# 5. 定期压缩和优化
redis-cli memory purge  # Redis 4.0+

# 6. 监控和告警设置
# 设置key数量阈值告警

echo "预防措施已应用"
```

8. 紧急情况处理

8.1 紧急清理脚本

```bash
#!/bin/bash
# emergency_cleanup.sh - 紧急清理

echo "!!! 紧急清理模式 !!!"
echo "当前Key数量: $(redis-cli dbsize)"
echo "当前内存使用: $(redis-cli info memory | grep used_memory_human)"

# 立即清理所有临时key
echo "清理所有临时key..."
redis-cli --scan --pattern "temp:*" | xargs -r redis-cli unlink

# 清理所有测试key
echo "清理所有测试key..."
redis-cli --scan --pattern "test:*" | xargs -r redis-cli unlink

# 清理所有缓存key(谨慎操作)
echo "清理所有缓存key..."
redis-cli --scan --pattern "cache:*" | while read key; do
    # 只清理特定模式的缓存
    if [[ "$key" =~ ^cache:(temp|session|token): ]]; then
        redis-cli unlink "$key"
    fi
done

# 强制内存回收
echo "强制内存回收..."
redis-cli memory purge 2>/dev/null || true

# 重启AOF重写(如果有AOF)
echo "触发AOF重写..."
redis-cli bgrewriteaof 2>/dev/null || true

echo "紧急清理完成"
echo "清理后Key数量: $(redis-cli dbsize)"
```

8.2 使用Redis的FLUSHDB(谨慎!)

```bash
# 仅在所有其他方法都无效时使用
# 警告:这会删除当前数据库的所有数据

# 1. 先备份
redis-cli save

# 2. 切换到非生产数据库进行测试
redis-cli select 9
redis-cli flushdb

# 3. 生产环境谨慎使用
# redis-cli flushdb async  # Redis 4.0+ 异步删除

# 4. 或者清空所有数据库
# redis-cli flushall
```

9. 维护计划示例

9.1 每日维护

```bash
#!/bin/bash
# daily_maintenance.sh

# 1. 清理过期session
redis-cli --scan --pattern "session:*" | while read key; do
    ttl=$(redis-cli ttl "$key")
    if [ "$ttl" -lt 0 ]; then
        redis-cli unlink "$key"
    fi
done

# 2. 清理24小时前的临时文件
redis-cli --scan --pattern "temp:*" | while read key; do
    idle=$(redis-cli object idletime "$key" 2>/dev/null || echo 0)
    if [ "$idle" -gt 86400 ]; then
        redis-cli unlink "$key"
    fi
done

# 3. 记录统计信息
echo "$(date),$(redis-cli dbsize)" >> /var/log/redis_daily_stats.csv
```

9.2 每周维护

```bash
#!/bin/bash
# weekly_maintenance.sh

# 1. 分析key分布
redis-cli --bigkeys

# 2. 深度清理长期未使用的key
for pattern in "cache:*" "log:*" "stat:*"; do
    redis-cli --scan --pattern "$pattern" | while read key; do
        idle=$(redis-cli object idletime "$key" 2>/dev/null || echo 0)
        if [ "$idle" -gt 604800 ]; then  # 7天
            redis-cli expire "$key" 3600  # 设置为1小时后过期
        fi
    done
done

# 3. 优化内存
redis-cli memory purge 2>/dev/null || true
```

10. 总结:清理检查清单

1. 分析阶段
   · 使用INFO keyspace查看key分布
   · 使用--bigkeys找出大key
   · 分析业务,识别可清理的key模式
2. 安全准备
   · 备份Redis数据(SAVE或BGSAVE)
   · 在非高峰时段执行
   · 先在小范围测试清理脚本
3. 执行清理
   · 使用SCAN而不是KEYS
   · 使用UNLINK而不是DEL(Redis 4.0+)
   · 分批处理,添加延迟
   · 监控Redis性能指标
4. 验证结果
   · 检查key数量变化
   · 检查内存使用变化
   · 监控应用是否正常
5. 建立预防机制
   · 设置合理的过期时间
   · 实现自动清理脚本
   · 建立监控告警
   · 定期审计和优化

记住:永远不要在生产环境直接使用KEYS *或FLUSHDB,除非你完全确定后果。始终采用渐进式、可监控的方式清理。

 

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐