背景
最近有一个需求,实现达到预设时长没有收全数据,就触发转发请求,我就想起了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的配置还是用你之前使用的就可以,这个使用之后就完美解决了,如果有其他问题,欢迎评论一起探讨!