后端

Redis批量删除大量Key的实用方法与注意事项

TRAE AI 编程助手

引言

在 Redis 的日常运维中,批量删除大量 Key 是一个常见但又充满挑战的操作场景。无论是清理过期缓存、删除测试数据,还是处理业务变更导致的数据迁移,都可能涉及到对成千上万甚至数百万个 Key 的批量删除操作。如果处理不当,可能会导致 Redis 服务阻塞、性能下降,甚至影响线上业务的正常运行。

本文将深入探讨 Redis 批量删除大量 Key 的多种实用方法,分析各种方案的优缺点,并提供详细的注意事项和最佳实践,帮助你在不同场景下选择最合适的删除策略。

Redis 删除操作的基本原理

单个 Key 删除机制

Redis 的 DEL 命令是最基本的删除操作,它会立即删除指定的 Key 并释放内存。这个操作的时间复杂度取决于被删除 Key 的类型:

  • String 类型:O(1)
  • List/Set/Sorted Set 类型:O(N),N 为元素数量
  • Hash 类型:O(N),N 为字段数量
# 删除单个 Key
redis-cli DEL mykey
 
# 删除多个 Key
redis-cli DEL key1 key2 key3

删除操作的阻塞问题

当删除包含大量元素的集合类型 Key 时,DEL 命令会阻塞 Redis 主线程,导致其他客户端请求无法及时响应。例如,删除一个包含百万元素的 Set 可能需要几秒钟,期间 Redis 无法处理其他命令。

批量删除的常用方法

方法一:使用 KEYS + DEL 组合

这是最直观但也是最危险的方法:

# 删除所有以 "temp:" 开头的 Key
redis-cli --raw KEYS "temp:*" | xargs redis-cli DEL
 
# 使用 Redis 管道优化
redis-cli --raw KEYS "temp:*" | xargs redis-cli --pipe

优点

  • 实现简单,一行命令即可完成
  • 适合开发环境的快速清理

缺点

  • KEYS 命令会阻塞 Redis,在生产环境中极其危险
  • 对于大数据集,可能导致服务长时间不可用
  • 不支持增量删除

方法二:使用 SCAN + DEL 迭代删除

SCAN 命令提供了游标式的迭代,不会阻塞服务器:

import redis
 
def batch_delete_with_scan(redis_client, pattern, batch_size=100):
    """
    使用 SCAN 迭代删除匹配的 Key
    """
    cursor = 0
    total_deleted = 0
    
    while True:
        # SCAN 返回下一个游标和匹配的 Key 列表
        cursor, keys = redis_client.scan(
            cursor=cursor, 
            match=pattern, 
            count=batch_size
        )
        
        if keys:
            # 批量删除找到的 Key
            deleted = redis_client.delete(*keys)
            total_deleted += deleted
            print(f"Deleted {deleted} keys, total: {total_deleted}")
        
        # 游标为 0 表示迭代结束
        if cursor == 0:
            break
    
    return total_deleted
 
# 使用示例
client = redis.Redis(host='localhost', port=6379, decode_responses=True)
deleted_count = batch_delete_with_scan(client, "session:*", batch_size=500)
print(f"Total deleted: {deleted_count} keys")

优点

  • 不会阻塞 Redis 服务
  • 可以控制每批删除的数量
  • 支持模式匹配

缺点

  • 删除速度相对较慢
  • 可能会重复扫描某些 Key
  • 在删除过程中新增的匹配 Key 可能不会被删除

Redis 4.0 引入了 UNLINK 命令,它会在后台线程中异步删除 Key:

def batch_delete_with_unlink(redis_client, pattern, batch_size=100):
    """
    使用 SCAN + UNLINK 组合进行异步删除
    """
    cursor = 0
    total_deleted = 0
    pipeline = redis_client.pipeline()
    
    while True:
        cursor, keys = redis_client.scan(
            cursor=cursor,
            match=pattern,
            count=batch_size
        )
        
        if keys:
            # 使用 UNLINK 进行异步删除
            for key in keys:
                pipeline.unlink(key)
            
            # 批量执行
            results = pipeline.execute()
            deleted = sum(results)
            total_deleted += deleted
            print(f"Unlinked {deleted} keys, total: {total_deleted}")
        
        if cursor == 0:
            break
    
    return total_deleted

优点

  • 不会阻塞主线程
  • 适合删除大型数据结构
  • 删除操作立即返回

