SpringBoot监听Redis Key过期事件的几种方式

 更新时间:2026年03月12日 10:17:44   作者:彭于晏Yan  
本文介绍了使用Redis监听键过期来实现电商平台优惠券自动失效的解决方案,包括配置Redis、实现监听器、使用线程池优化以及进一步优化措施,需要的朋友可以参考下

1. 示例场景

电商平台常会发放「限时优惠券」(比如 “满 100 减 20,30 分钟内有效”“24 小时专属券”),核心诉求是:

  1. 优惠券一旦超过有效期,必须自动标记为 “已失效”,用户无法再使用;
  2. 避免用户在优惠券过期后仍尝试用券下单,导致订单支付异常;
  3. 无需定时任务轮询(比如每分钟扫全量表),减少数据库压力;
  4. 精准触发失效逻辑(优惠券到期瞬间就处理,而非定时任务的 “延迟生效”)。

如果用传统定时任务方案,会面临两个核心问题:

  • 优惠券发放时间分散(用户随时领券),无法精准设定轮询规则(比如 30 分钟有效期的券,有的用户 10:01 领,有的 10:05 领,定时任务间隔设短了耗性能,设长了失效不及时);
  • 全量表轮询会频繁查询数据库,高并发下(比如大促发百万张券)数据库扛不住。

此时,用「Redis 监听键过期」的方案就能完美解决,既精准又低耗。

2. 配置 Redis 开启key 过期通知

  • Redis 默认关闭键空间通知,需要先开启并指定监听的事件类型。

方式一:临时配置(重启 Redis 失效)

# 开启过期事件监听,也可按需指定(如只监听过期+驱逐:KxKe)
127.0.0.1:6379> config set notify-keyspace-events Ex

方式 二:永久配置

  • 修改Redis配置文件redis.conf,查看 notify-keyspace-events 配置项,修改为notify-keyspace-events Ex,并重启Redis生效,相关参数说明如下:
K:keyspace 事件,事件以 keyspace@ 为前缀进行发布
E:keyevent 事件,事件以 keyevent@ 为前缀进行发布
g:一般性的,非特定类型的命令,比如del,expire,rename等
$:字符串特定命令
l:列表特定命令
s:集合特定命令
h:哈希特定命令
z:有序集合特定命令
x:过期事件,当某个键过期并删除时会产生该事件
e:驱逐事件,当某个键键因Redis内存达到maxmemory上限,被淘汰策略删除时触发
A:g$lshzxe的别名,因此”AKE”意味着所有事件

3. 添加pom文件

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

4. RedisListenerConfig配置类

  • 实现监听 Redis key过期时间
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 RedisListenerConfig {
    @Bean
    RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        return container;
    }
}

5. RedisKeyExpirationListener监听类

  • 实现KeyExpirationEventMessageListener接口,查看源码发现,该接口监听所有db的过期事件 keyevent@*:expired"
  • 键过期事件并非“实时触发”:Redis 采用 “惰性删除 + 定期删除” 策略,过期键可能延迟触发事件。
/**
 * 监听所有db的过期事件__keyevent@*__:expired"
 */
@Component
@Slf4j
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {

    public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
        super(listenerContainer);
    }

    /**
     * 针对 redis 数据失效事件,进行数据处理
     * @param message
     * @param pattern
     */
    @Override
    public void onMessage(Message message, byte[] pattern) {
        // 获取到失效的 key,进行取消订单业务处理
        String expiredKey = message.toString();
        log.info("失效的Key:{}", expiredKey);
    }
}

6. 使用线程池优化

  • 先定义一个专门处理过期事件业务的线程池,避免使用默认线程池导致资源竞争:
/**
 * 过期事件异步处理线程池配置
 */
@Configuration
@EnableAsync // 开启异步注解支持
public class RedisEventExecutorConfig {

    /**
     * 自定义线程池,处理过期事件的业务逻辑
     */
    @Bean("redisExpireEventExecutor")
    public Executor redisExpireEventExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 核心线程数(根据业务压力调整)
        executor.setCorePoolSize(5);
        // 最大线程数
        executor.setMaxPoolSize(20);
        // 队列容量(缓冲待处理的任务)
        executor.setQueueCapacity(1000);
        // 线程名前缀(便于日志排查)
        executor.setThreadNamePrefix("redis-expire-event-");
        // 拒绝策略:队列满+线程数到最大时,由提交任务的线程执行(避免任务丢失)
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        // 线程空闲超时时间
        executor.setKeepAliveSeconds(60);
        // 初始化线程池
        executor.initialize();
        return executor;
    }
}

