Redis 版本:5.0 :
一:过期监听:
Spring Data Redis 封装了 Redis 的 Pub/Sub 功能,提供了对 key 过期事件的监听支持。
1. 核心类:KeyExpirationEventMessageListener
这个抽象类是 Spring 提供的,专门用于监听 Redis 的 key 过期事件。
代码中的 RedisKeyExpiredListener 继承自它
Redis 的 key 过期事件是一种发布/订阅(Pub/Sub)机制,当某个设置了过期时间的 key 被 Redis 删除时,Redis 会发布一个事件通知。Spring 通过监听这些事件,并将其封装为 Spring 事件模型中的对象,从而实现对 key 过期事件的捕获和处理
Redis 的定期删除策略是 默认每秒执行 10 次(即每 100 毫秒运行一次),这是由 Redis 配置中的 hz 参数控制的。
实际上:默认配置中,hz=10,表示每秒运行 10 次过期键清理任务。
每次运行时,Redis 会从数据库中随机选取一批设置了过期时间的键进行检查,并删除其中已过期的键。
这个过程不会扫描所有键,因此可能会有部分过期键在过期后不会立刻被删除,导致“过期键延迟删除”
可以在 redis.conf 文件中调整 hz 参数来改变 Redis 清理过期键的频率:
增大 hz 值(例如设为 20 或 30)可以提高清理频率,从而提升过期键的删除实时性,但也会增加 CPU 消耗。
减小 hz 值则节省 CPU 资源,但可能导致更多的过期键滞留内存。
实际操作也会遇到,手动更改了TTL方便更快过期,然后不刷新Redis Manager的情况下,尽管有定期删除策略,也没有收到那几个key的过期监听
首先来认知原理:
Redis 使用两种策略来处理设置了 TTL 的键(即有过期时间的键):
删除策略 | 触发方式 | 是否会触发 expired 事件 |
惰性删除(Lazy Expiration) | key 被访问时发现已过期但未被删除(惰性删除) | ✅ 会触发 expired 事件 |
定期删除(Active Expiration) | Redis 每隔一段时间扫描并随机删除部分过期键 | ❌ 不会触发 expired 事件 |
public class RedisKeyExpiredListener extends KeyExpirationEventMessageListener
这个类继承自 Spring Data Redis 提供的 KeyExpirationEventMessageListener。
它监听的是 Redis 的 __keyevent@*:expired 通道。
只有当 Redis 通过惰性删除触发过期事件时,才会向这个通道发布消息,从而触发你的 onMessage() 方法。
如果一个 key 设置了过期时间(TTL),不主动访问它,也没有在过期后被 Redis 的惰性删除机制触发,那么你确实 可能永远收不到 expired 过期通知。
疑问?
如果 Redis 通过定期删除策略已经删除了一个设置了 TTL 的 key,此时再去手动 GET 这个 key,还会触发 Redis 的 expired 过期事件吗?
答:不会触发过期事件监听器(如 RedisKeyExpiredListener)
只有在访问一个 key 时,发现它已经过期但尚未被删除的情况下,才会真正执行删除操作,并触发 expired 事件。
但如果这个 key 已经被 Redis 的定期删除机制提前删掉了:
它已经不存在于 Redis 中;此时你再访问它(如 GET my_key),会直接返回 nil;
不会再次触发 Redis 的过期检查和事件通知。
二: 那么问题来了:什么是“查询”key?什么又是“访问”key?
2.1这是关键区分点 👇
操作 | 是否算作“访问” |
GET key | ✅ 是 |
EXISTS key | ✅ 是 |
TTL key | ✅ 是 |
DEL key | ✅ 是 |
Redis 内部线程检查 key 是否存在 | ✅ 是 |
Redis 定期删除时只是遍历并比较过期时间 | ❌ 不是 |
所以就是代码中有对Redis进行不定时的设置复合键的key时,就会访问key,这样不用直接去手动get 此key,一样是访问到了
key格式复合键:TEST_KEY:
redisTemplate.opsForValue().set(TEST_KEY+ relatedId , null ,60, TimeUnit.SECONDS);
使用这类代码,就是会导致,如果有进行定期set,就会访问key,从而触发一些过期key的过期监听收到消息,
如果不进行设置,则一些已经过期的就有可能被定时删除而收不到消息,导致消息丢失,数据不准确的问题
另外: Redis 定期删除到底做了什么?
Redis 使用了一个叫做 activeExpireCycle() 的函数来执行定期删除。
它的大致流程如下:
void activeExpireCycle(...) {
// 获取当前数据库
for (j = 0; j < dbs_per_call; j++) {
for (k = 0; k < keys_per_call; k++) {
// 随机选取一个设置了 TTL 的 key
if (currentExpiresTime < now()) {
// 直接删除,不进行任何访问操作
dbDelete(db, key);
}
}
}
}
关键点:
Redis 只检查 key 的过期时间;
没有真正访问这个 key 的内容(比如读取、修改等);
所以 不会触发惰性删除逻辑;自然也就 不会发布 expired 事件。
2.2 除了上面说的情况,某些 key 即使没手动 get,也被监听到过期事件?
模块 | 行为 | 是否可能访问 key |
Lua 脚本 | EVAL 中使用 key | ✅ 是 |
集群迁移 | MIGRATE 命令 | ✅ 是 |
持久化 | RDB 快照生成时 | ❌ 否 |
主从同步 | 复制 key 到从节点 | ❌ 否 |
慢日志 | 记录慢命令 | ❌ 否 |
其他业务模块 | 如缓存统计、监控工具 | ✅ 是 |
所以你虽然没有显式调用 get(key),但这些模块可能会在你不察觉的情况下访问 key,从而触发惰性删除。
三:Redis 淘汰策略中访问到这个 key
场景 | 是否触发 expired 事件 |
Redis 定期删除直接删掉了这个 key | ❌ 否 |
Redis 主从同步时访问了这个 key | ✅ 是 |
Redis 集群迁移该 key 到新节点 | ✅ 是 |
Redis 慢日志记录了访问该 key 的命令 | ✅ 是(说明它已经被访问) |
Redis 监控工具周期性检查 key | ✅ 是 |
Redis 淘汰策略中访问到这个 key | ✅ 是 |
所以,你的 key 是如何被访问到的?
机制 | 是否可能访问 key |
Redisson 的 RLock、RLock.tryLock() | ✅ 是 |
Spring Cacheable 缓存组件 | ✅ 是 |
Redis 的 SCAN 命令 | ✅ 是 |
Redis 的 KEYS * 命令 | ✅ 是 |
Redis 的 EXPIRE 命令 | ✅ 是 |
Redis 的 DEL 命令 | ✅ 是 |
这些命令虽然你不写,但 Redis 内部模块或框架库可能会使用它们。
四:使用 Redisson 的延迟队列(推荐)
RDelayedQueue<String> queue = redisson.getDelayedQueue(redisson.getQueue("my_queue"));
queue.offer("data", 60, TimeUnit.SECONDS);
// 注册监听器
queue.registerListener(item -> {
log.info("收到延迟消息: {}", item);
});
这种方式比依赖 Redis 的 expired 事件更加稳定可靠。
Redis 内部模块和后台任务 可能 会访问你的 key,但这不是一定会发生的行为。
所以你看到的 key 过期事件,是 “可能触发”的结果,而不是“一定会触发”的机制。
如果你需要确保 key 过期后触发回调,请使用 Redisson 的延迟队列 或 Kafka + 定时轮询 等更可靠的机制。