Redission集群下过期key监听回调事件不执行

文章讲述了在Redis集群环境下,使用key过期监听回调存在的问题,包括延迟、消息丢失和广播模式等,并提出了使用Redission的延迟队列作为替代方案,提供了相关Java代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

背景

最近有一个需求,实现达到预设时长没有收全数据,就触发转发请求,我就想起了redis的key过期回调事件去实现,起初单机redis的环境中没有任何问题,但是生产是redission集群环境,结果上去之后,就出现了过期key监听回调事件丢失的情况,当时还是很不可思议的,因为redis现在也比较成熟了,没想到遇到这个问题,然后,查了一下,redission确实在key事件上有bug,哪到底有哪些坑,如下所示:

集群环境下直接使用redis的key过期监听有哪些坑?

1.过期会有延迟

key的过期事件发布时机并不是当这个key的过期时间到了之后就发布,而是这个key在Redis中被清理之后,也就是真正被删除之后才会发布。所以根据redis过期key的两种清除策略

惰性清除。当这个key过期之后,访问时,这个Key才会被清除
定时清除。后台会定期检查一部分key,如果有key过期了,就会被清除
2.丢消息

Redis过期监听实现的发布订阅模式没有持久化机制,当消息发布到某个channel之后,如果没有客户端订阅这个channel,那么这个消息就丢了,并不会像MQ一样进行持久化,等有消费者订阅的时候再给消费者消费。

3.Redis的发布订阅模式消息消费只有广播模式

假如有多个服务实例同时监听redis key过期,那么他们都会得到通知并进行重复消费,需要额外增加lock防止重复消费

4.监听范围大

只能指定/不指定监听哪个库的所有的key,导致所有的key发生了事件都会被通知给消费者。没有针对性。

解决方式

一、redis改成单节点,弊端集群环境下不能保证redis高可用,也可能会有未知的问题;

二、因为这个bug是目前公认的坑,官方也不建议生产环境下使用,所以就果断放弃这种实现方式,寻求其它解决方案,经过调查,最后决定使用redission下的延迟队列,这个方式是经过很多成熟的场景下验证过,直接贴代码:

1、添加maven依赖

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.11.0</version>
</dependency>

2、延迟队列工具类 

import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RBlockingDeque;
import org.redisson.api.RDelayedQueue;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * @author lvkaifeng
 * @Description: 延迟队列工具
 */
@Slf4j
@Component
public class RedisDelayQueueUtil {
    private static final Logger logger = LoggerFactory.getLogger(RedisDelayQueueUtil.class);
    @Autowired
    private RedissonClient redissonClient;

    public <T> void addDelayQueue(T value, LocalDateTime endTime, String queueCode) {
        long seconds = Duration.between(LocalDateTime.now(), endTime).getSeconds();
        if (seconds > 0) {
            addDelayQueue(value, seconds, TimeUnit.SECONDS, queueCode);
        }
    }
    public <T> void addDelayQueue(T value, long delay, String queueCode) {
        addDelayQueue(value, delay, TimeUnit.SECONDS, queueCode);
    }
    /**
     * 添加延迟队列
     * @param value 队列值
     * @param delay 延迟时间
     * @param timeUnit 时间单位
     * @param queueCode 队列键
     * @param <T> 泛型
     */
    public <T> void addDelayQueue(T value, long delay, TimeUnit timeUnit, String queueCode){
        try {
            RBlockingDeque<Object> blockingDeque = redissonClient.getBlockingDeque(queueCode);
            RDelayedQueue<Object> delayedQueue = redissonClient.getDelayedQueue(blockingDeque);
            delayedQueue.offer(value, delay, timeUnit);
            logger.info("(添加延时队列成功) 队列键:{},队列值:{},延迟时间:{}", queueCode, value, timeUnit.toSeconds(delay) + "秒");
            //释放队列
            delayedQueue.destroy();
        } catch (Exception e) {
            logger.error("(添加延时队列失败) {}, value {}", e.getMessage(), value);
            throw new RuntimeException("(添加延时队列失败)");
        }
    }


    /**
     * 获取延迟队列
     * @param queueCode 队列主键
     * @param <T> 泛型
     * @return
     * @throws InterruptedException
     */
    public <T> T getDelayQueue(String queueCode) throws InterruptedException {
        RBlockingDeque<Map> blockingDeque = redissonClient.getBlockingDeque(queueCode);
        //避免消息伪丢失(应用重启未消费),官网推荐
        redissonClient.getDelayedQueue(blockingDeque) ;
        T value  = (T) blockingDeque.take();
        return value;
    }


