在 Spring Boot 中监听 Redis Key 过期事件

在 Redis 数据库中,可以通过 EXPIREEXPIREATEXPIRETIME 命令来设置 Key 的有效时间,当一个 Key 过期后会自动从数据库中删除,释放空间。

得益于于这个特性,我们可以很轻松地实现诸多类似于 “Session” 管理、数据缓存等功能。它们都有一个共同点就是,数据不会永久保存!

在有些场景中,我们可能希望在某些 Key 过期的时候获取到通知,进行一些业务处理。或者是干脆用于 “定时通知/任务” 功能,例如:下单 30 分钟后未支付,则取消订单。那么可以在用户下单的时候使用订单号作为 key 设置到 Redis 数据库中,并且设置过期时间为 30 分钟。当超时后,我们可以在 “key 过期通知” 中获取到 key 也就是订单号,判断用户是否已经支付从而是否取消订单。

注意: Redis 的 Key 过期通知功能本质上是通过 发布/订阅 功能实现的。也就是说,它不能保证通知消息的交付,当 Key 过期时如果服务器停机、重启中则该通知消息会永久丢失。

本文将会带你学习如何在 Spring Boot 应用中使用 Spring Data Redis 监听 Redis Key 过期事件。

本文所使用的软件版本:

  • Spring Boot:3.1.3
  • Redis:7.0.5

整合 Spring Data Redis

得益于 Spring Boot 对 Redis 开箱即用的支持,只需要 2 步,就可以快速地在 Spring Boot 中整合、使用 Redis。

你可以通过 start.springboot.io 快速创建示例应用。

添加 spring-boot-starter-data-redis 依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

配置 application.yaml

spring:
  data:
    redis:
      database: 0
      host: localhost
      port: 6379
      password:

在配置文件中指定 redis 服务的主机、端口、密码(如果有)以及要使用的数据库。

更详细的配置信息请参考 Spring Boot 中文文档

监听 Redis 过期 Key 通知

配置 RedisMessageListenerContainer

cn.springdoc.demo.config 包下创建 RedisConfiguration,用于定义 RedisMessageListenerContainer Bean。

package cn.springdoc.demo.config;

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;

@Configuration
public class RedisConfiguration {

    @Bean
    public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory factory) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        
        container.setConnectionFactory(factory);
        
    //  container.setTaskExecutor(null);            // 设置用于执行监听器方法的 Executor
    //  container.setErrorHandler(null);            // 设置监听器方法执行过程中出现异常的处理器
    //  container.addMessageListener(null, null);   // 手动设置监听器 & 监听的 topic 表达式
        return container;
    }
}

如上所述,Redis 过期 Key 通知本质上是使用 Redis 发布订阅(Pub/Sub) 实现,所以必须先在 Application Context 中定义 RedisMessageListenerContainer bean。

其唯一必须的设置项是 RedisConnectionFactory,得益于自动配置,factory 可以直接从方法注入。其他设置项非必须,你可以从 Java Doc 了解更多。

继承 KeyExpirationEventMessageListener

Spring Data Redis 专门提供了一个监听 Redis Key 过期事件的监听器:KeyExpirationEventMessageListener

我们只需要自定义监听器类,继承它,并且覆写 doHandleMessage(Message message) 方法即可。

cn.springdoc.demo.listener 包下创建 KeyExpireListener 类,如下:

package cn.springdoc.demo.listener;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.listener.KeyExpirationEventMessageListener;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.stereotype.Component;

@Component
public class KeyExpireListener extends KeyExpirationEventMessageListener {

    final static Logger logger = LoggerFactory.getLogger(KeyExpireListener.class);

    // 通过构造函数注入 RedisMessageListenerContainer 给 KeyExpirationEventMessageListener
    public KeyExpireListener(RedisMessageListenerContainer listenerContainer) {
        super(listenerContainer);
    }

    @Override
    public void doHandleMessage(Message message) {
        
        // 过期的 key
        byte[] body = message.getBody();
        
        // 消息通道
        byte[] channel = message.getChannel();
        
        logger.info("message = {}, channel = {}", new String(body), new String(channel));
    }
}

doHandleMessage 就是处理 Redis Key 过期通知事件的方法。其中 Message 表示通知消息,只有 2 属性,分别表示消息正文(在这里就是过期的 Key 名称)以及来自于哪个 channel。如下:

public interface Message extends Serializable {
    byte[] getBody();
    byte[] getChannel();
}

注意: 在 Redis Key 过期事件中,只能获取到已过期的 Key 的名称,不能获取到值。

测试

首先启动 Spring Boot 应用,确定程序正常启动。然后在 Redis 客户端执行如下命令:

SET hello "springdoc.cn" EX 10

上述命令,往 Redis 服务器设置了一个 Key 名称为 hello、值为 springdoc.cn 的 string 数据,并且通过 EX 参数指定它过期时间为 10 秒。

然后,观察程序控制台,耐心等待 10 秒后,我们会看到如下日志信息:

... [enerContainer-1] c.s.demo.listener.KeyExpireListener      : message = hello, channel = __keyevent@0__:expired

如你所见,我们成功监听到了 hello 这个 key 的过期事件。