缺点

  • 需要 Redis 4.0 以上版本
  • 内存释放有延迟
  • 后台线程资源消耗

方法四:使用 Lua 脚本批量删除

Lua 脚本可以保证删除操作的原子性:

-- batch_delete.lua
local cursor = "0"
local pattern = ARGV[1]
local count = tonumber(ARGV[2]) or 100
local deleted = 0
 
repeat
    local result = redis.call("SCAN", cursor, "MATCH", pattern, "COUNT", count)
    cursor = result[1]
    local keys = result[2]
    
    if #keys > 0 then
        deleted = deleted + redis.call("DEL", unpack(keys))
    end
until cursor == "0"
 
return deleted
def batch_delete_with_lua(redis_client, pattern, batch_size=100):
    """
    使用 Lua 脚本进行批量删除
    """
    lua_script = """
    local cursor = "0"
    local pattern = ARGV[1]
    local count = tonumber(ARGV[2])
    local deleted = 0
    local batch_deleted = 0
    
    -- 限制单次执行的迭代次数,避免脚本执行时间过长
    for i = 1, 10 do
        local result = redis.call("SCAN", cursor, "MATCH", pattern, "COUNT", count)
        cursor = result[1]
        local keys = result[2]
        
        if #keys > 0 then
            batch_deleted = redis.call("DEL", unpack(keys))
            deleted = deleted + batch_deleted
        end
        
        if cursor == "0" then
            break
        end
    end
    
    return {deleted, cursor}
    """
    
    total_deleted = 0
    cursor = "0"
    
    while True:
        result = redis_client.eval(lua_script, 0, pattern, batch_size)
        deleted, cursor = result[0], result[1]
        total_deleted += deleted
        
        print(f"Deleted {deleted} keys, total: {total_deleted}")
        
        if cursor == "0":
            break
    
    return total_deleted

优点

  • 原子性操作
  • 减少网络往返
  • 可以实现复杂的删除逻辑

缺点

  • Lua 脚本执行期间会阻塞其他命令
  • 需要控制脚本执行时间
  • 调试相对困难

高级删除策略

基于 TTL 的渐进式删除

对于不需要立即删除的 Key,可以设置较短的 TTL,让 Redis 自动清理:

def progressive_delete_with_ttl(redis_client, pattern, ttl_seconds=60):
    """
    为匹配的 Key 设置短 TTL,实现渐进式删除
    """
    cursor = 0
    total_marked = 0
    
    while True:
        cursor, keys = redis_client.scan(
            cursor=cursor,
            match=pattern,
            count=100
        )
        
        if keys:
            pipeline = redis_client.pipeline()
            for key in keys:
                pipeline.expire(key, ttl_seconds)
            
            results = pipeline.execute()
            marked = sum(1 for r in results if r)
            total_marked += marked
            print(f"Marked {marked} keys for expiration, total: {total_marked}")
        
        if cursor == 0:
            break
    
    return total_marked

分片并行删除

对于超大规模的删除任务,可以使用多线程或多进程并行处理:

import concurrent.futures
import string
 
def delete_keys_by_prefix(redis_client, prefix):
    """
    删除特定前缀的 Key
    """
    pattern = f"{prefix}*"
    cursor = 0
    deleted = 0
    
    while True:
        cursor, keys = redis_client.scan(
            cursor=cursor,
            match=pattern,
            count=500
        )
        
        if keys:
            deleted += redis_client.delete(*keys)
        
        if cursor == 0:
            break
    
    return deleted
 
def parallel_batch_delete(redis_client, base_pattern):
    """
    并行删除,按字符分片
    """
    # 创建多个 Redis 连接
    clients = [redis.Redis(host='localhost', port=6379) for _ in range(4)]
    
    # 按首字符分片
    prefixes = [f"{base_pattern}{c}" for c in string.ascii_lowercase + string.digits]
    
    with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
        futures = []
        for i, prefix in enumerate(prefixes):
            client = clients[i % len(clients)]
            future = executor.submit(delete_keys_by_prefix, client, prefix)
            futures.append(future)
        
        total_deleted = 0
        for future in concurrent.futures.as_completed(futures):
            deleted = future.result()
            total_deleted += deleted
            print(f"Thread completed, deleted: {deleted}")
    
    return total_deleted

性能优化技巧

使用 Pipeline 减少网络开销

