SpringBoot監(jiān)聽Redis Key過期事件的幾種方式
1. 示例場景
電商平臺常會發(fā)放「限時優(yōu)惠券」(比如 “滿 100 減 20,30 分鐘內(nèi)有效”“24 小時專屬券”),核心訴求是:
- 優(yōu)惠券一旦超過有效期,必須自動標(biāo)記為 “已失效”,用戶無法再使用;
- 避免用戶在優(yōu)惠券過期后仍嘗試用券下單,導(dǎo)致訂單支付異常;
- 無需定時任務(wù)輪詢(比如每分鐘掃全量表),減少數(shù)據(jù)庫壓力;
- 精準(zhǔn)觸發(fā)失效邏輯(優(yōu)惠券到期瞬間就處理,而非定時任務(wù)的 “延遲生效”)。
如果用傳統(tǒng)定時任務(wù)方案,會面臨兩個核心問題:
- 優(yōu)惠券發(fā)放時間分散(用戶隨時領(lǐng)券),無法精準(zhǔn)設(shè)定輪詢規(guī)則(比如 30 分鐘有效期的券,有的用戶 10:01 領(lǐng),有的 10:05 領(lǐng),定時任務(wù)間隔設(shè)短了耗性能,設(shè)長了失效不及時);
- 全量表輪詢會頻繁查詢數(shù)據(jù)庫,高并發(fā)下(比如大促發(fā)百萬張券)數(shù)據(jù)庫扛不住。
此時,用「Redis 監(jiān)聽鍵過期」的方案就能完美解決,既精準(zhǔn)又低耗。
2. 配置 Redis 開啟key 過期通知
- Redis 默認(rèn)關(guān)閉鍵空間通知,需要先開啟并指定監(jiān)聽的事件類型。
方式一:臨時配置(重啟 Redis 失效)
# 開啟過期事件監(jiān)聽,也可按需指定(如只監(jiān)聽過期+驅(qū)逐:KxKe) 127.0.0.1:6379> config set notify-keyspace-events Ex
方式 二:永久配置
- 修改Redis配置文件redis.conf,查看 notify-keyspace-events 配置項,修改為
notify-keyspace-events Ex,并重啟Redis生效,相關(guān)參數(shù)說明如下:
K:keyspace 事件,事件以 keyspace@ 為前綴進(jìn)行發(fā)布 E:keyevent 事件,事件以 keyevent@ 為前綴進(jìn)行發(fā)布 g:一般性的,非特定類型的命令,比如del,expire,rename等 $:字符串特定命令 l:列表特定命令 s:集合特定命令 h:哈希特定命令 z:有序集合特定命令 x:過期事件,當(dāng)某個鍵過期并刪除時會產(chǎn)生該事件 e:驅(qū)逐事件,當(dāng)某個鍵鍵因Redis內(nèi)存達(dá)到maxmemory上限,被淘汰策略刪除時觸發(fā) A:g$lshzxe的別名,因此”AKE”意味著所有事件
3. 添加pom文件
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>4. RedisListenerConfig配置類
- 實現(xiàn)監(jiān)聽 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監(jiān)聽類
- 實現(xiàn)
KeyExpirationEventMessageListener接口,查看源碼發(fā)現(xiàn),該接口監(jiān)聽所有db的過期事件 keyevent@*:expired" - 鍵過期事件并非“實時觸發(fā)”:Redis 采用 “惰性刪除 + 定期刪除” 策略,過期鍵可能延遲觸發(fā)事件。
/**
* 監(jiān)聽所有db的過期事件__keyevent@*__:expired"
*/
@Component
@Slf4j
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}
/**
* 針對 redis 數(shù)據(jù)失效事件,進(jìn)行數(shù)據(jù)處理
* @param message
* @param pattern
*/
@Override
public void onMessage(Message message, byte[] pattern) {
// 獲取到失效的 key,進(jìn)行取消訂單業(yè)務(wù)處理
String expiredKey = message.toString();
log.info("失效的Key:{}", expiredKey);
}
}
6. 使用線程池優(yōu)化
- 先定義一個專門處理過期事件業(yè)務(wù)的線程池,避免使用默認(rèn)線程池導(dǎo)致資源競爭:
/**
* 過期事件異步處理線程池配置
*/
@Configuration
@EnableAsync // 開啟異步注解支持
public class RedisEventExecutorConfig {
/**
* 自定義線程池,處理過期事件的業(yè)務(wù)邏輯
*/
@Bean("redisExpireEventExecutor")
public Executor redisExpireEventExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心線程數(shù)(根據(jù)業(yè)務(wù)壓力調(diào)整)
executor.setCorePoolSize(5);
// 最大線程數(shù)
executor.setMaxPoolSize(20);
// 隊列容量(緩沖待處理的任務(wù))
executor.setQueueCapacity(1000);
// 線程名前綴(便于日志排查)
executor.setThreadNamePrefix("redis-expire-event-");
// 拒絕策略:隊列滿+線程數(shù)到最大時,由提交任務(wù)的線程執(zhí)行(避免任務(wù)丟失)
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 線程空閑超時時間
executor.setKeepAliveSeconds(60);
// 初始化線程池
executor.initialize();
return executor;
}
}
// 在監(jiān)聽類中調(diào)用
@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. 進(jìn)一步優(yōu)化
如果過期事件量極大(比如每秒上萬),僅靠線程池可能不夠,可疊加以下方案:
7.1. 引入消息隊列(如 RocketMQ/Kafka)
- 監(jiān)聽器只做 “生產(chǎn)消息”:將過期 key 發(fā)送到消息隊列;
- 單獨的消費者服務(wù)處理業(yè)務(wù)邏輯:解耦Redis監(jiān)聽和業(yè)務(wù)處理,支持削峰填谷。
7.2. 優(yōu)化 Redis 監(jiān)聽容器線程池
RedisMessageListenerContainer 自身也有監(jiān)聽線程池,可調(diào)整參數(shù)避免監(jiān)聽線程不夠用:
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
// 設(shè)置監(jiān)聽線程池的核心參數(shù)(默認(rèn)線程數(shù)可能不足)
container.setTaskExecutor(new ThreadPoolTaskExecutor() {{
setCorePoolSize(3); // 監(jiān)聽線程數(shù),根據(jù)事件量調(diào)整
setMaxPoolSize(5);
setThreadNamePrefix("redis-listener-");
initialize();
}});
return container;
}
以上就是SpringBoot監(jiān)聽Redis Key過期事件的幾種方式的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot監(jiān)聽Redis Key過期事件的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java實現(xiàn)監(jiān)控多個線程狀態(tài)的簡單實例
下面小編就為大家?guī)硪黄狫ava實現(xiàn)監(jiān)控多個線程狀態(tài)的簡單實例。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-03-03
Spring?Boot?3.1中整合Spring?Security和Keycloak的方法
本文介紹在最新的SpringBoot3.1版本之下,如何將Keycloak和Spring?Security一起跑起來,文中結(jié)合實例代碼給大家介紹的非常詳細(xì),需要的朋友參考下吧2023-06-06
JavaWeb開發(fā)入門第二篇Tomcat服務(wù)器配置講解
JavaWeb開發(fā)入門第二篇主要介紹了Tomcat服務(wù)器配置的方法教大家如何使用Tomcat服務(wù)器,感興趣的小伙伴們可以參考一下2016-04-04
Spring Cloud根據(jù)服務(wù)名獲取服務(wù)的ip端口問題
這篇文章主要介紹了Spring Cloud根據(jù)服務(wù)名獲取服務(wù)的ip端口,本篇示例我就以Nacos注冊中心為例了,下面是我注冊的兩個服務(wù),需要的朋友可以參考下2022-09-09
教你開發(fā)腳手架集成Spring?Boot?Actuator監(jiān)控的詳細(xì)過程
這篇文章主要介紹了開發(fā)腳手架集成Spring?Boot?Actuator監(jiān)控的詳細(xì)過程,集成包括引入依賴配置文件及訪問驗證的相關(guān)知識,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-05-05
elasticsearch集群發(fā)現(xiàn)zendiscovery的Ping機制分析
這篇文章主要為大家介紹了elasticsearch集群發(fā)現(xiàn)zendiscovery的Ping機制分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-04-04
Spring?Boot與graphql-java如何構(gòu)建高效GraphQL服務(wù)
文章介紹如何在SpringBoot中集成graphql-java庫,通過定義GraphQLSchema、構(gòu)建實例及注冊Web端點實現(xiàn)高效數(shù)據(jù)查詢與突變操作,解決RESTAPI的過度獲取問題,提升開發(fā)靈活性與性能,并結(jié)合工具如ApolloClient優(yōu)化前端數(shù)據(jù)管理,感興趣的朋友跟隨小編一起看看吧2025-09-09

