Redis过期键不立即释放内存,因采用惰性删除+定期抽样策略,低访问量下易积压“僵尸键”,故需脚本主动扫描(SCAN)、判断TTL==-2、分批DEL清理,辅以节流与锁机制保障安全。

Redis 的过期键是自动清理的,但为什么还要写脚本?
因为 EXPIRE 或 SET ... EX 3600 设置的过期时间只是“标记”,Redis 采用惰性删除 + 定期抽样删除策略,并不保证过期键立刻释放内存。尤其在低访问量场景下,大量过期键可能长期滞留,导致 INFO memory 中 mem_used_human 持续偏高,而 dbX.expires 数值远大于实际活跃 key 数——这是典型的“僵尸过期键”现象。
脚本不是替代 Redis 自身机制,而是补位:主动扫描、强制驱逐、记录异常 key。
- 只对明确标记为缓存且带过期时间的 key 模式操作(如
cache:user:*),避免误删 session 或锁类数据 - 用
SCAN替代KEYS,防止阻塞主线程 - 每次扫描限制数量(如
COUNT 100),并加短 sleep,降低 CPU 和网络抖动 - 对返回的每个 key 调用
TTL,仅当TTL == -2(已过期)或TTL == -1(无过期)且业务语义上应淘汰时才DEL
用 redis-py 写一个安全可控的扫描清理脚本
核心逻辑是分批 SCAN → 过滤模式 → TTL 判断 → 条件删除。注意不要用 redis.Redis().keys("cache:*"),那在生产环境等于自毁。
import redis
import time
import sys
<p>r = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
cursor = 0
pattern = "cache:*"
cleaned = 0</p><p>while True:
cursor, keys = r.scan(cursor=cursor, match=pattern, count=50)
for key in keys:
ttl = r.ttl(key)</p><h1>TTL == -2 表示 key 已过期(Redis 内部已标记,但未清理)</h1><pre class="brush:php;toolbar:false;"> # TTL == -1 表示永不过期,但若业务定义该前缀下所有 key 都应有时效,可额外规则判断
if ttl == -2:
try:
r.delete(key)
cleaned += 1
except redis.ConnectionError:
print("Redis connection lost")
sys.exit(1)
if cursor == 0:
break
time.sleep(0.01) # 防止扫描过快打满带宽print(f"Cleaned {cleaned} expired cache keys")
这个脚本默认只处理 TTL == -2 的 key,安全边界清晰;count=50 和 time.sleep(0.01) 是关键节流点,线上压测表明单次 scan >100 或无 sleep 容易触发 Redis 的 slowlog 告警。
立即学习“Python免费学习笔记(深入)”;
让 macOS 自动化像说话一样简单 —— 用代码控制你的 Mac,无需学习 AppleScript 系统要求 macOS 14+ (Sonoma) Python 3.9+ 需要开启:辅助功能 + 屏幕录制权限
如何让脚本真正“定时”运行而不依赖 crontab?
crontab 启动 Python 脚本有概率重叠(比如上一次没结束,下一次又启动),导致重复删除或连接冲突。更稳的方式是用 APScheduler 做进程内调度,并加文件锁防多实例。
- 安装:
pip install apscheduler - 用
BlockingScheduler+interval触发器,比 crontab 更精确控制间隔(支持秒级) - 加
os.open(... os.O_CREAT | os.O_EXCL)尝试创建锁文件,失败则直接退出,不抢锁、不等待 - 任务函数里捕获
redis.ConnectionError和redis.TimeoutError,避免调度器崩溃
如果你的部署环境不允许额外依赖,那就用 crontab,但务必加上 flock -n /tmp/redis-clean.lock -c "python clean.py" —— -n 是非阻塞锁,失败直接跳过,比 pidfile 更可靠。
清理后发现内存没降?先查这三件事
执行完脚本,redis-cli INFO memory | grep -E "(used|peak)" 如果没变化,别急着改脚本,先确认:
-
redis-cli --stat看实时内存波动,有些释放会延迟几秒才反映在used_memory -
redis-cli MEMORY USAGE some_expired_key验证目标 key 是否真被删了(返回(integer) 0或报错ERR no such key) - 检查是否启用了
maxmemory和maxmemory-policy,比如设了allkeys-lru,Redis 可能正在后台异步淘汰,和你的脚本“打架”
最常被忽略的是:脚本删的是主库 key,但业务连的是读副本(replica),而 replica 默认不执行 DEL 命令的本地清理——它只等主从同步后自动失效。这种情况下,必须在 replica 上也跑一遍脚本,或改用 redis.Redis(..., decode_responses=True) 连接 replica 并显式调用 client.slaveof_no_one()(不推荐)或直接切到主节点操作。

