def optimized_batch_delete(redis_client, pattern, pipeline_size=1000):
    """
    使用 Pipeline 优化批量删除
    """
    cursor = 0
    total_deleted = 0
    
    while True:
        cursor, keys = redis_client.scan(
            cursor=cursor,
            match=pattern,
            count=pipeline_size
        )
        
        if keys:
            # 使用 Pipeline 批量执行删除
            pipeline = redis_client.pipeline(transaction=False)
            
            for key in keys:
                pipeline.delete(key)
            
            results = pipeline.execute()
            deleted = sum(results)
            total_deleted += deleted
            
            print(f"Deleted {deleted} keys in batch, total: {total_deleted}")
        
        if cursor == 0:
            break
    
    return total_deleted

监控删除进度

import time
import threading
 
class DeletionMonitor:
    def __init__(self, redis_client):
        self.redis_client = redis_client
        self.deleted_count = 0
        self.start_time = None
        self.is_running = False
        
    def start(self):
        self.start_time = time.time()
        self.is_running = True
        monitor_thread = threading.Thread(target=self._monitor)
        monitor_thread.daemon = True
        monitor_thread.start()
    
    def _monitor(self):
        while self.is_running:
            elapsed = time.time() - self.start_time
            rate = self.deleted_count / elapsed if elapsed > 0 else 0
            
            # 获取 Redis 信息
            info = self.redis_client.info('memory')
            used_memory = info['used_memory_human']
            
            print(f"Progress: {self.deleted_count} keys deleted | "
                  f"Rate: {rate:.2f} keys/sec | "
                  f"Memory: {used_memory}")
            
            time.sleep(5)
    
    def stop(self):
        self.is_running = False
        elapsed = time.time() - self.start_time
        print(f"\nDeletion completed: {self.deleted_count} keys in {elapsed:.2f} seconds")

注意事项与最佳实践

生产环境注意事项

1. 避免使用 KEYS 命令

在生产环境中,绝对不要使用 KEYS 命令进行批量查询。即使是包含少量 Key 的 Redis 实例,KEYS 命令也会造成明显的性能影响。

2. 控制删除速率

import time
 
def rate_limited_delete(redis_client, pattern, max_rate=1000):
    """
    限速删除,避免影响正常业务
    """
    cursor = 0
    total_deleted = 0
    batch_size = 100
    
    while True:
        start_time = time.time()
        
        cursor, keys = redis_client.scan(
            cursor=cursor,
            match=pattern,
            count=batch_size
        )
        
        if keys:
            deleted = redis_client.delete(*keys)
            total_deleted += deleted
            
            # 计算需要等待的时间以维持速率
            elapsed = time.time() - start_time
            expected_time = deleted / max_rate
            if expected_time > elapsed:
                time.sleep(expected_time - elapsed)
        
        if cursor == 0:
            break
    
    return total_deleted

3. 选择合适的时间窗口

批量删除操作应该在业务低峰期进行,可以通过监控 Redis 的 QPS 和响应时间来确定最佳时间窗口。

4. 做好数据备份

在执行大规模删除操作前,务必:

  • 使用 BGSAVE 创建 RDB 快照
  • 或确保 AOF 持久化已开启
  • 在测试环境先验证删除逻辑

内存管理注意事项

1. 内存碎片问题

大量删除操作可能导致内存碎片增加:

def check_memory_fragmentation(redis_client):
    """
    检查内存碎片率
    """
    info = redis_client.info('memory')
    frag_ratio = float(info['mem_fragmentation_ratio'])
    
    if frag_ratio > 1.5:
        print(f"Warning: High memory fragmentation ratio: {frag_ratio}")
        print("Consider running 'MEMORY PURGE' or restarting Redis")
    
    return frag_ratio

2. 使用 MEMORY PURGE

Redis 4.0+ 提供了 MEMORY PURGE 命令来清理内存碎片:

def cleanup_after_deletion(redis_client):
    """
    删除后的清理工作
    """
    try:
        # 清理内存碎片
        redis_client.execute_command('MEMORY', 'PURGE')
        print("Memory purge completed")
    except redis.ResponseError:
        print("MEMORY PURGE not supported in this Redis version")

错误处理与恢复

import logging
from typing import Set
 
