监听 Redis key 过期事件无效的问题

import com.shgbitai.manmachineability.constance.RedisKeyPrefix;
import com.shgbitai.manmachineability.service.RedisService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.ReactiveRedisMessageListenerContainer;
import org.springframework.stereotype.Component;

import java.util.Optional;

@Component
@Slf4j
public class RedisKeyExpirationListenerConfig {

	@Resource
	private RedisService redisService;

	@Bean
	ReactiveRedisMessageListenerContainer keyExpirationListenerContainer(ReactiveRedisConnectionFactory connectionFactory, @Value("${spring.data.redis.database}") String databaseIndex) {
		ReactiveRedisMessageListenerContainer container = new ReactiveRedisMessageListenerContainer(connectionFactory);

		// 订阅过期事件。这里的"__keyevent@0__:expired"是监听默认数据库中Key过期事件的频道。
		// 如果你的应用使用的不是默认数据库,需要更改这里的数字。
		String expiredTopic = String.format("__keyevent@%s__:expired", 0);

		container.receive(ChannelTopic.of(expiredTopic))
				.doOnNext(message -> {
					// 此处处理接收到的过期Key。message.getMessage()包含了过期的Key。
					String expiredKey = message.getMessage();
					log.info("会话过期的key: " + expiredKey);
					//如果session聊天室会话时长过期,通知前端,会话关闭
					if(expiredKey.contains(RedisKeyPrefix.SESSION_LAST_ACTIVITY_KEY_PREFIX)){
						String sessionId = Optional.of(expiredKey)
								.filter(s -> s.contains(":"))
								.map(s -> s.substring(s.indexOf(":") + 1)).orElse("");
						log.info("会话关闭,过期的会话SessionId:{}",sessionId);
						redisService.setWebSocketSessionExpired(sessionId);
					}
				})
				.subscribe();

		return container;
	}
}

监听 Redis key 过期事件无效的问题可能有几个原因。以下是一些可能的原因和相应的解决方案:

  1. Redis 配置未启用过期事件通知
    默认情况下,Redis 可能没有启用键过期事件的通知。你需要在 Redis 的配置文件 redis.conf 中设置 notify-keyspace-events 选项,或者通过运行 CONFIG SET 命令动态设置。

    • 例如,如果你希望监听 key 的过期事件,你可以设置 Ex(E 代表 Keyevent 事件,x 代表过期事件):
      CONFIG SET notify-keyspace-events Ex
      
    • 或者在 redis.conf 中添加:
      notify-keyspace-events Ex
      
  2. 监听错误的数据库
    在你的代码中,String expiredTopic = String.format("__keyevent@%s__:expired", 0); 这一行硬编码了数据库索引为 0。如果你的应用使用的不是数据库 0,则需要修改这个索引以匹配你实际使用的数据库。你已经通过注解 @Value("${spring.data.redis.database}") String databaseIndex 获取了数据库索引,但是没有使用它来格式化 expiredTopic

    • 修改代码以使用正确的数据库索引:
      String expiredTopic = String.format("__keyevent@%s__:expired", databaseIndex);
      
  3. 确保 Redis 版本支持
    确保你使用的 Redis 版本支持键空间通知。虽然大多数现代版本都支持,但是如果你使用的是较旧的版本,可能会有问题。

  4. 日志配置
    确保你的日志配置正确,以便可以看到 log.info 输出的相关信息。如果日志级别设置过高,可能导致这些信息未被记录。

  5. 检查连接工厂配置
    确保 ReactiveRedisConnectionFactory 正确配置并且能够与 Redis 服务器建立连接。

  6. 测试和调试

    • 你可以尝试在 Redis 中手动发布消息到相应的频道,看看你的监听器是否能够捕捉到这些消息。
    • 使用 Redis CLI 或客户端库测试键过期通知是否正常工作。

调整上述设置后,你的应用应该能够正确监听和响应 Redis 键过期事件。如果问题依旧,请进一步检查网络配置及权限设置。

