餐饮智能互联平台技术要点——定时处理(Spring Task)

简介

Spring Task 是Spring框架提供的任务调度工具,可以按照约定的时间自动执行某个代码逻辑。
作用:定时自动执行某段Java代码

应用场景

  • 信用卡每月还款提醒
  • 银行贷款每月还款提醒
  • 火车票售票系统处理未支付订单
  • 入职纪念日为用户发送通知
  • ... (只要是需要定时处理的场景都可以使用Spring Task)

cron表达式

cron表达式其实就是一个字符串,通过cron表达式可以定义任务触发的时间。

构成规则:分为6或7个域,由空格分隔开,每个域代表一个含义。

每个域的含义分别为:秒、分钟、小时、日、月、周、年(可选)

e.g., 2022年10月12日上午9点整 对应的cron表达式为:0 0 9 12 10 ? 2022

cron表达式在线生成器:https://cron.qqe2.com/

说明:一般的值不同时设置,其中一个设置,另一个用?表示。

比如: 描述2月份的最后一天,最后一天具体是几号呢?可能是28号,也有可能是29号,所以就不能写具体数字。

案例分析

使用步骤

  • 导入maven坐标 spring-context
  • 启动类添加注解 @EnableScheduling 开启任务调度
  • 自定义定时任务类

需求分析

用户下单后可能存在的情况:

  • 下单后未支付,订单一直处于“待支付”状态
  • 用户收货后管理端未点击完成按钮,订单一直处于“派送中”状态

对于上面两种情况需要通过定时任务来修改订单状态,具体逻辑为:

  • 通过定时任务每分钟检查一次是否存在支付超时订单(下单后超过15分钟仍未支付则判定为支付超时订单),如果存在则修改订单状态为“已取消”
  • 通过定时任务每天凌晨1点检查一次是否存在“派送中”的订单,如果存在则修改订单状态为“已完成”

代码开发

1). 自定义定时任务类OrderTask(待完善):

package com.sky.task;

/**
 * 自定义定时任务,实现订单状态定时处理
 */
@Component
@Slf4j
public class OrderTask {

    @Autowired
    private OrderMapper orderMapper;

    /**
     * 处理支付超时订单
     */
    @Scheduled(cron = "0 * * * * ?")
    public void processTimeoutOrder(){
        log.info("处理支付超时订单:{}", new Date());
    }

    /**
     * 处理“派送中”状态的订单
     */
    @Scheduled(cron = "0 0 1 * * ?")
    public void processDeliveryOrder(){
        log.info("处理派送中订单:{}", new Date());
    }

}

2). 在OrderMapper接口中扩展方法:

	/**
     * 根据状态和下单时间查询订单
     * @param status
     * @param orderTime
     */
    @Select("select * from orders where status = #{status} and order_time < #{orderTime}")
    List<Orders> getByStatusAndOrdertimeLT(Integer status, LocalDateTime orderTime);

3). 完善定时任务类的processTimeoutOrder方法:

	/**
     * 处理支付超时订单
     */
    @Scheduled(cron = "0 * * * * ?")
    public void processTimeoutOrder(){
        log.info("处理支付超时订单:{}", new Date());

        LocalDateTime time = LocalDateTime.now().plusMinutes(-15);

        // select * from orders where status = 1 and order_time < 当前时间-15分钟
        List<Orders> ordersList = orderMapper.getByStatusAndOrdertimeLT(Orders.PENDING_PAYMENT, time);
        if(ordersList != null && ordersList.size() > 0){
            ordersList.forEach(order -> {
                order.setStatus(Orders.CANCELLED);
                order.setCancelReason("支付超时,自动取消");
                order.setCancelTime(LocalDateTime.now());
                orderMapper.update(order);
            });
        }
    }

4). 完善定时任务类的processDeliveryOrder方法:

	/**
     * 处理“派送中”状态的订单
     */
    @Scheduled(cron = "0 0 1 * * ?")
    public void processDeliveryOrder(){
        log.info("处理派送中订单:{}", new Date());
        // select * from orders where status = 4 and order_time < 当前时间-1小时
        LocalDateTime time = LocalDateTime.now().plusMinutes(-60);
        List<Orders> ordersList = orderMapper.getByStatusAndOrdertimeLT(Orders.DELIVERY_IN_PROGRESS, time);

        if(ordersList != null && ordersList.size() > 0){
            ordersList.forEach(order -> {
                order.setStatus(Orders.COMPLETED);
                orderMapper.update(order);
            });
        }
    }

底层原理