// 在监听类中调用
@Component
public class RedisExpireBusinessService {
    
    @Async("redisExpireEventExecutor")
    public void handleExpiredKeyAsync(String expiredKey) {
        try {
            System.out.println("异步处理过期key:" + expiredKey + ",线程:" + Thread.currentThread().getName());
        } catch (Exception e) {
            System.err.println("处理过期key失败:" + expiredKey + ",异常:" + e.getMessage());
        }
    }
}

7. 进一步优化

如果过期事件量极大(比如每秒上万),仅靠线程池可能不够,可叠加以下方案:

7.1. 引入消息队列(如 RocketMQ/Kafka)

  • 监听器只做 “生产消息”:将过期 key 发送到消息队列;
  • 单独的消费者服务处理业务逻辑:解耦Redis监听和业务处理,支持削峰填谷。

7.2. 优化 Redis 监听容器线程池

RedisMessageListenerContainer 自身也有监听线程池,可调整参数避免监听线程不够用:

@Bean
public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) {
    RedisMessageListenerContainer container = new RedisMessageListenerContainer();
    container.setConnectionFactory(connectionFactory);
    // 设置监听线程池的核心参数(默认线程数可能不足)
    container.setTaskExecutor(new ThreadPoolTaskExecutor() {{
        setCorePoolSize(3); // 监听线程数,根据事件量调整
        setMaxPoolSize(5);
        setThreadNamePrefix("redis-listener-");
        initialize();
    }});
    return container;
}

以上就是SpringBoot监听Redis Key过期事件的几种方式的详细内容,更多关于SpringBoot监听Redis Key过期事件的资料请关注脚本之家其它相关文章!

相关文章

  • 详解Spring+Hiernate整合

    详解Spring+Hiernate整合

    这篇文章主要介绍了详解Spring+Hiernate整合,spring整合hibernate主要介绍以xml方式实现,有兴趣的可以了解一下。
    2017-04-04
  • Java中操作超大数的方法

    Java中操作超大数的方法

    本篇文章是小编在网上整理的关于java操作超大数的方法以及解决思路,有兴趣的朋友参考学习下。
    2018-06-06
  • Struts2实现上传单个文件功能

    Struts2实现上传单个文件功能

    这篇文章主要为大家详细介绍了Struts2实现上传单个文件功能,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-06-06
  • Java利用Hutool-Script封装JS脚本执行

    Java利用Hutool-Script封装JS脚本执行

    在 Java 开发中,有时需要动态执行脚本代码,比如 JavaScript 脚本,来实现一些灵活的业务逻辑,下面我们就来看看如何利用Hutool-Script模块对Java的脚本执行功能进行封装吧
    2025-02-02
  • Java实现单向链表的基本功能详解

    Java实现单向链表的基本功能详解

    这篇文章主要给大家介绍了关于Java实现单向链表基本功能的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧。
    2018-03-03
  • 一文详解Java中的异常机制

    一文详解Java中的异常机制

    Java的异常处理机制可以让程序具有极好的容错性,让程序更加健壮,这篇文章主要介绍了Java中异常机制的相关资料,包括Error和Exception的分类、特点以及异常处理的最佳实践,需要的朋友可以参考下
    2025-05-05
  • mybatis参数String与Integer类型的判断方式

    mybatis参数String与Integer类型的判断方式

    这篇文章主要介绍了mybatis参数String与Integer类型的判断方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-03-03
  • java多线程应用实现方法

    java多线程应用实现方法

    以前没有写笔记的习惯,现在慢慢的发现及时总结是多么的重要了,呵呵。虽然才大二,但是也快要毕业了,要加油
    2012-11-11
  • Java中Lombok @Value注解导致的variable not been initialized问题

    Java中Lombok @Value注解导致的variable not been initialized问题

    本文主要介绍了Java中Lombok @Value注解导致的variable not been initialized问题,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-07-07
  • Java编程使用Runtime和Process类运行外部程序的方法

    Java编程使用Runtime和Process类运行外部程序的方法

    这篇文章主要介绍了Java编程使用Runtime和Process类运行外部程序的方法,结合实例形式分析了java使用Runtime.getRuntime().exec()方法运行外部程序的常见情况与操作技巧,需要的朋友可以参考下
    2017-08-08

最新评论