class RobustBatchDeleter:
    def __init__(self, redis_client):
        self.redis_client = redis_client
        self.failed_keys: Set[str] = set()
        self.logger = logging.getLogger(__name__)
    
    def delete_with_retry(self, pattern, max_retries=3):
        """
        带重试机制的批量删除
        """
        cursor = 0
        total_deleted = 0
        
        while True:
            try:
                cursor, keys = self.redis_client.scan(
                    cursor=cursor,
                    match=pattern,
                    count=100
                )
                
                if keys:
                    for attempt in range(max_retries):
                        try:
                            deleted = self.redis_client.delete(*keys)
                            total_deleted += deleted
                            break
                        except redis.RedisError as e:
                            self.logger.warning(f"Deletion attempt {attempt + 1} failed: {e}")
                            if attempt == max_retries - 1:
                                self.failed_keys.update(keys)
                            else:
                                time.sleep(2 ** attempt)  # 指数退避
                
                if cursor == 0:
                    break
                    
            except redis.ConnectionError as e:
                self.logger.error(f"Connection error: {e}")
                # 等待并重新连接
                time.sleep(5)
                continue
        
        if self.failed_keys:
            self.logger.error(f"Failed to delete {len(self.failed_keys)} keys")
            self._save_failed_keys()
        
        return total_deleted
    
    def _save_failed_keys(self):
        """
        保存删除失败的 Key 列表
        """
        with open('failed_deletions.txt', 'w') as f:
            for key in self.failed_keys:
                f.write(f"{key}\n")

实战案例分析

案例一:清理过期会话数据

class SessionCleaner:
    def __init__(self, redis_client):
        self.redis_client = redis_client
    
    def clean_expired_sessions(self, session_prefix="session:"):
        """
        清理过期的会话数据
        """
        cursor = 0
        total_deleted = 0
        current_time = int(time.time())
        
        while True:
            cursor, keys = self.redis_client.scan(
                cursor=cursor,
                match=f"{session_prefix}*",
                count=100
            )
            
            if keys:
                pipeline = self.redis_client.pipeline()
                keys_to_delete = []
                
                for key in keys:
                    # 获取会话的过期时间
                    pipeline.hget(key, 'expire_time')
                
                expire_times = pipeline.execute()
                
                for key, expire_time in zip(keys, expire_times):
                    if expire_time and int(expire_time) < current_time:
                        keys_to_delete.append(key)
                
                if keys_to_delete:
                    deleted = self.redis_client.delete(*keys_to_delete)
                    total_deleted += deleted
                    print(f"Deleted {deleted} expired sessions")
            
            if cursor == 0:
                break
        
        return total_deleted

案例二:数据迁移后的清理

class DataMigrationCleaner:
    def __init__(self, redis_client):
        self.redis_client = redis_client
    
    def clean_migrated_data(self, old_prefix, new_prefix):
        """
        数据迁移后清理旧数据
        """
        cursor = 0
        total_deleted = 0
        verified_keys = []
        
        while True:
            cursor, keys = self.redis_client.scan(
                cursor=cursor,
                match=f"{old_prefix}*",
                count=50
            )
            
            if keys:
                pipeline = self.redis_client.pipeline()
                
                # 验证新数据是否存在
                for key in keys:
                    new_key = key.replace(old_prefix, new_prefix)
                    pipeline.exists(new_key)
                
                exists_results = pipeline.execute()
                
                # 只删除已成功迁移的数据
                for key, exists in zip(keys, exists_results):
                    if exists:
                        verified_keys.append(key)
                
                # 批量删除
                if len(verified_keys) >= 100:
                    deleted = self.redis_client.delete(*verified_keys)
                    total_deleted += deleted
                    print(f"Deleted {deleted} migrated keys")
                    verified_keys = []
            
            if cursor == 0:
                break
        
        # 删除剩余的 Key
        if verified_keys:
            deleted = self.redis_client.delete(*verified_keys)
            total_deleted += deleted
        
        return total_deleted

性能测试与基准

不同删除方法的性能对比

import time
import random
import string
 
def generate_test_data(redis_client, count=10000, prefix="test:"):
    """
    生成测试数据
    """
    pipeline = redis_client.pipeline()
    
    for i in range(count):
        key = f"{prefix}{''.join(random.choices(string.ascii_letters, k=10))}"
        value = ''.join(random.choices(string.ascii_letters, k=100))
        pipeline.set(key, value)
        
        if i % 1000 == 0:
            pipeline.execute()
            pipeline = redis_client.pipeline()
    
    pipeline.execute()
    print(f"Generated {count} test keys")
 
