当前位置: 首页 > news >正文

SpringBoot监听Redis Key过期事件

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>configsetnotify-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过期时间
importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.data.redis.connection.RedisConnectionFactory;importorg.springframework.data.redis.listener.RedisMessageListenerContainer;@ConfigurationpublicclassRedisListenerConfig{@BeanRedisMessageListenerContainercontainer(RedisConnectionFactoryconnectionFactory){RedisMessageListenerContainercontainer=newRedisMessageListenerContainer();container.setConnectionFactory(connectionFactory);returncontainer;}}

5. RedisKeyExpirationListener监听类

  • 实现KeyExpirationEventMessageListener接口,查看源码发现,该接口监听所有db的过期事件 keyevent@*:expired"
  • 键过期事件并非“实时触发”:Redis 采用 “惰性删除 + 定期删除” 策略,过期键可能延迟触发事件。
/** * 监听所有db的过期事件__keyevent@*__:expired" */@Component@Slf4jpublicclassRedisKeyExpirationListenerextendsKeyExpirationEventMessageListener{publicRedisKeyExpirationListener(RedisMessageListenerContainerlistenerContainer){super(listenerContainer);}/** * 针对 redis 数据失效事件,进行数据处理 * @param message * @param pattern */@OverridepublicvoidonMessage(Messagemessage,byte[]pattern){// 获取到失效的 key,进行取消订单业务处理StringexpiredKey=message.toString();log.info("失效的Key:{}",expiredKey);}}

6. 使用线程池优化

  • 先定义一个专门处理过期事件业务的线程池,避免使用默认线程池导致资源竞争:
/** * 过期事件异步处理线程池配置 */@Configuration@EnableAsync// 开启异步注解支持publicclassRedisEventExecutorConfig{/** * 自定义线程池,处理过期事件的业务逻辑 */@Bean("redisExpireEventExecutor")publicExecutorredisExpireEventExecutor(){ThreadPoolTaskExecutorexecutor=newThreadPoolTaskExecutor();// 核心线程数(根据业务压力调整)executor.setCorePoolSize(5);// 最大线程数executor.setMaxPoolSize(20);// 队列容量(缓冲待处理的任务)executor.setQueueCapacity(1000);// 线程名前缀(便于日志排查)executor.setThreadNamePrefix("redis-expire-event-");// 拒绝策略:队列满+线程数到最大时,由提交任务的线程执行(避免任务丢失)executor.setRejectedExecutionHandler(newThreadPoolExecutor.CallerRunsPolicy());// 线程空闲超时时间executor.setKeepAliveSeconds(60);// 初始化线程池executor.initialize();returnexecutor;}}// 在监听类中调用@ComponentpublicclassRedisExpireBusinessService{@Async("redisExpireEventExecutor")publicvoidhandleExpiredKeyAsync(StringexpiredKey){try{System.out.println("异步处理过期key:"+expiredKey+",线程:"+Thread.currentThread().getName());}catch(Exceptione){System.err.println("处理过期key失败:"+expiredKey+",异常:"+e.getMessage());}}}

7. 进一步优化

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

7.1. 引入消息队列(如 RocketMQ/Kafka)
  • 监听器只做 “生产消息”:将过期 key 发送到消息队列;
  • 单独的消费者服务处理业务逻辑:解耦Redis监听和业务处理,支持削峰填谷。
7.2. 优化 Redis 监听容器线程池

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

@BeanpublicRedisMessageListenerContainerredisMessageListenerContainer(RedisConnectionFactoryconnectionFactory){RedisMessageListenerContainercontainer=newRedisMessageListenerContainer();container.setConnectionFactory(connectionFactory);// 设置监听线程池的核心参数(默认线程数可能不足)container.setTaskExecutor(newThreadPoolTaskExecutor(){{setCorePoolSize(3);// 监听线程数,根据事件量调整setMaxPoolSize(5);setThreadNamePrefix("redis-listener-");initialize();}});returncontainer;}
http://www.jsqmd.com/news/477987/

相关文章:

  • 华为云OBS实战配置:从基础创建到高级策略部署
  • 超4亿元!知识产权行业单笔最大融资落地
  • 重燃创作激情,Webnovel Writer助你轻松连载!
  • MVVM 架构demo
  • 单目结构光三维重建:MATLAB代码实现
  • 【软考】中级软件设计师 23年上半年真题深度解析:从综合知识到案例实战
  • GESP5级C++考试语法知识(四、高精度计算(一)加法和减法)
  • Linux下hadoop2.9.2单节点伪分布搭建完全教程
  • InstructPix2Pix应用全解析:从风景优化到人像精修,小白也能轻松上手
  • 垂直泊车(带Matlab可调参)
  • SEER‘S EYE 一键部署后,如何通过Git进行版本管理与协作开发
  • SQL 窗口函数 学习笔记
  • R提供了一些函数用于判断逻辑表达式的结果
  • C++面试总结(1)
  • MacBook + 台式机如何共享双屏?KVM切换器是终极答案|附TESmart全系对比推荐
  • 深入解析桥接模式:一个C++模板实现的通用桥接类库
  • 统信UOS 20 高效部署实战指南
  • 机械臂轨迹规划是机器人开发中的重头戏,今天咱们用Matlab的Robotics Toolbox带PUMA560走两步。先给机械臂充个电——初始化模型
  • 报错v-bind is missing expression
  • 局部遮阴光伏MPPT仿真:粒子群算法详解及视频解析
  • Multisim仿真TL494BUCK闭环,稳定输出5v,带软启动。 电流限制为0.14A电流...
  • 从时序建模到寿命预测:TCN在轴承RUL预测中的实战解析
  • k8s工作负载-HPA控制器
  • 【Docker】Linux系统上卸载旧Docker、卸载Podman并重新安装Docker及配置国内镜像源
  • 基于二次规划的路径规划与速度规划:从MATLAB到C++的实践
  • 又崩了?C++的灵活,新手的坑,老手的泪,高手都没控制就泄漏了
  • 2026做媒介宣发,真没必要再求人找关系了
  • LTspice仿真学习指南:掌握LDO模拟集成电路电源及其关键仿真技巧(包括相位裕度等)
  • 周期 Pattern Removal 算法
  • OpenClaw 技能插件开发实战:适配职业教育的 AI 实训案例