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

<think>嗯,用户想查找Redis中所有没有设置过期时间的key,或者TTL等于-1的。那我得先回忆一下Redis的相关命令。首先,RedisKEYS命令可以匹配所有符合模式的键,比如KEYS *,但这样会阻塞服务器,尤其是在数据量大的时候,生产环境不太推荐使用。用户提到的示例代码里用了SCAN命令,可能更合适,因为SCAN是增量式的,不会阻塞。 接下来,每个键的过期时间可以通过TTL命令查看。TTL返回-1表示没有设置过期时间,-2表示键不存在。所以需要遍历所有键,检查它们的TTL是否为-1。但如何高效地遍历呢?使用SCAN命令迭代,然后对每个键执行TTL,判断是否为-1。 不过,用户提供的引用里提到了一些相关的命令,比如SINTERSTORE和BRPOP,但这些可能和当前问题无关。不过引用中的示例代码使用了scan方法来获取匹配模式的键,比如"future_*"。这可能对用户的问题有帮助,因为用户需要扫描所有键,然后过滤出没有过期时间的。 另外,用户提到的问题可能涉及到性能问题。如果直接使用KEYS *,可能会导致Redis阻塞,尤其是在数据量大的情况下。所以应该推荐使用SCAN命令来分批次获取键,减少对服务器的影响。 然后,具体步骤应该是:用SCAN遍历所有键,对每个键执行TTL命令,收集那些TTL为-1的键。这个过程可以通过脚本实现,比如用Lua脚本或者用客户端代码循环SCAN和TTL。 不过,Redis本身并没有直接提供一次性查询所有无过期时间键的命令,所以需要自己组合命令。例如,在shell中可以用redis-cli的--scan选项,结合管道和xargs来处理。比如: redis-cli --scan | while read key; do if [ $(redis-cli ttl "$key") -eq -1 ]; then echo "$key" fi done 但这种方法在键很多的时候会很慢,因为每个TTL命令都是一个请求。有没有更高效的方法?可能用Lua脚本,用SCAN在服务端遍历,然后直接返回符合条件的键。不过Lua脚本需要考虑执行时间,避免阻塞。 另外,用户可能需要考虑权限问题,是否有权限执行SCAN和TTL命令,以及数据量大的情况下内存和网络的影响。如果键的数量极大,可能需要分多次处理,或者使用并行处理的方法。 还有一个问题,如何验证这个方法的正确性。比如,测试环境中先试一下,确认返回的键确实是没有设置过期时间的。用户提供的引用中有一个测试方法用了scan,可能可以参考。 总结下来,解决方案的大致步骤是: 1. 使用SCAN命令遍历所有键。 2. 对每个键执行TTL命令,检查是否为-1。 3. 收集符合条件的键并返回。 需要注意避免使用KEYS命令,使用SCAN代替。另外,处理大量数据时需要分批次,避免长时间阻塞或占用过多资源。</think>要查找Redis中所有没有设置过期时间的键(即TTL等于-1),可以通过以下分步实现: ### 实现步骤 1. **使用SCAN命令安全遍历所有键** ```bash redis-cli --scan --pattern '*' | while read key; do echo "检查键: $key" done ``` 避免使用`KEYS *`命令,防止阻塞Redis服务[^1]。 2. **结合TTL命令过滤无过期时间的键** ```bash redis-cli --scan --pattern '*' | while read key; do ttl=$(redis-cli ttl "$key") if [ $ttl -eq -1 ]; then echo "无过期时间: $key" fi done ``` TTL返回值说明: - `-1`: 无过期时间 - `-2`: 键不存在 - `>=0`: 剩余生存时间(秒)[^2] 3. **优化方案(Lua脚本实现)** ```lua local cursor = 0 local keys = {} repeat local res = redis.call('SCAN', cursor, 'MATCH', '*', 'COUNT', 100) cursor = tonumber(res[1]) for _,key in ipairs(res[2]) do if redis.call('TTL', key) == -1 then table.insert(keys, key) end end until cursor == 0 return keys ``` 执行脚本: ```bash redis-cli --eval find_no_expire.lua ``` ### 注意事项 - **性能影响**:扫描百万级键可能耗时数分钟,建议在从节点执行 - **内存消耗**:返回大量键可能占用客户端内存,建议分批处理 - **版本要求**:SCAN命令需要Redis 2.8+版本支持[^3] ### 验证方法 ```bash # 设置测试键 redis-cli set test_key1 value1 redis-cli set test_key2 value2 EX 60 # 执行过滤脚本应只返回test_key1 ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值