前言
在开发过程中,有很多场景都需要用到延迟队列来解决。目前支持延迟队列的中间件也不少,RabbitMQ、kafka、RocketMq等。但是有时我们项目规模可能比较小,或者客户给的资源比较少。那么利用Redis也可以实现延迟队列的功能。
原理
利用Redis来实现延迟队列的主要思路是借助Redis的Sorted Set数据类型来实现。 具体做法是将任务的执行时间作为分数(score),任务的内容作为值(value),将任务按照执行时间排序存储在有序集合中。然后周期性地检查有序集合中的任务,根据当前时间和任务的执行时间来决定是否执行任务。 当需要添加新的延迟任务时,只需将任务的执行时间和内容添加到有序集合中即可。当然,你可能需要一个后台进程或定时任务来不断地检查有序集合,以执行到期的任务。
优势和适用场景
Redis 实现的延迟队列具有以下优势
- 性能极高:基于 Redis 的内存数据库特性和异步 I/O 模型,延迟队列的性能非常高,可以轻松处理大量延迟任务。
- 高可用性:Redis 的多节点部署和复制机制可以有效地保证系统的高可用性,避免单点故障。
- 扩展性强:由于 Redis 的数据结构非常灵活,支持多种数据类型和数据结构,因此可以根据业务需要更加方便地扩展和修改队列
Redis 实现的延迟队列主要适用于以下场景:
- 需要延迟处理任务的业务场景,例如定时发送邮件、短信、推送通知等。
- 需要高性能和高可用性的业务场景,例如大规模分布式系统中的任务调度和消息处理等
代码实现
一、依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.15.0</version>
</dependency>
二、代码
TaskWarningDelayQueue
类实现了一个使用延迟队列(RDelayedQueue
)和阻塞队列(RBlockingQueue
)来处理延迟任务的逻辑。其中的线程池那块是用来进行消费任务的,再往下依次是添加和删除任务的方法。RedissonDelayQueueConfigure
类定义了两个 @Bean
方法,这些方法用于在 Spring 容器中创建和配置 RBlockingQueue
和 RDelayedQueue
的实例。创建了两个可重用的组件,这些组件在 Spring 容器中可以被其他类通过 @Autowired
或构造器注入的方式引用。这样做的好处是,可以在多个地方重用这些队列实例,而不需要在每个使用的地方都重新创建它们。这有助于减少代码的重复,并使得组件之间的依赖关系更加清晰和易于管理。
@Slf4j
@Component
@RequiredArgsConstructor
public class TaskWarningDelayQueue {
private final RDelayedQueue<MqDelayMessage> delayedQueue;
private final RBlockingQueue<MqDelayMessage> blockingQueue;
private final TaskTypeWarningService taskTypeWarningService;
//private final Map<String, AbstractInfectionHandler> map;
@PostConstruct
@SuppressWarnings("InfiniteLoopStatement")
public void init() {
//单线程线程池
ThreadFactory factory = new CustomizableThreadFactory("emergency-delay-");
ExecutorService executor = new ThreadPoolExecutor(1, 1, 5L, TimeUnit.MINUTES,
new LinkedBlockingQueue<>(1024), factory, new ThreadPoolExecutor.CallerRunsPolicy());
executor.submit(() -> {
while (true) {
try {
// 消费
MqDelayMessage mqDelayMessage = blockingQueue.take();
log.info("receive delay task:{}", JSON.toJSONString(mqDelayMessage));
taskTypeWarningService.warning(mqDelayMessage);
} catch (InterruptedException e) {
log.error("CountryResultDelayQueue occur error", e);
Thread.currentThread().interrupt();
}
}
});
}
public void offerTask(MqDelayMessage task, long milliseconds) {
log.info("add delay task:{},delay time:{}", JSON.toJSONString(task), milliseconds);
delayedQueue.offer(task, milliseconds, TimeUnit.MILLISECONDS);
}
public void deleteTask(MqDelayMessage task) {
log.info("delete delay task:{},delay time:{}s", JSON.toJSONString(task));
delayedQueue.remove(task);
/*if (removed) {
// 如果成功删除,则重新添加到队列中,设置新的延迟时间
delayedQueue.offer(task, seconds, TimeUnit.MINUTES);
} else {
// 如果元素不存在或已被消费,则无法修改延迟时间
System.out.println("Element not found or already consumed.");
}*/
}
@Configuration
static class RedissonDelayQueueConfigure {
@Bean
public RBlockingQueue<MqDelayMessage> blockingQueue(RedissonClient redissonClient) {
return redissonClient.getBlockingQueue("QUERY-TASK_WARNING_DELAY1");
}
@Bean
public RDelayedQueue<MqDelayMessage> delayedQueue(RBlockingQueue<MqDelayMessage> blockingQueue, RedissonClient redissonClient) {
return redissonClient.getDelayedQueue(blockingQueue);
}
}
}
下方就没啥好讲,设置延时队列。就相当于是生成者。主要就是offTask方法
private void setWarning(TaskVo taskVo) {
// 设置延时队列/获取该任务类型有几种预警设置
List<TaskTypeWarning> taskTypeWarnings = taskTypeWarningService.lambdaQuery()
.eq(TaskTypeWarning::getTaskTypeId, taskVo.getTaskTypeId()).list();
if (CollectionUtils.isNotEmpty(taskTypeWarnings)) {
taskTypeWarnings.forEach(x -> {
MqDelayMessage mqDelayMessage = new MqDelayMessage();
mqDelayMessage.setTaskId(taskVo.getId());
mqDelayMessage.setType(x.getType());
long timeInterval = x.getTimeInterval() * 3600 * 1000;
// 预警类型:1、未阅读 2、未处理 3、将超时 4、已超时
if (Constants.NUMBER_ONE.equals(x.getType())) {
taskWarningDelayQueue.offerTask(mqDelayMessage, timeInterval);
} else if (Constants.NUMBER_THREE.equals(x.getType())) {
LocalDateTime endTime = LocalDateTime.ofInstant(taskVo.getPlanCompleteTime().toInstant(),
ZoneId.systemDefault());
Duration duration = Duration.between(LocalDateTime.now(), endTime);
taskWarningDelayQueue.offerTask(mqDelayMessage, duration.toMillis() - timeInterval);
} else if (Constants.NUMBER_FOUR.equals(x.getType())) {
LocalDateTime endTime = LocalDateTime.ofInstant(taskVo.getPlanCompleteTime().toInstant(),
ZoneId.systemDefault());
Duration duration = Duration.between(LocalDateTime.now(), endTime);
taskWarningDelayQueue.offerTask(mqDelayMessage, duration.toMillis() + timeInterval);
}
});
}
}