基于redis实现的延迟队列

整体介绍

工作中是否碰到过需要延迟一定时间后再执行的任务?通常的做法是将延迟任务持久化至数据库,然后通过定时任务轮询扫描,处理符合条件的任务。通常来说,此类场景主要考虑2个要素:

  1. 延迟执行的时延,和轮询的时间间隔有关
  2. 任务的处理性能,和整体的处理架构有关 这里提供另外一种轻量级的实现思路:基于 redis 有序集合实现延迟队列,提供任务添加、查询、删除、处理能力。并封装成spring-boot starter组件,供应用方低成本接入。 使用redis从根本上避免了重复处理任务的问题,不必考虑分布式锁等其他互斥方案,降低方案复杂度。

软件架构

整体架构介绍如下

  1. 基于 redis 有序集合实现延迟队列,提供添加、拉取、删除任务基础功能,见 DelayQueue 接口
  2. 延迟队列消费采用异步线程模式,定时拉取 redis 有序集合,整体调度方式见 DelayQueuePollScheduler
  3. 延时队列中的任务处理方式采用 spring 自带的事件模式,事件处理可配置异步、同步模式
  4. 使用方通过实现 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());

    }

源码下载

延迟队列实现源码

相关推荐
Q_19284999067 小时前
基于Spring Boot的电影售票系统
java·spring boot·后端
陈无左耳、7 小时前
Spring Boot应用开发实战:从入门到精通
spring boot
烟波人长安吖~7 小时前
【目标跟踪+人流计数+人流热图(Web界面)】基于YOLOV11+Vue+SpringBoot+Flask+MySQL
vue.js·pytorch·spring boot·深度学习·yolo·目标跟踪
顽疲12 小时前
从零用java实现 小红书 springboot vue uniapp (6)用户登录鉴权及发布笔记
java·vue.js·spring boot·uni-app
编程洪同学13 小时前
Spring Boot 中实现自定义注解记录接口日志功能
android·java·spring boot·后端
GraduationDesign14 小时前
基于SpringBoot的蜗牛兼职网的设计与实现
java·spring boot·后端
customer0814 小时前
【开源免费】基于SpringBoot+Vue.JS安康旅游网站(JAVA毕业设计)
java·vue.js·spring boot·后端·kafka·开源·旅游
罗政17 小时前
PDF书籍《手写调用链监控APM系统-Java版》第10章 插件与链路的结合:SpringBoot环境插件获取应用名
java·spring boot·pdf
sin220117 小时前
springboot测试类里注入不成功且运行报错
spring boot·后端·sqlserver
kirito学长-Java18 小时前
springboot/ssm网上宠物店系统Java代码编写web宠物用品商城项目
java·spring boot·后端