背景:redis2.8后提供了发布订阅(pub|sub)功能
实现redis过期key的监听,只需要在监听容器中将键过期事件的消息通道(keyevent@*:expired)与listener绑定即可。keyevent@*:expired中的*号表示匹配redis中所有db0-db15的数据库,keyevent@0:expired表示只监听db0数据库的key过期事件
spring-data-redis的实现
1.在spring-data-redis中提供了KeyExpirationEventMessageListener监听器,实现监听key过期可以直接继承KeyExpirationEventMessageListener并重写onMessage方法
代码
package com.wl.redis.listener;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.listener.KeyExpirationEventMessageListener;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
/**
* Created by Administrator on 2020/12/10.
*/
@Component
public class KeyExpirationListener extends KeyExpirationEventMessageListener {
public KeyExpirationListener(RedisMessageListenerContainer listenerContainer){
super(listenerContainer);
}
public void onMessage(Message message, @Nullable byte[] pattern) {
String key = message.toString();
System.out.println("监听到key:" + key + "过期");
}
}
KeyExpirationEventMessageListener 只有一个带RedisMessageListenerContainer参数的构造器,所以我们需要注入RedisMessageListenerContainer
package com.wl.redis.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;
/**
* Created by Administrator on 2020/12/10.
*/
@Configuration
public class RedisConfig {
@Bean
public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}
}
将通道与listener绑定是在KeyExpirationEventMessageListener实现的
在KeyExpirationEventMessageListener 中重写了其父类的doRegister方法。将__keyevent@*__:expired与监听器绑定。部分源代码如下
private static final Topic KEYEVENT_EXPIRED_TOPIC = new PatternTopic("__keyevent@*__:expired");
@Override
protected void doRegister(RedisMessageListenerContainer listenerContainer) {
listenerContainer.addMessageListener(this, KEYEVENT_EXPIRED_TOPIC);
}
测试set order 123456 ex 4
控制台输出监听到key:order过期
2.自定义key过期监听
实现MessageListener接口
package com.wl.redis.listener;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
/**
* Created by Administrator on 2020/12/11.
*/
@Component
public class CustomerKeyExpirationListener implements MessageListener {
@Override
public void onMessage(Message message, @Nullable byte[] pattern) {
String key = message.toString();
System.out.println("监听到key:" + key + "过期");
}
}
container绑定channel与listener
package com.wl.redis.config;
import com.wl.redis.listener.CustomerKeyExpirationListener;
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.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
/**
* Created by Administrator on 2020/12/10.
*/
@Configuration
public class RedisConfig {
@Bean
public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory
, CustomerKeyExpirationListener customerKeyExpirationListener) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.addMessageListener(customerKeyExpirationListener,new PatternTopic("__keyevent@*__:expired"));
return container;
}
}
redis key过期监听使用建议
1.需要监听的key命名应与其他正常不需要监听的区别。例如
public void onMessage(Message message, @Nullable byte[] pattern) {
String key = message.toString();
if(!key.startsWith("expiration")){
return;
}
System.out.println("监听到key:" + key + "过期");
}
2.监听触发时该key已经被删除,只能获取key的值而获取不到其value的值,因此key中应该包含你需要的信息,或者通过该key可以查询到你所需要的信息
3.在key过期前主动删除该key是不会触发过期监听事件的,在订单过期未支付取消订单的场景下,如果key过期前该订单已经支付或取消应删除该key
4.在分布式场景下,该监听器会监听多次,因此需要使用锁(防止同一个key被监听执行多次).例如
package com.wl.redis.listener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.listener.KeyExpirationEventMessageListener;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* Created by Administrator on 2020/12/10.
*/
@Component
public class KeyExpirationListener extends KeyExpirationEventMessageListener {
@Autowired
private StringRedisTemplate redisTemplate;
public KeyExpirationListener(RedisMessageListenerContainer listenerContainer){
super(listenerContainer);
}
public void onMessage(Message message, @Nullable byte[] pattern) {
String key = message.toString();
if(!key.startsWith("expiration")){
return;
}
//加锁(不同的key过期获取的锁是不一样的)
String lockKey = "lock_" + key;
boolean lock = lock(lockKey);
if(!lock){
System.out.println("===return" + key + "===");
return;
}
try {
System.out.println("监听到key:" + key + "过期");
//释放锁(可以不用释放)
// 这里睡眠5秒后解锁,防止程序太快,导致服务1已经执行完毕,服务2才刚刚开始获取锁
Thread.sleep(5000);
unlock(lockKey);
} catch (InterruptedException e) {
//
}
}
private Boolean lock(String lockKey){
Long timeOut = redisTemplate.getExpire(lockKey);
SessionCallback<Boolean> sessionCallback = new SessionCallback<Boolean>() {
List<Object> exec = null;
@Override
@SuppressWarnings("unchecked")
public Boolean execute(RedisOperations operations) throws DataAccessException {
operations.multi();
operations.opsForValue().setIfAbsent(lockKey,"lock");
if(timeOut == null || timeOut == -2) {
operations.expire(lockKey, 30, TimeUnit.SECONDS);
}
exec = operations.exec();
if(exec.size() > 0) {
return (Boolean) exec.get(0);
}
return false;
}
};
return redisTemplate.execute(sessionCallback);
}
private void unlock(String lockKey){
redisTemplate.delete(lockKey);
}
}
开启两个服务,添加测试数据
for(int i = 1;i<=10;i++){
stringRedisTemplate.opsForValue().set("expiration_order_" + i,i + "",6,TimeUnit.SECONDS);
Thread.sleep(1000);
}
两个服务分别输出如下
===returnexpiration_order_1===
===returnexpiration_order_2===
===returnexpiration_order_3===
===returnexpiration_order_4===
===returnexpiration_order_5===
监听到key:expiration_order_6过期
监听到key:expiration_order_7过期
===returnexpiration_order_8===
===returnexpiration_order_9===
===returnexpiration_order_10===
监听到key:expiration_order_1过期
监听到key:expiration_order_2过期
监听到key:expiration_order_3过期
监听到key:expiration_order_4过期
监听到key:expiration_order_5过期
===returnexpiration_order_6===
===returnexpiration_order_7===
监听到key:expiration_order_8过期
监听到key:expiration_order_9过期
监听到key:expiration_order_10过期
5.如果你的redis重启之后发布订阅失效,请将redis.conf的配置文件修改如下
6.
notify-keyspace-events EA
使用redis的发布订阅
上面已经实现了通道与监听器的绑定(订阅)的示例,下面只需要实现发布消息即可
package com.wl.redis.publisher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.Topic;
import org.springframework.stereotype.Component;
/**
* Created by Administrator on 2020/12/11.
*/
@Component
public class RedisPublisher {
@Autowired
private RedisTemplate redisTemplate;
public void sendMessage(String channel,String message){
redisTemplate.convertAndSend(channel,message);
}
}
listener
package com.wl.redis.listener;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
/**
* Created by Administrator on 2020/12/11.
*/
@Component
public class CustomerListener implements MessageListener{
@Autowired
private RedisTemplate redisTemplate;
@Override
public void onMessage(Message message, @Nullable byte[] pattern) {
//反序列化解决message乱码
RedisSerializer<?> serializer = redisTemplate.getValueSerializer();
System.out.println("================" + serializer.deserialize(message.getBody()).toString());
}
}
config
package com.wl.redis.config;
import com.wl.redis.listener.CustomerKeyExpirationListener;
import com.wl.redis.listener.CustomerListener;
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.ChannelTopic;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
/**
* Created by Administrator on 2020/12/10.
*/
@Configuration
public class RedisConfig {
@Bean
public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory
,CustomerListener customerListener) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
// container.addMessageListener(customerKeyExpirationListener,new PatternTopic("__keyevent@*__:expired"));
container.addMessageListener(customerListener,new ChannelTopic("customer"));
return container;
}
}
测试
publisher.sendMessage("customer","你好");
注意在CustomerListener中没有经过RedisSerializer反序列化,可能会导致接收的消息乱码
Jedis的实现
1.监听器继承JedisPubSub 类
package com.wl.redis.jedis;
import org.springframework.stereotype.Component;
import redis.clients.jedis.JedisPubSub;
/**
* Created by Administrator on 2020/12/11.
*/
@Component
public class JedisKeyExpirationListener extends JedisPubSub {
public void onMessage(String channel, String message) {
System.out.println("channel:"+ channel + " message:" + message+":");
}
}
订阅__keyevent@0__:expired(注意这里@后面不是星号而是0,只能监听db0,否则监听不到)
@Autowired
private JedisKeyExpirationListener jedisKeyExpirationListener;
static JedisPool pool = new JedisPool("192.168.92.128",6380);
@Override
public void run(String... args) throws Exception {
new Thread(new Runnable() {
@Override
public void run() {
pool.getResource().subscribe(jedisKeyExpirationListener,"__keyevent@0__:expired");
}
}).start();
Jedis jedis = pool.getResource();
jedis.set("wl","你好");
jedis.expire("wl",5);
jedis.close();
}
最后介绍一个免费的redis客户端
https://github.com/qishibo/AnotherRedisDesktopManager