不能用 keys * 扫描过期 key,因其会阻塞主线程、引发雪崩;应改用 scan + ttl 分两步判定并清理,同时通过命名空间或正则排除敏感 key,并需与业务方对齐“过期”语义。

为什么不能用 KEYS * 扫描过期 Key
直接调用 KEYS * 在生产 Redis 里是危险操作:它会阻塞主线程,数据量大时可能引发超时甚至服务雪崩。Redis 官方明确建议禁用该命令。真正安全的方案必须基于增量式、非阻塞、可控速率的遍历机制。
-
SCAN是唯一合规的替代方案,它以游标方式分批返回 key,不阻塞服务器 - 但
SCAN本身不区分是否过期——Redis 不保证已过期但尚未被惰性删除或定期删除的 key 一定出现在结果中 - 因此“清理过期 key”实际要分两步:先用
SCAN获取候选 key,再逐个用TTL判定是否真正过期
如何用 SCAN + TTL 实现可控扫描
核心逻辑是循环调用 SCAN,每次取一批 key(比如 100 个),对每个 key 执行 TTL,只删除 TTL 返回 -2(key 不存在)或 -1(无过期时间)不算过期;只有 TTL 返回值 且不是 <code>-2 的情况极少,真正过期的判定依据是 TTL 返回 -1 表示永不过期,-2 表示 key 已被删除,而 0 或正数表示剩余秒数——所以真正过期的是 TTL 返回 -1?不对,TTL 返回 -1 是永不过期,-2 是 key 不存在,**只有 TTL 返回 0 或负数且非 -2 才说明已过期**?其实更准确的是:TTL 返回 -2 → key 不存在;返回 -1 → 无过期时间;返回 ≥ 0 → 剩余秒数;所以“已过期”在 Redis 语义中表现为:key 存在但 TTL 返回 0 或负值?不,TTL 永远不会返回负值(除了 -1/-2),它返回 0 表示“刚好过期”,此时 key 仍存在但下次访问会被清除。因此稳妥做法是:对每个 scan 到的 key 调用 EXISTS + TTL,仅当 EXISTS 为 true 且 TTL ≤ 0 时才认为可清理(注意:TTL=0 是临界过期状态,应删)。
- 使用
redis.Client.Scan()(go-redis/v9)比裸发SCAN命令更安全,自动处理游标和类型转换 - 务必设置
count参数(如 50–200),避免单次 SCAN 返回过多 key 导致内存/延迟尖峰 - 每次迭代后加
time.Sleep()(如 10–50ms),给 Redis 留出响应余量,防止扫描压垮服务 - 用
context.WithTimeout()包裹单次 SCAN 和 TTL 批处理,防止单轮卡死
Go 代码里怎么避免并发误删和性能抖动
如果多个实例同时跑这个扫描函数,可能重复删同一个 key,虽无害但浪费资源;更严重的是,密集调用 DEL 可能打满 Redis 连接或触发慢日志。必须加控制。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 用 Redis 分布式锁(如
SET key value NX PX 30000)确保同一时刻只有一个扫描进程运行 - 不要对每个过期 key 单独发
DEL,改用redis.Pipeline()批量提交,每批最多 10–50 个 key - 遇到
redis.Nil错误(key 已被其他进程删掉)直接跳过,不报错 - 记录扫描进度(当前游标)到本地文件或 Redis 自身,崩溃重启后能续扫,避免全量重来
// 示例片段:批量检查并删除
keys := make([]string, 0, 50)
for _, key := range scannedKeys {
ttl, err := rdb.TTL(ctx, key).Result()
if err != nil || ttl > 0 {
continue
}
if ttl == 0 || ttl 0 {
rdb.Del(ctx, keys...)
}
哪些 Key 绝对不该被这个函数碰
扫描函数默认面向全局 namespace,但实际项目里总有例外:比如用作分布式锁的 key、带特殊前缀的配置 key、或依赖长期存活的 session key。硬编码排除列表容易漏,推荐用声明式规则。
- 通过
regexp.MustCompile("^lock:.*|^config:.*")预过滤 key 名,匹配即跳过 - 给需保护的 key 设置一个特殊 field(如
__protected: true),用HGET判断——但会增加额外请求,慎用 - 最稳妥的是约定命名空间:所有缓存 key 加统一前缀(如
cache:user:),扫描只作用于该前缀,其余一概忽略 - 上线前务必用
MONITOR观察真实流量中的 key pattern,确认排除规则覆盖了所有敏感 key
真正的难点不在代码怎么写,而在确定“哪些算过期”——业务语义里的“过期”和 Redis 底层的“过期”常有偏差。比如某个 key 设了 1 小时 TTL,但业务要求 10 分钟未更新就必须清,这时得结合 GET + 时间戳字段判断,而不是只信 TTL。这个边界,得和业务方当面敲定。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