Spring通过 TaskExecutor 和 TaskScheduler 接口提供了一个抽象层,用于实现异步定时任务。这意味着 Spring 允许你选择使用其他定时任务框架,同时也提供了自身的定时任务实现,即 Spring Task。 Spring Task支持线程池,能够高效处理多种不同的定时任务需求。
20231007164050

TaskExecutor

TaskExecutor 是 Spring 框架中的一个抽象接口,它自然地与Java标准库中的 java.util.concurrent.Executor 产生了关联。然而,Spring 引入 TaskExecutor 的目的不是为了替代Java标准库中的Executor,而是为了增强其功能,特别是在支持定时任务的执行上。TaskExecutor源码如下:

public interface TaskExecutor extends Executor {
  void execute(Runnable var1);
}

TaskScheduler

TaskScheduler是Spring Task的第二个抽象,用于提供定时任务的支持。
它需要传入一个 Runnable 任务作为参数,并指定任务的执行时间或触发器,以便周期性执行任务。
传入时间很好理解,有意思的是传入一个触发器(Trigger)的情况,因为这里需要使用cron表达式去触发一个定时任务。Spring 提供了一个 CronTrigger,通过传入一个 Runnable任务和 CronTrigger,就可以使用cron表达式去指定定时任务了。

scheduler.schedule(task, new CronTrigger("30 * * * * ?"));

TaskScheduler的抽象优点在于,它使需要执行定时任务的代码不必直接依赖于特定的定时任务框架(例如Timer或Quartz)。其中一种更简便的实现是ThreadPoolTaskScheduler,它实际上是对JDK中的SchedulingTaskExecutor的代理,并同时实现了TaskExecutor接口。因此,如果需要频繁执行定时任务的场景,可以考虑使用这个实现,而这也是Spring官方推荐的方式。

关键源代码

postProcessAfterInitialization
20231007184238

processScheduled
重点在第 4 步,processScheduled 封装了较多的主要处理逻辑。

protected void processScheduled(Scheduled scheduled, Method method, Object bean) {
        try {
            // 1. 创建线程
            Runnable runnable = createRunnable(bean, method);
            ...

            // 2. 处理 initialDelay 与 initialDelayString 属性
            long initialDelay = scheduled.initialDelay();
            String initialDelayString = scheduled.initialDelayString();
            ...

            // 3. 处理 cron 与 zone 属性
            String cron = scheduled.cron();
            if (StringUtils.hasText(cron)) {
                String zone = scheduled.zone();
                if (this.embeddedValueResolver != null) {
                    cron = this.embeddedValueResolver.resolveStringValue(cron);
                    zone = this.embeddedValueResolver.resolveStringValue(zone);
                }
                if (StringUtils.hasLength(cron)) {
                    Assert.isTrue(initialDelay == -1, "'initialDelay' not supported for cron triggers");
                    processedSchedule = true;
                    // 4. 此处用到了注解的第一个属性 CRON_DISABLED
                    if (!Scheduled.CRON_DISABLED.equals(cron)) {
                        TimeZone timeZone;
                        if (StringUtils.hasText(zone)) {
                            timeZone = StringUtils.parseTimeZoneString(zone);
                        }
                        else {
                            timeZone = TimeZone.getDefault();
                        }
                        // 5. 处理重点
                        // cron 与 zone 构造 CronTrigger,再同方法开始构造好的线程一起构造 CronTask ( Task 的子类 )
                        // 再将构造好的 CronTask 存到到 ScheduledTaskRegistrar 的 CronTask 列表中
                        tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone))));
                    }
                }
            }

            ...

            // 6. 处理 fixedDelay ,与上述过程类似
            long fixedDelay = scheduled.fixedDelay();
            ...

            // 7. 处理 fixedRate,与上述过程类似
            long fixedRate = scheduled.fixedRate();
            ...

            // 8. 最后将 task 注册到 scheduledTasks 中
            synchronized (this.scheduledTasks) {
                Set<ScheduledTask> regTasks = this.scheduledTasks.computeIfAbsent(bean, key -> new LinkedHashSet<>(4));
                regTasks.addAll(tasks);
            }
        }
        ...
    }

处理重点在第五步:

private final ScheduledTaskRegistrar registrar;
...
// 5. 处理重点
// cron 与 zone 构造 CronTrigger,再同方法开始构造好的线程一起构造 CronTask ( Task 的子类 )
// 再将构造好的 CronTask 存到到 ScheduledTaskRegistrar 的 CronTask 列表中
tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone))));

概括——项目启动时,在初始化 bean 后,带 @Scheduled 注解的方法会被拦截,然后依次:构建执行线程,解析参数,加入线程池。

注:默认的线程池用的是单线程。

posted @ 2023-10-07 18:51  岸南  阅读(64)  评论(0)    收藏  举报