redis过期key监听与发布订阅功能java

背景: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

猜你喜欢

转载自blog.csdn.net/name_is_wl/article/details/111037739