背景:
最近有个需求是跟第三方对接事故报案,事故报案单状态是由kafka来实现发布订阅的。具体是这样的,事故报案双方都可以发起,如果是第三方发起的事故报案,我们接到kafka推送的状态后,需要创建一张事故报案单,如果是我们发起的事故报案,第三方也需要创建一张事故报案单。因为我们创建事故报案单后,还有后续流程要走,导致当消费该报案单第一个状态的时候,事故报案单还没有创建完成,其结果就是该消息没有被成功消费,导致事故报案单缺少了第一个状态节点。因为状态消息实时性要求不高,所以我们决定用延时队列来解决这个问题。同时为了保证服务重启后,或者服务器宕机时,保存在延时队列中的数据不会丢失,最终选择使用Redisson中的延时队列。
在此先不讨论其实现原理,我们先来实现这个延时队列。
1.引入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>2.8.2</version>
</dependency>
2.添加配置 (默认可以直接注入,不用再自行配置。由于依赖版本不同,自行配置可能会报错)
@Configuration
public class RedissonConfiguration {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Bean
public RedissonClient redissonClient(){
Config config = new Config();
SingleServerConfig singleServerConfig = config.useSingleServer();
String addr = host+":"+port;
singleServerConfig.setAddress(addr);
return Redisson.create(config);
}
}
3.定义延时队列名称枚举类 (后续可以添加其它延时队列名称)
/**
* 延时队列名称枚举
*/
public enum DelayQueueNameEnum {
DELAY_QUEUE_TYPE("DELAY_QUEUE_TYPE");
private String name;
DelayQueueNameEnum(String name){}
}
4.延时任务执行器接口,业务实现该接口,可以实现自己的业务逻辑
/**
* 延时任务执行器接口
* @param <T>
*/
public interface RedisDelayQueueExecute<T> {
/**
* 回调方法
* @param t
*/
void execute(T t);
}
5.具体业务实现
@Component(value = "ADelayQueueExecuteImpl")
public class ADelayQueueExecuteImpl implements RedisDelayQueueExecute<Map>{
@Autowired
private AService aService;
@Async
@Override
public void execute(Map obj) {
try {
// 延时任务处理实现
aService.processStatus(obj);
} catch (Exception e) {
log.error(e.getMessage(),e);
}
}
}
6.延时队列服务类
/**
* redis延迟队列服务
*/
@Slf4j
@Component
public class RedisDelayJobService {
@Autowired
private RedissonClient redissonClient;
/**
* 添加到消息队列
* @param value 添加到队列的对象
* @param delay 延迟时间
* @param timeUnit 时间单位
* @param queueName 队列名称
* @param <T>
*/
public <T> void addQueue(T value , long delay, TimeUnit timeUnit,String queueName){
try {
log.info("添加到延时队列【{}】【{}】【{}】",value,delay,queueName);
RBlockingDeque<Object> blockingDeque = redissonClient.getBlockingDeque(queueName);
RDelayedQueue<Object> delayedQueue = redissonClient.getDelayedQueue(blockingDeque);
delayedQueue.offer(value,delay,timeUnit);
} catch (Exception e) {
log.error("添加到延时队列失败:"+e.getMessage(),e);
throw new RuntimeException("添加到延时队列失败");
}
}
/**
* 取值
*/
public <T> T take(String queueName) throws InterruptedException {
RBlockingDeque<Map> blockingDeque = redissonClient.getBlockingDeque(queueName);
T value = (T) blockingDeque.take();
return value;
}
}
7.将满足条件的kafka消息添加到延时队列,只需要调用addQueue()方法即可
8.取出延时任务,处理该延时任务
@Slf4j
@Component
public class RedisDelayQueueRunner implements ApplicationRunner {
/**
* 注入延时任务执行器,指定具体业务实现类
*/
@Resource(name = "ADelayQueueExecuteImpl")
private RedisDelayQueueExecute ADelayQueueExecute;
@Autowired
private RedisDelayJobService redisDelayJobService;
@Override
public void run(ApplicationArguments args) {
String queueName = DelayQueueNameEnum.DELAY_QUEUE_TYPE.name();
//DESC 开启新的线程(常驻)处理延迟队列
new Thread(()->{
while (true){
try {
Map value = redisDelayJobService.take(queueName);
if (value!=null){
ADelayQueueExecute.execute(value);
}
} catch (InterruptedException e) {
log.error("延迟队列取值中断异常:"+e.getMessage(),e);
}
}
}).start();
}
}
到此就实现了延时任务的处理。如有错误之后,欢迎指正。
记实战过程中的疑问
在测试过程中发现一个问题,当延时任务从队列中取出之后,在处理该延时任务时(线程未结束),会阻塞延时队列的添加操作。
希望有大神能够解答。