def benchmark_deletion_methods(redis_client):
    """
    基准测试不同的删除方法
    """
    results = {}
    
    # 测试 DEL 方法
    generate_test_data(redis_client, 10000, "del_test:")
    start = time.time()
    deleted = batch_delete_with_scan(redis_client, "del_test:*", 100)
    results['SCAN+DEL'] = {
        'time': time.time() - start,
        'deleted': deleted,
        'rate': deleted / (time.time() - start)
    }
    
    # 测试 UNLINK 方法
    generate_test_data(redis_client, 10000, "unlink_test:")
    start = time.time()
    deleted = batch_delete_with_unlink(redis_client, "unlink_test:*", 100)
    results['SCAN+UNLINK'] = {
        'time': time.time() - start,
        'deleted': deleted,
        'rate': deleted / (time.time() - start)
    }
    
    # 输出结果
    print("\nBenchmark Results:")
    print("-" * 50)
    for method, result in results.items():
        print(f"{method}:")
        print(f"  Time: {result['time']:.2f} seconds")
        print(f"  Deleted: {result['deleted']} keys")
        print(f"  Rate: {result['rate']:.2f} keys/second")
    
    return results

监控与告警

实时监控删除操作

import prometheus_client
from prometheus_client import Counter, Histogram, Gauge
 
# Prometheus 指标
deletion_counter = Counter('redis_keys_deleted_total', 'Total number of deleted keys')
deletion_duration = Histogram('redis_deletion_duration_seconds', 'Deletion operation duration')
current_progress = Gauge('redis_deletion_progress', 'Current deletion progress')
 
class MonitoredDeleter:
    def __init__(self, redis_client):
        self.redis_client = redis_client
    
    @deletion_duration.time()
    def monitored_delete(self, pattern):
        """
        带监控的删除操作
        """
        cursor = 0
        total_deleted = 0
        
        # 先统计总数
        total_keys = self._estimate_total_keys(pattern)
        current_progress.set(0)
        
        while True:
            cursor, keys = self.redis_client.scan(
                cursor=cursor,
                match=pattern,
                count=100
            )
            
            if keys:
                deleted = self.redis_client.delete(*keys)
                total_deleted += deleted
                deletion_counter.inc(deleted)
                
                # 更新进度
                if total_keys > 0:
                    progress = (total_deleted / total_keys) * 100
                    current_progress.set(progress)
            
            if cursor == 0:
                break
        
        current_progress.set(100)
        return total_deleted
    
    def _estimate_total_keys(self, pattern):
        """
        估算匹配的 Key 总数
        """
        # 使用采样估算
        sample_size = 0
        iterations = 0
        max_iterations = 10
        
        cursor = 0
        while iterations < max_iterations:
            cursor, keys = self.redis_client.scan(
                cursor=cursor,
                match=pattern,
                count=100
            )
            sample_size += len(keys)
            iterations += 1
            
            if cursor == 0:
                break
        
        # 基于采样估算总数
        if iterations > 0:
            db_size = self.redis_client.dbsize()
            estimated_total = (sample_size / iterations) * (db_size / 100)
            return int(estimated_total)
        
        return 0

总结

批量删除 Redis 中的大量 Key 是一个需要谨慎处理的操作。选择合适的删除策略需要考虑多个因素:

  1. 数据规模:少量 Key 可以直接使用 DEL,大量 Key 必须使用 SCAN 迭代
  2. 业务影响:生产环境优先使用 UNLINK 或设置 TTL 的方式
  3. Redis 版本:充分利用新版本的特性,如 UNLINK、MEMORY PURGE
  4. 性能要求:根据业务特点选择同步或异步删除
  5. 数据安全:做好备份和错误处理

在使用 TRAE IDE 开发 Redis 相关应用时,其智能代码补全和上下文理解功能可以帮助你快速实现这些批量删除策略。TRAE 的 AI 编程助手能够理解 Redis 命令的语义,自动生成优化的批量操作代码,并提供性能优化建议。通过 TRAE 的实时代码分析,你可以及时发现潜在的性能问题,确保删除操作的安全性和高效性。

记住,没有一种方法适用于所有场景。根据实际需求和环境特点,灵活组合使用这些方法,才能实现安全、高效的批量删除操作。在实施前充分测试,监控执行过程,做好应急预案,是保证生产环境稳定的关键。

(此内容由 AI 辅助生成,仅供参考)