基于Redis设计延迟任务管理器

业务场景

我们买火车票或者叫外卖的时候,下完单之后会跳转到支付页面,页面里通常会有一个计时器,要求在指定时间内完成支付,否则订单就会被自动取消。这就是延时任务的一个典型业务场景。分析这个场景,其实最关键的就是如何在订单超时的时候立即触发取消订单的动作。

那么如何实现这种延时业务呢?通常有以下4种方案。

定时任务轮询db

用户下单后db中会生成一条订单记录,记录了订单号、用户ID、创建时间、订单详情、订单状态等信息。假设超时时间是600秒,我们后台起一个定时任务,每隔固定时间运行一次,每次扫描db中的超时订单select * from order where createTime <= now()-600,然后取消查询到的订单。

这种方法实现简单,但是有很多缺点。超时时间通常是秒级的,如果定时任务每秒运行一次,那么就相当于每秒就要对订单表做一次扫描,这是相当消耗db资源的操作,因此定时任务一般不会设置为秒级;但是如果设置为分钟级,又会牺牲即时性,比如600秒超时,很有可能660秒的时候订单才被取消。

DelayQueue

JDK的DelayQueue(延迟队列)是无界阻塞队列,只有在延迟期满时才能从中获取元素。每生成一个订单,在把订单记录到db的同时,要把订单id等信息投递到延迟队列中去,队列会按照超时时间进行排序,最先超时的订单排在队列的头部;起一个单独的线程不断地从队列中摘取元素然后去做取消订单的动作。

这种方法最大的缺点就是没有将超时信息持久化,服务重启之后延迟队列的元素不会被恢复。

redis的zset

在redis中创建一个key是”delayOrders”的zset,每个member就是订单ID,member的score就是该订单的超时时间戳。我们每次从zset中取出score最小也就是最先超时的元素,判断其是否超时,如果超时就将其从zset中删除并取消订单,如果未超时就继续执行下一次循环。

RabbitMQ的TTL+DLX

RabbitMQ可设置消息过期时间(TTL),当消息过期后可以将该消息投递到队列上设置的死信交换器(DLX)上。然后投递到死信队列中,重新消费。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eqEU9zNh-1670921230498)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/4fa79598-6109-4df7-b10a-9b426c9fec9c/Untitled.png)]

四种方案对比

方案技术难度性能异常恢复分布式/集群支持任务修改/撤回
定时任务轮询DB简单较差支持支持,需要自己加分布式锁支持
DelayQueue简单不支持不支持支持
redis的zset复杂支持支持,需要自己加分布式锁支持
RabbitMQ的TTL+DLX复杂支持原生支持不支持

通用型延迟任务管理器

在这里我们尝试用Redis实现一个通用的延迟任务管理器

依赖

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.11.5</version>
        </dependency>

定义延迟任务抽象接口DelayTask,任务需要实现getTaskType方法获取任务类型,toString方法输出任务的字符串形式,hashCode方法输出任务的哈希值。

同一个任务的toString方法输出结果要相同,不同任务的toString方法输出结果要不同

同一个任务的hashCode方法输出结果要相同,不同任务的hashCode方法输出结果尽量不同

package ltd.loveacg.delaytask;

/**
 * @author ALVIN
 */
public interface DelayTask {

    /**
     * 获取延迟任务类型
     * @return 任务类型
     */
    String getTaskType();

    /**
     * 同一个任务的toString结果要确保相同
     * @return 延迟任务的字符串表示
     */
    @Override
    String toString();

    /**
     * 同一个任务的hashCode结果要确保相同
     * @return 哈希值
     */
    @Override
    int hashCode();
}

定义延迟任务处理器的抽象接口DelayTaskSubscriber。

package ltd.loveacg.delaytask;

/**
 * 延迟任务订阅处理器
 * @author ALVIN
 */
public interface DelayTaskSubscriber {
    /**
     * 是否支持处理指定任务
     * @param task 延迟任务
     * @return 是否支持
     */
    boolean isSupport(DelayTask task);

    /**
     * 任务处理逻辑
     * @param task 延迟任务
     * @return 是否处理成功
     */
    boolean handleTask(DelayTask task);
}

定义延迟任务管理器的抽象接口DelayTaskChannel。延迟任务管理器包含发布任务和撤回任务两个方法。发布同一个任务时

package ltd.loveacg.delaytask;

/**
 * 延迟任务管理器
 * @author ALVIN
 */
public interface DelayTaskChannel {

    /**
     * 发布延迟任务
     * @param task 延迟任务
     * @param wakeTime 期望任务开始处理的时间(时间戳)
     */
    void publish(DelayTask task, long wakeTime);

    /**
     * 撤销任务
     * @param task 延迟任务
     */
    void withdraw(DelayTask task);
}

默认的延迟任务管理器实现类

package ltd.loveacg.delaytask;

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;

import javax.annotation.PreDestroy;
import javax.annotation.Resource;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;

/**
 * @author ALVIN
 */

public class DefaultDelayTaskChannel implements DelayTaskChannel {

    private final static Logger LOGGER = LoggerFactory.getLogger(DefaultDelayTaskChannel.class);

    private final static String KEY = "delayTask";

