spring boot-使用redis的Keyspace Notifications实现定时任务队列

前言:

最近项目中有一个需求:需要将执行失败的指令存起来,隔5分钟之后,再取出来执行一次,看到这个需求描述,我第一时间想到了Redis的Keyspace Notifications机制。当然也可以使用jdk自带的DelayQueue来实现,或者更进一步,使用ScheduledThreadPoolExecutor池来实现。由于系统是分布式的,所以考虑使用Redis来实现。

关于Redis的Keyspace Notifications机制,请参考:http://redisdoc.com/topic/notification.htm

实现原理:在Redis2.8之后的版本中,当我们将<key, value>对使用Redis缓存起来并设置缓存失效时间的时候,会触发Redis的键事件通知,客户端订阅这个通知事件,服务端会将对应的通知事件发送给每个订阅的客户端,然后客户端根据收到的通知,做相应的后续处理(例如:键过期时间通知对应的topic为:"__keyevent@0__:expired")。Redis支持的通知类型如下:

因为开启键空间通知功能需要消耗一些 CPU ,所以在默认配置下,该功能处于关闭状态。

可以通过修改 redis.conf 文件,或者直接使用CONFIGSET 命令来开启或关闭键空间通知功能:

配置文件修改方式如下:

[java]  view plain  copy
  1. notify-keyspace-events Ex  // 打开此配置,其中Ex表示键事件通知里面的key过期事件,每当有过期键被删除时,会发送通知  

  • 当 notify-keyspace-events 选项的参数为空字符串时,功能关闭。
  • 另一方面,当参数不是空字符串时,功能开启。

notify-keyspace-events 的参数可以是以下字符的任意组合,它指定了服务器该发送哪些类型的通知:

字符发送的通知
K键空间通知,所有通知以 __keyspace@<db>__ 为前缀
E键事件通知,所有通知以 __keyevent@<db>__ 为前缀
gDEL 、 EXPIRE 、 RENAME 等类型无关的通用命令的通知
$字符串命令的通知
l列表命令的通知
s集合命令的通知
h哈希命令的通知
z有序集合命令的通知
x过期事件:每当有过期键被删除时发送
e驱逐(evict)事件:每当有键因为 maxmemory 政策而被删除时发送
A参数 g$lshzxe 的别名

输入的参数中至少要有一个 K 或者E ,否则的话,不管其余的参数是什么,都不会有任何通知被分发。

举个例子,如果只想订阅键空间中和列表相关的通知,那么参数就应该设为 Kl ,诸如此类。

将参数设为字符串 "AKE" 表示发送所有类型的通知。

1、编写监听器

[java]  view plain  copy
  1. package com.chhliu.springboot.redis.config;  
  2.   
  3.   
  4. import org.springframework.data.redis.connection.Message;  
  5. import org.springframework.data.redis.connection.MessageListener;  
  6. import org.springframework.stereotype.Component;  
  7.   
  8. @Component  
  9. public class TopicMessageListener implements MessageListener {  
  10.   
  11.     @Override  
  12.     public void onMessage(Message message, byte[] pattern) {// 客户端监听订阅的topic,当有消息的时候,会触发该方法  
  13.             byte[] body = message.getBody();// 请使用valueSerializer  
  14.             byte[] channel = message.getChannel();  
  15.             String topic = new String(channel);  
  16.             String itemValue = new String(body);  
  17.             // 请参考配置文件,本例中key,value的序列化方式均为string。  
  18.             System.out.println("topic:"+topic);  
  19.             System.out.println("itemValue:"+itemValue);  
  20.     }  
  21. }  

2、配置RedisMessageListenerContainer监听容器

[java]  view plain  copy
  1. package com.chhliu.springboot.redis.config;  
  2.   
  3. import java.util.concurrent.Executor;  
  4. import java.util.concurrent.ThreadPoolExecutor;  
  5.   
  6. import org.springframework.beans.factory.annotation.Autowired;  
  7. import org.springframework.beans.factory.annotation.Value;  
  8. import org.springframework.context.annotation.Bean;  
  9. import org.springframework.context.annotation.Configuration;  
  10. import org.springframework.data.redis.core.RedisTemplate;  
  11. import org.springframework.data.redis.listener.ChannelTopic;  
  12. import org.springframework.data.redis.listener.RedisMessageListenerContainer;  
  13. import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;  
  14.   
  15. @Configuration  
  16. public class RedisMessageListenerContainerConfig {  
  17.       
  18.     @Autowired  
  19.     private RedisTemplate<Object,Object> redisTemplate;  
  20.       
  21.     @Autowired  
  22.     private TopicMessageListener messageListener;  
  23.       
  24.     @Autowired  
  25.     private TaskThreadPoolConfig config;  
  26.       
  27.     @Value("spring.redis.topic")  
  28.     private String topic;  
  29.       
  30.     @Bean  
  31.     public RedisMessageListenerContainer configRedisMessageListenerContainer(Executor executor){  
  32.         RedisMessageListenerContainer container = new RedisMessageListenerContainer();  
  33.         // 设置Redis的连接工厂  
  34.         container.setConnectionFactory(redisTemplate.getConnectionFactory());  
  35.         // 设置监听使用的线程池  
  36.         container.setTaskExecutor(executor);  
  37.         // 设置监听的Topic  
  38.         ChannelTopic channelTopic = new ChannelTopic("__keyevent@0__:expired");  
  39.         // 设置监听器  
  40.         container.addMessageListener(messageListener, channelTopic);  
  41.         return container;  
  42.     }  
  43.       
  44.     @Bean // 配置线程池  
  45.     public Executor myTaskAsyncPool() {  
  46.         ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();  
  47.         executor.setCorePoolSize(config.getCorePoolSize());  
  48.         executor.setMaxPoolSize(config.getMaxPoolSize());  
  49.         executor.setQueueCapacity(config.getQueueCapacity());  
  50.         executor.setKeepAliveSeconds(config.getKeepAliveSeconds());  
  51.         executor.setThreadNamePrefix(config.getThreadNamePrefix());  
  52.   
  53.         // rejection-policy:当pool已经达到max size的时候,如何处理新任务  
  54.         // CALLER_RUNS:不在新线程中执行任务,而是由调用者所在的线程来执行  
  55.         executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());  
  56.         executor.initialize();  
  57.         return executor;  
  58.     }  
  59. }  
