基于Redis的延时任务队列

一.背景

  笔者先前遇到了一个订单超时关闭的问题,首先就排除了:起一任务轮询数据库的方案,太耗资源,也增加DB的负担。查阅了一些资料,RocketMQ可以实现延时消息的功能,下单的时候推一个延时24小时的MQ消息,24小时后系统再次收到这个消息时检测订单是否是待付款状态,如果是,则关闭订单。由于笔者的后台服务使用的是阿里云ONS(基于RocketMQ),于是就这样完成了需求。
  但是,如果不幸的是你的服务没有使用RocketMQ,如ActiveMQ等不支持延时消息的消息中间件怎么办?可以基于Redis的SkipList(跳跃表), Redis Client的命令是zset(sorted set)、zadd 等。简而言之,就是在set key的时候,除了value还可以添加一个score, 当你取出key中取出value时可以指定取出指定范围score,本篇博文就是基于Redis跳跃表实现的。
  本文参考了另外一篇博文:有赞延迟队列设计

二.整体构架

设计思路

  1. 用redis zset存放延时任务队列,这个队列只放Delay Queue任务id
  2. 用一个队列queue存放就绪任务队列,这个队列也只放任务id
  3. 任务的详细信息通过一个redis map结构存储,以任务id为key, 任务详情为value
  4. 任何详情包含任务的id、任务topic、执行时刻(score)、失败重试次数、失败后重试间隔、任务内容等
  5. 一个线程(也可以多个)从Delay Queue轮询当前时刻需要执行的任务,放置到Ready Queue
  6. 另外一个组线程轮询Ready Queue,交给指定Topic的Job Handler去执行任务,当任务执行成功或达到失败重试次数后,从Job Detail Pool中删除对应id的任务。如果任务失败,则重新放到Delay Queue,消耗一次重试次数

构架图如下:

在这里插入图片描述

三.代码类图

DelayJob(任务详情)

public class DelayJob<T> {
    /**
     * job id
     */
    private String id;
    /**
     * 消息类型
     */
    private String topic;
    /**
     * 任务执行时间(时间戳:精确到秒)
     */
    private Long execTime;
    /**
     * 重试次数
     */
    private Integer retryTimes = 0;
    /**
     * 消费失败,重新消费间隔(单位秒)
     * 默认0L, 消费失败不重新消费
     */
    private Long retryDelay = 0L;
    /**
     * 消息体
     */
    private T body;
}

在这里插入图片描述

WaitQueue(延时队列)

在这里插入图片描述

ReadyQueue(就绪队列)

在这里插入图片描述

Scanner(扫描线程,轮询任务)

在这里插入图片描述

四.使用

Maven依赖

<dependency>
    <groupId>com.github.rxyor</groupId>
    <artifactId>carp-distributed</artifactId>
    <version>1.0.4</version>
</dependency>

spring mvc中使用

xml配置bean示例:

    <bean id="fastJsonCodec" class="com.github.rxyor.redis.redisson.codec.FastJsonCodec"/>

    <bean id="delayRedisConfig"
        class="com.github.rxyor.redis.redisson.config.RedisConfig">
        <property name="host" value="${redis.host}"/>
        <property name="port" value="${redis.port}"/>
        <property name="password" value="${redis.password}"/>
        <property name="database" value="${redis.database}"/>
        <property name="codec" ref="fastJsonCodec"/>
    </bean>

    <bean id="redisDelayJobConfig"
        class="com.github.rxyor.distributed.redisson.delay.config.DelayConfig">
        <property name="appName" value="${redis.app}"/>
    </bean>

    <bean id="scanWrapper" class="com.github.rxyor.distributed.redisson.delay.core.ScanWrapper"
        init-method="initAndScan" destroy-method="destroy">
        <property name="delayConfig" ref="redisDelayJobConfig"/>
        <property name="redisConfig" ref="delayRedisConfig"/>
        <property name="handlerList">
            <list>
                <bean class="com.github.rxyor.distributed.redisson.delay.handler.LogJobHandler">
                    <property name="topic" value="girl"/>
                </bean>
            </list>
        </property>
    </bean>

    <bean id="delayClientProxy" factory-bean="scanWrapper" factory-method="getDelayClientProxy"/>

