背景
项目中有延迟处理任务的的需求,如订单创建完成后提醒用户付款、超时关闭等。比如订单创建完成后,在3min 10min提醒用户进行支付。
技术选型过程不进行讨论,最终是采用Redisson实现。
简单说下Redisson实现的优点:
- 任务保存在redis中,不用担心服务重启导致任务丢失;
- 不用担心服务集群部署的场景下,任务会重复消费,因为redis是单线程处理请求的;
Redisson延迟任务
要实现Redisson延迟任务需要使用两个队列,一个延迟队列作为生产者队列,一个阻塞队列作为消费者队列。
// 阻塞队列是消费者队列
RBlockingQueue consumerQueue = redissonClient.getBlockingQueue("testQuery", new JsonJacksonCodec());
// 延迟队列是生产者队列,任务达到过期时间后会从生产者队列转移到消费者队列
RDelayedQueue producerQueue = redissonClient.getDelayedQueue(consumerQueue);
// 生产者产生任务
producerQueue.offer(new Object, 1, TimeUnit.Minute);
// 消费者消费任务
Object task = consumerQueue.take();
业务实现
定义抽象类
AbstractDelayTask
其中要包含延迟时间和任务类型,这里定义任务类型的目的是为了让消费者在消费时能够找到具体的消费策略,通过任务类型能够使得这个模型适用于任意的延迟消费场景。
public abstract class AbstractDelayTask {
private long timeout;
private TimeUnit timeUnit;
// 任务类型
private Integer taskType;
}
IDelayTaskProcessor
延迟任务处理器,其要声明支持的任务类型。
public interface IDelayTaskProcessor<T extends AbstractDelayTask> {
// 支持的任务类型
TaskTypeEnum supportTask();
// 处理任务
void process(T task);
}
public enum TaskTypeEnum {
Order(1);
TaskTypeEnum(Integer taskType) {
this.taskType = taskType;
}
private Integer taskType;
public Integer getTaskType() {
return taskType;
}
}
生产者消费者逻辑实现
以项目需求所说的订单提醒支付需求为例,我们首先定义任务处理器OrderDelayTaskProcessor
public class OrderDelayTaskProcessor implements IDelayTaskProcessor<OrderDelayTask> {
@Override
public TaskTypeEnum supportTask() {
return TaskTypeEnum.Order;
}
@Override
public void process(OrderDelayTask task) {
}
}
后面就是我们的核心内容,声明生产者、消费者,并且维护消费逻辑、进行任务分发。
我们将所有逻辑都维护在DelayTaskDispatcher中,其负责接收任务和分发任务,其内部维护了生产者和消费者队列,还有任务处理器。
public class DelayTaskDispatcher {
private static final RedissonClient redissonClient;
// 消费队列,出库操作
private static final RBlockingQueue<AbstractDelayTask> consumerQueue;
// 生产队列,入库使用
private static final RDelayedQueue<AbstractDelayTask> producerQueue;
// 不同任务类型对应了不同处理器
private static Map<Integer, IDelayTaskProcessor> taskProcessorByTaskTypeMap;
static {
Config config = new Config();
SingleServerConfig singleServerConfig = config.useSingleServer();
singleServerConfig.setAddress("redis://127.0.0.1:6379");
redissonClient = Redisson.create(config);
// 阻塞队列是消费者队列
consumerQueue = redissonClient.getBlockingQueue("testQuery", new JsonJacksonCodec());
// 延迟队列是生产者队列,任务达到过期时间后会从生产者队列转移到消费者队列
producerQueue = redissonClient.getDelayedQueue(consumerQueue);
// 初始化任务处理器
taskProcessorByTaskTypeMap = new HashMap<>();
OrderDelayTaskProcessor orderDelayTaskProcessor = new OrderDelayTaskProcessor();
taskProcessorByTaskTypeMap.put(orderDelayTaskProcessor.supportTask().getTaskType(), orderDelayTaskProcessor);
}
// 添加任务
public static void offerTask(AbstractDelayTask abstractDelayTask) {
if (!taskProcessorByTaskTypeMap.containsKey(abstractDelayTask.getTaskType())) {
System.out.println("不支持的任务类型");
} else {
producerQueue.offer(abstractDelayTask, abstractDelayTask.getTimeout(), abstractDelayTask.getTimeUnit());
}
}
private DelayTaskDispatcher() {
}
public synchronized static void startDispatcher() {
// 启动消费者线程
new Thread(() -> {
while (true) {
try {
// 阻塞获取任务
AbstractDelayTask task = consumerQueue.take();
// 获取任务对应的处理器
IDelayTaskProcessor iDelayTaskProcessor = taskProcessorByTaskTypeMap.get(task.getTaskType());
if (Objects.isNull(iDelayTaskProcessor)) {
System.out.println("不支持的任务类型");
continue;
}
// 执行处理,这里可以进行优化,任务处理使用单独的线程池,以免阻塞任务的获取
iDelayTaskProcessor.process(task);
} catch (Exception e) {
System.out.println(e);
try {
// sleep 500ms,防止极端情况cpu空转
Thread.sleep(500);
} catch (InterruptedException ex) {
System.out.println(ex);
}
}
}
}, "delayTaskDispatcherThread").start();
}
}
总结
以上就是使用Redisson实现延迟任务的全部过程,其实实现起来并不复杂,因为是第一次使用Redisson的延迟队列,所以验证过程比较长,主要是防止重复消费。
另外Redisson延迟队列的延迟队列实现原理,后续再开个帖子分享下,主要就是使用redis的sorted_set。