你提出的需求是: --- ### ✅ 需求目标: 1. **使用 Redis 缓存一个时间队列**(按时间顺序删除最先进入的数据)。 2. **每个 key 设置 2 小时的有效期**。 3. **监听 key 是否过期**(即是否失效)。 4. **key 过期后自动删除数据**。 5. **不使用定时任务** 实现自动删除。 --- ## ✅ 实现思路 为了实现这个需求,我们采用以下技术组合: - 使用 Redis 的 **ZSet(Sorted Set)** 存储时间队列,以时间戳为 `score`,实现 FIFO(先进先出)。 - 使用 Redis 的 `EXPIRE` 命令设置 key过期时间(2 小时)。 - 使用 Redis 的 **Keyspace Notifications(键空间通知)** 来监听 key过期事件。 - 当 key 过期时,Redis 会自动发送事件通知,我们可以在 Java监听并执行自定义逻辑。 --- ## ✅ 示例代码实现 --- ### 1. Redis 时间队列服务类 ```java import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ZSetOperations; import org.springframework.stereotype.Service; import java.util.Set; import java.util.concurrent.TimeUnit; @Service public class RedisTimeQueueService { private final RedisTemplate<String, String> redisTemplate; private final ZSetOperations<String, String> zSetOps; private static final String QUEUE_KEY = "time_queue"; private static final long TTL_SECONDS = 2 * 60 * 60; // 2小时 public RedisTimeQueueService(RedisTemplate<String, String> redisTemplate) { this.redisTemplate = redisTemplate; this.zSetOps = redisTemplate.opsForZSet(); } // 添加数据到队列,使用当前时间戳作为 score public void addToQueue(String data) { long timestamp = System.currentTimeMillis() / 1000L; zSetOps.add(QUEUE_KEY, data, timestamp); // 设置整个 key过期时间为 2 小时 redisTemplate.expire(QUEUE_KEY, TTL_SECONDS, TimeUnit.SECONDS); } // 删除最早的数据(FIFO) public String removeOldest() { Set<ZSetOperations.TypedTuple<String>> oldest = zSetOps.popMin(QUEUE_KEY, 1); if (oldest != null && !oldest.isEmpty()) { return oldest.iterator().next().getValue(); } return null; } // 获取当前队列中的所有数据 public Set<String> getAllData() { return zSetOps.range(QUEUE_KEY, 0, -1); } } ``` --- ### 2. Key 过期监听器类 ```java import org.springframework.data.redis.connection.Message; import org.springframework.data.redis.connection.MessageListener; import org.springframework.stereotype.Component; @Component public class RedisKeyExpirationListener implements MessageListener { private final RedisTimeQueueService queueService; public RedisKeyExpirationListener(RedisTimeQueueService queueService) { this.queueService = queueService; } @Override public void onMessage(Message message, byte[] pattern) { String expiredKey = new String(message.getBody()); if ("time_queue".equals(expiredKey)) { System.out.println("Key [" + expiredKey + "] 已过期,执行清理逻辑..."); // 可以在这里执行额外的清理逻辑或数据库更新 Set<String> remainingData = queueService.getAllData(); System.out.println("过期时队列中仍存在的数据: " + remainingData); } } } ``` --- ### 3. Redis 监听器容器配置 ```java import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.listener.RedisMessageListenerContainer; import org.springframework.data.redis.listener.patternmatch.PatternTopic; @Configuration public class RedisListenerConfig { @Bean public RedisMessageListenerContainer redisMessageListenerContainer( RedisConnectionFactory connectionFactory, RedisKeyExpirationListener expirationListener) { RedisMessageListenerContainer container = new RedisMessageListenerContainer(); container.setConnectionFactory(connectionFactory); // 监听所有数据库key 过期事件 container.addMessageListener(expirationListener, new PatternTopic("__keyevent@*:expired")); return container; } } ``` --- ### 4. 控制器类(用于测试) ```java import org.springframework.web.bind.annotation.*; import java.util.Set; @RestController @RequestMapping("/queue") public class QueueController { private final RedisTimeQueueService queueService; public QueueController(RedisTimeQueueService queueService) { this.queueService = queueService; } @PostMapping("/add") public String addToQueue(@RequestBody String data) { queueService.addToQueue(data); return "Added to queue: " + data; } @GetMapping("/remove") public String removeOldest() { String removed = queueService.removeOldest(); return removed == null ? "Queue is empty" : "Removed: " + removed; } @GetMapping("/all") public Set<String> getAllData() { return queueService.getAllData(); } } ``` --- ## ✅ Redis 自动删除机制说明 - Redis 提供了两种方式设置 key过期时间: - `EXPIRE key seconds`:以秒为单位设置过期时间。 - `PEXPIRE key milliseconds`:以毫秒为单位设置过期时间。 - Redis 采用 **惰性删除 + 定期扫描** 的方式来清理过期 key: - **惰性删除**:当访问一个 key 时,Redis 检查它是否已过期,如果过期则删除。 - **定期扫描**:Redis 每隔一段时间随机检查一部分 key,删除其中已过期key。 - 因此,**key 会在过期后“尽快”被删除**,但不是实时的。 --- ## ✅ Redis Keyspace Notifications(键空间通知) - Redis 提供了发布/订阅机制,可以监听 key事件(如新增、过期、删除等)。 - 要启用 key 过期事件监听,需要在 `redis.conf` 中启用如下配置: ```properties notify-keyspace-events Ex ``` 其中: - `E` 表示事件类型前缀。 - `x` 表示 key 过期事件。 重启 Redis 后生效。 --- ## ✅ 代码说明总结 - 使用 `ZSet` 实现基于时间戳的排序和 FIFO 删除。 - 使用 Redis 的 `EXPIRE` 设置 key过期时间,实现自动删除。 - 使用 Redis 的 **Keyspace Notifications** 实现 key 过期事件监听,避免定时任务。 - key 在 2 小时后自动过期并被删除,无需手动干预。 --- ## ✅ 注意事项 - Rediskey 过期事件不是实时的,可能有一定延迟。 - 如果你需要更精确控制,可以结合 `ZScore` 判断时间戳,手动清理。 - 本方案适用于缓存、临时队列等对精确性要求不高的场景。 --- ##
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

MonkeyKing.sun

对你有帮助的话,可以打赏

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值