注:具体的线程池配置,可以参考我的另一篇博客: http://blog.csdn.net/liuchuanhong1/article/details/64132520

注意:”_keyevent@0_:expired“中的"_",并不是英文下的下划线,建议从Redis客户端直接拷贝,否则会匹配不上订阅的topic


3、配置文件

[java]  view plain  copy
  1. server.port=9999  
  2. ########################################################  
  3. ###REDIS (RedisProperties) redis基本配置;  
  4. ########################################################  
  5. # database name  
  6. spring.redis.database=0  
  7. # server host1 单机使用,对应服务器ip  
  8. #spring.redis.host=127.0.0.1    
  9. # server password 密码,如果没有设置可不配  
  10. #spring.redis.password=  
  11. #connection port  单机使用,对应端口号  
  12. #spring.redis.port=6379  
  13. # pool settings ...池配置  
  14. spring.redis.pool.max-idle=8  
  15. spring.redis.pool.min-idle=0  
  16. spring.redis.pool.max-active=8  
  17. spring.redis.pool.max-wait=-1  
  18. # name of Redis server  哨兵监听的Redis server的名称  
  19. spring.redis.sentinel.master=mymaster  
  20. # comma-separated list of host:port pairs  哨兵的配置列表  
  21. spring.redis.sentinel.nodes=192.168.1.108:26379,192.168.1.108:26479,192.168.1.108:26579  
  22.   
  23. spring.task.pool.corePoolSize=10  
  24. spring.task.pool.maxPoolSize=20  
  25. spring.task.pool.keepAliveSeconds=60  
  26. spring.task.pool.queueCapacity=100  
  27. spring.task.pool.threadNamePrefix=myThreadPool  
  28.   
  29. spring.redis.topic=__keyevent@0__:expired  

4、启动程序

[java]  view plain  copy
  1. package com.chhliu.springboot.redis;  
  2.   
  3. import org.springframework.boot.SpringApplication;  
  4. import org.springframework.boot.autoconfigure.SpringBootApplication;  
  5. import org.springframework.boot.context.properties.EnableConfigurationProperties;  
  6.   
  7. import com.chhliu.springboot.redis.config.TaskThreadPoolConfig;  
  8.   
  9. @SpringBootApplication  
  10. @EnableConfigurationProperties({TaskThreadPoolConfig.class} ) // 开启配置属性支持  
  11. public class SpringbootRedisApplication {  
  12.       
  13.     public static void main(String[] args) {  
  14.         SpringApplication.run(SpringbootRedisApplication.class, args);  
  15.     }  
  16. }  

5、在客户端输入命令

[java]  view plain  copy
  1. set myname chhliu  
  2. expire myname 2  
  3. 或者如下:  
  4. set myname chhliu px 2000  
设置键值,并指定超时时间为2s,大概2s后,就会看到console端如下的输出了:

[java]  view plain  copy
  1. 2017-04-12 20:23:16.367  INFO 12464 --- [  myThreadPool2] c.c.s.redis.config.TopicMessageListener  : 是否获取到锁:true  
  2. topic:__keyevent@0__:expired  
  3. itemValue:myname  
  4. 2017-04-12 20:23:16.369  INFO 12464 --- [  myThreadPool2] c.c.s.redis.config.TopicMessageListener  : 任务结束,释放锁!  
注意:

1、上面的_keyevent@0_:expired是key过期事件对应的topic,服务端会将过期事件推送到该topic中,然后客户端监听这个topic。

2、key过期事件推送到topic中的内容只有key,而无value,因为一旦过期,value就不存在了。

注意事项

Redis 使用以下两种方式删除过期的键:

  • 当一个键被访问时,程序会对这个键进行检查,如果键已经过期,那么该键将被删除。
  • 底层系统会在后台渐进地查找并删除那些过期的键,从而处理那些已经过期、但是不会被访问到的键。

当过期键被以上两个程序的任意一个发现、并且将键从数据库中删除时,Redis 会产生一个 expired 通知。

Redis 并不保证生存时间(TTL)变为 0 的键会立即被删除:如果程序没有访问这个过期键,或者带有生存时间的键非常多的话,那么在键的生存时间变为0 ,直到键真正被删除这中间,可能会有一段比较显著的时间间隔。

因此,Redis 产生  expired  通知的时间为过期键被删除的时候,而不是键的生存时间变为 0  的时候。如果业务无法容忍从过期到删除中间的时间间隔,那么就只有用其他的方式了。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值