    private final RedisTemplate<String, Object> redisTemplate;

    private final RedissonClient redissonClient;

    private final Executor executor;

    private final Set<DelayTaskSubscriber> subscribers;

    private volatile boolean destroyFlag = false;

    public DefaultDelayTaskChannel(RedisTemplate<String, Object> redisTemplate, RedissonClient redissonClient, Executor executor,
                                   Set<DelayTaskSubscriber> subscribers) {
        this.redisTemplate = redisTemplate;
        this.redissonClient = redissonClient;
        this.executor = executor;
        this.subscribers = subscribers;
    }

    @Override
    public void publish(DelayTask task, long wakeTime) {
        // 用于任务排序
        redisTemplate.opsForZSet().add(KEY, task, wakeTime);
        // 用于双重校验
        redisTemplate.opsForValue().set(KEY + ":" + task, wakeTime);
    }

    @Override
    public void withdraw(DelayTask task) {
        redisTemplate.opsForZSet().remove(KEY, task);
        redisTemplate.delete(KEY + ":" + task.toString());
    }

    @PreDestroy
    public void destroy() {
        this.destroyFlag = true;
    }

    public final class EventDispatcher implements Runnable {

        @Override
        public void run() {
            LOGGER.debug("延迟任务分发器 - 启动");
            while (!Thread.interrupted() && !destroyFlag) {
                try {
                    Set<Object> delayTasks = redisTemplate.opsForZSet().rangeByScore(KEY, 0L, System.currentTimeMillis());
                    if (delayTasks != null) {
                        for (Object task : delayTasks) {
                            DelayTask delayEvent = (DelayTask) task;
                            RLock taskLock = redissonClient.getLock(KEY + ":dispatcher:lock:" + task.hashCode());
                            try {
                                // 尝试获取任务锁,若获取锁失败,则取下一个任务
                                if (!taskLock.tryLock()) {
                                    continue;
                                }

                                Long wakeTime = (Long) redisTemplate.opsForValue().get(KEY + ":" + task);
                                if (wakeTime == null || System.currentTimeMillis() < wakeTime) {
                                    continue;
                                }

                                for (DelayTaskSubscriber subscriber : subscribers) {
                                    if (subscriber.isSupport(delayEvent)) {
                                        // 开启新线程处理事件
                                        CompletableFuture.supplyAsync(() -> subscriber.handleTask(delayEvent), executor).whenComplete((result, throwable) -> {
                                            if (throwable != null || !result) {
                                                LOGGER.error("延迟任务处理异常,任务:{},异常:{}", delayEvent, throwable);
                                                // 一分钟后重试
                                                publish(delayEvent, System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(1L));
                                            } else {
                                                withdraw(delayEvent);
                                            }
                                        });
                                    }
                                }
                            } finally {
                                if (taskLock.isHeldByCurrentThread()) {
                                    taskLock.unlock();
                                }
                            }

                        }
                    }

                    Thread.sleep(100);
                } catch (Exception e) {
                    LOGGER.error("延迟任务分发器异常:{},{}", e.getMessage(), e.toString());
                }
            }
        }
    }
}

配置类

package ltd.loveacg.delaytask;

import jodd.util.concurrent.ThreadFactoryBuilder;

import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;

import javax.annotation.Resource;
import java.util.Set;
import java.util.concurrent.*;

/**
 * @author ALVIN
 */
@Configuration
public class DelayTaskAutoConfiguration {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    @Resource
    private RedissonClient redissonClient;

    @Resource
    private Set<DelayTaskSubscriber> subscriberSet;

    public static final Logger LOGGER = LoggerFactory.getLogger(DelayTaskAutoConfiguration.class);

    /**
     * 创建默认的延迟任务处理线程池
     * @return 线程池
     */
    @Bean(name = "delayTaskExecutor")
    public Executor executor() {
        ThreadFactoryBuilder threadFactoryBuilder = ThreadFactoryBuilder.create();
        threadFactoryBuilder.setDaemon(true);
        threadFactoryBuilder.setNameFormat("延迟任务处理线程-%d");
        threadFactoryBuilder.setPriority(Thread.NORM_PRIORITY);
        threadFactoryBuilder.setUncaughtExceptionHandler((thread, exception) -> {
            LOGGER.error("延迟任务处理线程[{}]出现异常,原因:{}", thread.getName(), exception);
        });

        ThreadFactory threadFactory = threadFactoryBuilder.get();
        ThreadPoolExecutor.CallerRunsPolicy callerRunsPolicy = new ThreadPoolExecutor.CallerRunsPolicy();
        return new ThreadPoolExecutor(10, 30, 30,
                TimeUnit.MINUTES, new LinkedBlockingQueue<>(2000), threadFactory, callerRunsPolicy);
    }

    /**
     * 创建延迟任务通道
     * @return 延迟任务通道
     */
    @Bean
    public DelayTaskChannel delayTaskChannel() {
        DefaultDelayTaskChannel delayTaskChannel = new DefaultDelayTaskChannel(redisTemplate, redissonClient, executor(), subscriberSet);
        Executors.newSingleThreadExecutor().submit(delayTaskChannel.new EventDispatcher());
        return delayTaskChannel;
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值