整体介绍
工作中是否碰到过需要延迟一定时间后再执行的任务?通常的做法是将延迟任务持久化至数据库,然后通过定时任务轮询扫描,处理符合条件的任务。通常来说,此类场景主要考虑2个要素:
- 延迟执行的时延,和轮询的时间间隔有关
- 任务的处理性能,和整体的处理架构有关 这里提供另外一种轻量级的实现思路:基于 redis 有序集合实现延迟队列,提供任务添加、查询、删除、处理能力。并封装成spring-boot starter组件,供应用方低成本接入。 使用redis从根本上避免了重复处理任务的问题,不必考虑分布式锁等其他互斥方案,降低方案复杂度。
软件架构
整体架构介绍如下
- 基于 redis 有序集合实现延迟队列,提供添加、拉取、删除任务基础功能,见 DelayQueue 接口
- 延迟队列消费采用异步线程模式,定时拉取 redis 有序集合,整体调度方式见 DelayQueuePollScheduler
- 延时队列中的任务处理方式采用 spring 自带的事件模式,事件处理可配置异步、同步模式
- 使用方通过实现 DelayQueuePollEventHandler 接口,定制事件处理具体逻辑,即策略模式思想
模块架构图如下
重点设计
DelayQueue
客户端 SDK,提供延迟队列的基本操作,接口DelayQueue定义如下,提供基于redis有序集合的实现 ZSetDelayQueue
java
public interface DelayQueue<T> {
/**
* 往延迟队列中添加任务, 如果队列容量满,则直接返回 false
*
* @param task 任务
* @param topic 主题,区分不同的延迟队列
* @param delayTime 延迟时间,即相对值
* @param timeUnit 延迟时间单位
* @return true 表示添加成功,false 表示添加失败
*/
boolean add(T task, String topic, int delayTime, TimeUnit timeUnit);
/**
* 往延迟队列中添加任务, 如果队列容量满,则直接返回 false
*
* @param task 任务
* @param topic 主题,区分不同的延迟队列
* @param executeTime 任务执行时间,即绝对值
* @return true 表示添加成功,false 表示添加失败
*/
boolean add(T task, String topic, Date executeTime);
/**
* 获取到期任务列表
*
* @param topic 主题,区分不同的延迟队列
* @param expireTime 到期时间
* @return 任务列表
*/
List<T> poll(String topic, Long expireTime);
}
应用方若要使用默认实现ZSetDelayQueue,直接注入,具体代码如下
java
@Autowired(required = false)
@Qualifier("zSetDelayQueue")
private DelayQueue<String> delayQueue;
DelayQueuePollEventHandler
延迟队列组件提供DelayQueuePollEventHandler接口供应用方具体实现处理逻辑。接口定义如下
java
public interface DelayQueuePollEventHandler {
/**
* 事件处理
*
* @param pollEvent 轮询事件
*/
void handle(DelayQueuePollEvent pollEvent);
/**
* 支持处理的延迟队列主题集合
*
* @return 主题集合
*/
Set<String> getSupportedTopics();
其中:
- handle方法即为任务具体处理逻辑
- getSupportedTopics方法返回此handler支持处理的topic集合 组件使用策略模式,自动构建 topic-handler 的映射关系,根绝具体topic选取符合要求的handler进行处理。核心逻辑如下:
java
/**
* 延迟队列处理器
*/
@Bean
public Map<String, DelayQueuePollEventHandler> delayQueueTopic2Handler(@Autowired List<DelayQueuePollEventHandler> eventHandlerList) {
// 获取所有业务应用定义的handler bean
Map<String, DelayQueuePollEventHandler> delayQueueTopic2Handler = Maps.newHashMap();
for (DelayQueuePollEventHandler handler : eventHandlerList) {
// 遍历所有handler,收集topic集合
handler.getSupportedTopics().forEach(t -> delayQueueTopic2Handler.put(t, handler));
}
// 返回 topic-handler 映射
return delayQueueTopic2Handler;
}
DelayQueuePollScheduler
延迟队列任务的定时拉取采用 ScheduledThreadPoolExecutor 实现,corePoolSize固定等于1,线程池的数量由配置 DelayQueuePollSchedulerConfig.size 决定。默认情况下,size=1,即一个线程池串行循环拉取所有topic的延迟队列任务(其中topic集合通过调用所有DelayQueuePollEventHandler实现类的getSupportedTopics实现)。由上面的模块图,定时拉取到任务后,可以通过同步或者异步的方式发送事件,由具体的handler(2中的策略模式实现topic维度路由)实现处理。如果这里使用异步事件模式,那么拉取过程耗时非常短,建议一个线程池循环处理所有topic即可。
初始化核心代码如下:
java
@PostConstruct
public void init() {
if (CollectionUtils.isEmpty(delayQueueTopics)) {
return;
}
Assert.notNull(schedulerConfig, "DelayQueuePollSchedulerConfig cannot be null!");
int schedulerSize = schedulerConfig.getSize();
Assert.isTrue(schedulerSize > 0, "scheduler size must > 0");
List<List<String>> topicGroupList = ListUtils.partitionByFixedGroup(delayQueueTopics, schedulerSize);
createExecutors();
int idx = 0;
for (List<String> topicGroup : topicGroupList) {
ScheduledExecutorService executorService = scheduledExecutorServices.get(idx);
log.info("ScheduledExecutorService idx: {}, topic list: {}", idx, JsonUtils.toJson(topicGroup));
Runnable command = () -> topicGroup.forEach(t -> {
BatchDelayQueueTask task =
BatchDelayQueueTask.builder().topic(t).expireTime(System.currentTimeMillis()).build();
try {
delayQueueConsumer.accept(task);
} catch (Exception e) {
delayQueueConsumerExceptionHandler.handle(e, task);
}
});
executorService.scheduleAtFixedRate(command, schedulerConfig.getInitialDelayInMillis(),
schedulerConfig.getPeriodInMillis(), TimeUnit.MILLISECONDS);
idx++;
}
}
/**
* 创建定时调度 ScheduledExecutorService
*/
private void createExecutors() {
scheduledExecutorServices = Lists.newArrayList();
for (int i = 0; i < schedulerConfig.getSize(); i++) {
String threadName = "DelayQueuePollExecutor" + i + "-%d";
// 核心1个线程
scheduledExecutorServices.add(new ScheduledThreadPoolExecutor(1,
new ThreadFactoryBuilder().setNameFormat(threadName).setDaemon(true).build(),
new ThreadPoolExecutor.DiscardPolicy()));
}
log.info("Create scheduledExecutorServices size: {}", scheduledExecutorServices.size());
}
DelayQueuePollEventAsyncConfig
基于Spring自带事件机制,实现异步模式,直接使用SimpleAsyncTaskExecutor
,具体配置如下
java
/**
* 延时队列轮询事件发送模式,默认同步
*/
@Bean(name = "applicationEventMulticaster")
public ApplicationEventMulticaster simpleApplicationEventMulticaster(
@Autowired(required = false) @Qualifier("delayQueuePollEventErrorHandler") ErrorHandler errorHandler) {
SimpleApplicationEventMulticaster eventMulticaster = new SimpleApplicationEventMulticaster();
ThreadPoolProperties properties = delayQueuePollEventListenerPoolConfig();
Executor taskExecutor = buildExecutor(properties);
eventMulticaster.setTaskExecutor(taskExecutor);
eventMulticaster.setErrorHandler(errorHandler);
return eventMulticaster;
}
如何使用
maven配置
xml
<!-- 引入leporidae依赖模块,做为统一版本管理,通常在父模块引入 -->
<properties>
<leporidae.version>1.0.1</nmp-starters.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.gitee.skyarthur1987</groupId>
<artifactId>leporidae</artifactId>
<version>${leporidae.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!-- 引入leporidae-delay-queue, 通常在实际需要使用的子模块引入 -->
<dependencies>
<dependency>
<groupId>io.gitee.skyarthur1987</groupId>
<artifactId>leporidae-delay-queue</artifactId>
</dependency>
</dependencies>
配置信息
yaml
leporidae:
# 全局 redis 配置
redis:
host: 127.0.0.1
port: 6378
lettuce:
pool:
min-idle: 5
max-idle: 10
max-active: 8
max-wait: 1ms
shutdown-timeout: 100ms
delay-queue:
# 延迟队列组件总开关
enable: true
# 延迟队列使用redis zset实现,组件redis配置,优先于全局配置
redis:
host: 127.0.0.1
port: 6478
lettuce:
pool:
min-idle: 5
max-idle: 10
max-active: 8
max-wait: 1ms
shutdown-timeout: 100ms
# 是否开启延迟队列消费配置,默认false。针对消费端,无需开启消费能力。对于消费端,后面的poll-executor、poll-event-async均无需配置
consumer.enable: false
# 可选(有默认值),延迟队列定时轮询(请求redis,拉取任务)配置
poll-executor:
# 定时轮询周期,单位:毫秒,默认 5000(5秒)
periodInMillis: 5000
# 初始延迟时间,单位:毫秒,默认 5*1000*60(5分钟)
initialDelayInMillis: 5000
# 轮询的线程池数量,即并发度,默认 1。仅在多topic延迟队列情况下设置有意义
size: 1
# 可选,默认同步处理,延迟队列任务异步处理配置
poll-event-async:
# 可选,默认false,即同步处理
enable: true
# 异步处理开启后,线程池配置
pool:
# 可选,指定已有线程池名称,默认不指定即会更具如下配置创建新线程池
executor-bean-name: testTaskExecutor
# 如果指定executor-bean-name,后续线程池配置无意义
core-pool-size: 8
maximum-pool-size: 16
keep-alive-in-seconds: 30
blocking-queue-size: 500
pool-name: delayQueuePollEventListener-pool-%d
starter使用
1.提供接口实现延迟队列任务添加和查询,需要在业务应用中注入 DelayQueue,代码如下
java
@Autowired(required = false)
@Qualifier("zSetDelayQueue")
private DelayQueue<String> delayQueue;
2.延迟队列任务处理,通过实现 DelayQueuePollEventHandler 接口,接口定义如下:其中handler方法即为具体处理逻辑;getSupportedTopics 即此handler支持的延迟队列topic集合,组件框架会自动探测handler,建立 topic->handler的映射,即策略模式,应用方无需感知。具体例子,在单测中也有体现(建议维护一个枚举 DeLayQueueTaskType)
java
// DelayQueueTopic.java
@Getter
public enum DelayQueueTopic {
TASK_PRINT("taskPrint", TaskDto.class);
private final String topic;
private final Class<?> elementClz;
DelayQueueTopic(String topic, Class<?> elementClz) {
this.topic = topic;
this.elementClz = elementClz;
}
}
// EventCollectTestHandler.java
@Service
public class EventCollectTestHandler implements DelayQueuePollEventHandler {
@Override
public void handle(DelayQueuePollEvent pollEvent) {
System.out.println("EventCollectTestHandler handle event.¬");
}
@Override
public Set<String> getSupportedTopics() {
return Sets.newHashSet(DelayQueueTopic.TASK_PRINT.getTopic());
}
}
3.支持自定义延迟队列消费异常处理,实现 DelayQueueConsumerExceptionHandler 接口,组件默认提供了一种实现(打印异常栈并发送如流报警),基本能满足大多数场景,核心代码如下
java
public interface DelayQueueConsumerExceptionHandler {
/**
* 具体处理方法
*
* @param e 异常
* @param task 一批延迟队列任务
*/
void handle(Throwable e, BatchDelayQueueTask task);
}
4.事件异步处理线程池初始化:如果应用方已定义 Executor,可以通过leporidae.delay-queue.poll-event-async.pool.executor-bean-name配置bean名称,指定具体要使用的executor,否则会默认根据pool配置创建executor,见如下代码
java
private Executor buildExecutor(ThreadPoolProperties properties) {
String executorBeanName = properties.getExecutorBeanName();
if (StringUtils.hasText(executorBeanName)) {
log.info("ApplicationEventMulticaster executor: {}", executorBeanName);
return applicationContext.getBean(executorBeanName, Executor.class);
}
log.info("Init new executor, config: {}", JsonUtils.toJson(properties));
return new ThreadPoolExecutor(properties.getCorePoolSize(),
properties.getMaximumPoolSize(),
properties.getKeepAliveInSeconds(), TimeUnit.SECONDS,
new LinkedBlockingQueue<>(properties.getBlockingQueueSize()),
new ThreadFactoryBuilder().setNameFormat(properties.getPoolName()).setDaemon(true).build(),
new ThreadPoolExecutor.AbortPolicy());
}