    public boolean isContain(@NonNull Object o, @NonNull String queueCode) {
        if (StringUtils.isBlank(queueCode) || Objects.isNull(o)) {
            return false;
        }
        try {
            RBlockingDeque<Object> blockingDeque = redissonClient.getBlockingDeque(queueCode);
            RDelayedQueue<Object> delayedQueue = redissonClient.getDelayedQueue(blockingDeque);
            boolean flag = delayedQueue.contains(o);
            if(flag){
                logger.info("(存在延时队列对应的值) 队列键:{},队列值:{}", queueCode, o);
            }
            delayedQueue.destroy();
            return flag;
        } catch (Exception e) {
            logger.error("(判断存在延时队列异常) 队列键:{},队列值:{},错误信息:{}",queueCode, o, e.getMessage());
            throw new RuntimeException("(判断存在延时队列异常)");
        }

    }

    public boolean removeDelayedQueue(@NonNull Object o, @NonNull String queueCode) {
        if (StringUtils.isBlank(queueCode) || Objects.isNull(o)) {
            return false;
        }
        try {
            RBlockingDeque<Object> blockingDeque = redissonClient.getBlockingDeque(queueCode);
            RDelayedQueue<Object> delayedQueue = redissonClient.getDelayedQueue(blockingDeque);
            boolean flag = false;
            if (delayedQueue.contains(o)) {
                flag = delayedQueue.remove(o);
            }
            if(flag){
                logger.info("(删除延时队列保证唯一性) 队列键:{},队列值:{}", queueCode, o);
            }
            delayedQueue.destroy();
            return flag;
        } catch (Exception e) {
            logger.error("(删除延时队列异常) 队列键:{},队列值:{},错误信息:{}",queueCode, o, e.getMessage());
            throw new RuntimeException("(删除延时队列异常)");
        }

    }
}

3、key过期监听回调类

import org.redisson.api.RBlockingDeque;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

/**
 * @author lvkaifeng
 * @description: redission 延迟队列处理过期回调事件
 */
@Component
public class RedisDelayHandle {
    private static final Logger logger = LoggerFactory.getLogger(RedisDelayHandle.class);
    @Autowired
    private RedissonClient redissonClient;

    @Autowired
    private RedisService testService;

    @PostConstruct
    public void listener() {
        logger.info("进入延迟队列中的信息");
        new Thread(()->{
            while (true){
                RBlockingDeque<String> blockingDeque = redissonClient.getBlockingDeque("timeOutCarriageDelayDeque");
                //这行必须有
                RDelayedQueue<String> delayedQueue = redissonClient.getDelayedQueue(blockingDeque);
                String msg  = null;
                try {
                    msg = blockingDeque.take();
                    logger.info("执行延迟队列中的信息:{}", msg);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //自己的业务代码
                testService.handleIncompleteCarriageData(msg);
            }
        }).start();
    }
}

好了,就这么多了,redission的配置还是用你之前使用的就可以,这个使用之后就完美解决了,如果有其他问题,欢迎评论一起探讨!

出现这个错误的原因是在导入seaborn包时,无法从typing模块中导入名为'Protocol'的对象。 解决这个问题的方法有以下几种: 1. 检查你的Python版本是否符合seaborn包的要求,如果不符合,尝试更新Python版本。 2. 检查你的环境中是否安装了typing_extensions包,如果没有安装,可以使用以下命令安装:pip install typing_extensions。 3. 如果你使用的是Python 3.8版本以下的版本,你可以尝试使用typing_extensions包来代替typing模块来解决该问题。 4. 检查你的代码是否正确导入了seaborn包,并且没有其他导入错误。 5. 如果以上方法都无法解决问题,可以尝试在你的代码中使用其他的可替代包或者更新seaborn包的版本来解决该问题。 总结: 出现ImportError: cannot import name 'Protocol' from 'typing'错误的原因可能是由于Python版本不兼容、缺少typing_extensions包或者导入错误等原因造成的。可以根据具体情况尝试上述方法来解决该问题。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [ImportError: cannot import name ‘Literal‘ from ‘typing‘ (D:\Anaconda\envs\tensorflow\lib\typing....](https://blog.youkuaiyun.com/yuhaix/article/details/124528628)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值