定义一个controller用于测试:

@Api(value = "延时队列")
@RestController
@AllArgsConstructor
@RequestMapping("/delay")
public class DelayJobController {

    private final DelayClientProxy delayClientProxy;

    @ApiOperation(value = "添加一个延时任务", httpMethod = "POST")
    @PostMapping("/job/add")
    @ResponseBody
    public R addDelayJob(@RequestBody DelayJob<Map<String, Object>> delayJob) {
    	log.info("received a job ,current time is:{}", System.currentTimeMillis() / 1000);
    	//没有执行时间,设置为当前时间延时10秒后执行
        if (delayJob.getExecTime() == null) {
            delayJob.setExecTime(System.currentTimeMillis() / 1000 + 10);
        }
        delayClientProxy.offer(delayJob);
        return RUtil.success(delayJob);
    }
}

启动我们的spring项目,发送一个请求,请求参数如下:

{
  "body": {"girl":"I Like You"},
  "retryDelay": 10,
  "retryTimes": 3,
  "topic": "girl"
}

在这里插入图片描述
由于我们一个LogJobHandler,大概10秒后会打印Job的内容,如图所示:
在这里插入图片描述
这里两条打印是因为,LogJobHandler 执行了一次System.out.printlnLog, LogJobHandler只是一个示例的Handler,你可以自定义JobHandler进行业务操作。

spring boot中使用

配置bean:

@Configuration
public class DelayQueueConfig {

    @Value("${spring.redis.host}")
    private String redisHost;

    @Value("${spring.redis.port}")
    private Integer port;

    @Value("${spring.redis.database}")
    private Integer database;

    @Value("${spring.redis.password}:''")
    private String redisPassword;

    @Bean
    public RedisConfig redisConfig() {
        RedisConfig redisConfig = new RedisConfig();
        redisConfig.setHost(redisHost);
        redisConfig.setPort(port);
        redisConfig.setPassword(redisPassword);
        redisConfig.setDatabase(database);
        return redisConfig;
    }

    @Bean
    public com.github.rxyor.distributed.redisson.delay.config.DelayConfig delayConfig() {
        com.github.rxyor.distributed.redisson.delay.config.DelayConfig delayConfig = new com.github.rxyor.distributed.redisson.delay.config.DelayConfig();
        delayConfig.setAppName("carp-boot");
        return delayConfig;
    }

    @Bean
    public List<JobHandler> handlerList() {
        List<JobHandler> handlerList = new ArrayList<>(4);
        LogJobHandler logJobHandler = new LogJobHandler();
        logJobHandler.setTopic("girl");
        handlerList.add(logJobHandler);
        return handlerList;
    }

    @Bean(initMethod = "initAndScan", destroyMethod = "destroy")
    public ScanWrapper scanWrapper() {
        ScanWrapper scanWrapper = new ScanWrapper();
        scanWrapper.setRedisConfig(redisConfig());
        scanWrapper.setDelayConfig(delayConfig());
        scanWrapper.setHandlerList(handlerList());
        return scanWrapper;
    }

    @Bean
    public DelayClientProxy delayClientProxy() {
        return scanWrapper().getDelayClientProxy();
    }
    
}

五.源码地址

git工程中包含源码以及上述示例工程:
https://github.com/xxxx/xxxx.git
捐助后告知(QQ:1256670189):
在这里插入图片描述
示例代码:https://download.csdn.net/download/liuyanglglg/11377595

发布了17 篇原创文章 · 获赞 9 · 访问量 6519

猜你喜欢

转载自blog.csdn.net/liuyanglglg/article/details/96299435