SpringTask

118 阅读5分钟

SpringTask介绍

SpringTask是Spring框架提供的一种任务调度和异步处理的解决方案。可以按照约定的时间自动执行某个代码逻辑,它可以帮助开发者在Spring应用中轻松实现定时任务、异步任务等功能,提高应用的效率和可维护性。

应用场景:

  • 信用卡每月还款提醒
  • 银行贷款每月还款提醒
  • 火车票售票系统处理未支付订单
  • 入职纪念日为用户发通知

使用步骤

  1. 在引导类上添加注解@EnableScheduling,开启定时任务功能
  2. 哪个方法需要周期性执行,就在方法上加注解@Scheduled()

注意:必须是bean对象里的方法,否则Spring不能管理

注解@Scheduled

注解@Scheduled:加在方法上,表示要周期性执行

执行的时机,是通过注解的一些属性参数配置,常用的有fixedDelayfixedRatecron

一些简单的定时任务,使用

fixedDelay:设置一个毫秒值,是上一次执行结束后,延迟多长时间开始下一个任务

fixedRate:设置一个毫秒值,是上一次任务执行开始之后,延迟多长时间开始下一个任务 image-20240406152747817.png

一些复杂的定时任务,使用

cron:设置一个cron表达式,通过cron表达式可以非常方便的表示一些复杂的执行周期

cron表达式

语法: 由7个域组成,使用空格隔开,是 秒 分 时 日 月 周 年 但是“年”一般不写,所以使用时通常用前6个域

名称是否必须允许值特殊字符
0-59, - * /
0-59, - * /
0-23, - * /
1-31 , - * ? / L W C
1-12 或 JAN-DEC, - * /
1-7(表示周一到周日) 或 SUN-SAT, - * ? / L C #

说明:

如果写精确值,就表示只有这个值才会触发
* 表示所有合法值都会触发
? 表示忽略这个值。通常 日和周两个域,一个有值,另外一个就必须是?。如果日和周两个域都有值,就会出现不可预测的结果
, 表示可以在一个域上设置多个值
- 表示设置一个范围值。在指定范围内每个合法值都会触发
/ 表示设置一个递增值。比如 1/5 从1开始,每递增5就触发一次
L 表示last,可以用在日和周域上
	如果用在日域上,表示月的最后一天
    如果用在周域上,前边要加一个数字X,表示最后一个星期X。 3L 最后一个星期三
W 表示Weekday,离指定日期最近的工作日(不跨月)触发
# 用在周域上,X#Y 表示第Y个星期X触发

示例:

0 0 0 1 1 ?  表示每年的1月1日 00:00:00
0 0 0 * * ?  表示每年、每月、每天的 00:00:00
0 0 0 1 * ?  表示每年、每月、1号的 00:00:00
* * * * * ?  表示每年、每月、每天的 每小时、每分钟、每一秒
5 * * * * ?  表示每年、每月、每天的 每小时、每分钟的 第5秒时
0,30 * * * * ? 表示每年、每月、每天的 每小时、每分钟的 第0秒和第30秒执行
0-30 * * * * ? 表示每年、每月、每天的 每小时、每分钟的 第0~30秒中每秒执行
0/5 * * * * ?  表示每年、每月、每天的 每小时、每分钟的 第0秒开始,5秒执行一次
0 0 0 1W * ?   表示每年、每月的1号(离1号最近的工作日)那天 00:00:00
0 0 8 ? 6 7#3  每年父亲节早上8点整触发执行一次(每年6月的第3个星期日)

示例代码:

//每秒钟打印一次当前时间
@Component
public class DemoTask {
    //@Scheduled(fixedDelay = 1000)
    @Scheduled(cron = "0/5 * * * * ?")
    public void printTime(){
        System.out.println(LocalTime.now());
    }
}

运行结果 image-20240406171818236.png

应用案例

需求分析

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

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

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

  • 自动取消 超时未支付 的订单:

    通过定时任务,每分钟检查一次是否存在支付超时订单(下单后超过15分钟仍未支付则判定为支付超时订单),如果存在则修改订单状态为“已取消”

  • 自动完成 派送中超时 的订单

    通过定时任务,每天凌晨1点检查一次是否存在“派送中”的订单,如果存在则修改订单状态为“已完成”

代码及分析步骤:

@Component
@Slf4j
public class OrderTask {
    /*
    * 清理超时未支付的订单
    * 执行时机:每分钟执行一次
    * 执行任务:
    *   1. 找到15分钟之前下的订单,并且状态是 未支付
    *   2. 如果找到了,把这些订单的状态修改成 已取消 设置取消原因和取消时间
    * */

    @Autowired
    private UserOrderMapper orderMapper;
    @Scheduled(fixedDelay = 60*1000)
    public void cancelTimeoutOrder(){
        log.info("扫描超时未支付的订单");
        //1. 找到15分钟之前下的订单,并且状态是 未支付
        OrdersPageQueryDTO dto = new OrdersPageQueryDTO();
        dto.setStatus(Orders.PENDING_PAYMENT);
        //获取当前时间,并在当前时间的基础上减15分钟
        LocalDateTime time = LocalDateTime.now().plusMinutes(-15);
        dto.setEndTime(time);
        List<Orders> orders = orderMapper.pageQuery(dto);

        //2.如果找到了,把这些订单的状态修改成 已取消 设置取消原因和取消时间
        if(orders != null && orders.size() > 0){
            for (Orders order : orders) {
                order.setStatus(Orders.CANCELLED);
                order.setCancelReason("订单超时未支付,自动取消");
                order.setCancelTime(LocalDateTime.now());
                orderMapper.update(order);
                log.info("清理超时未支付的订单:{}",order);
            }
        }
    }

    /**
     * 定时完成 派送中的订单
     * 执行时机:每天凌晨2点整
     * 执行任务:
     *  1. 查找所有派送中的订单
     *  2. 如果找到了,遍历这些订单,直接把订单的状态修改成“已完成”
     */
    @Scheduled(cron = "0 0 2 * * ?")
    public void completeDeliveryOrder(){
        log.info("开始扫描派送中超时的订单..........");
        //1. 查询所有派送的订单
        OrdersPageQueryDTO dto = new OrdersPageQueryDTO();
        dto.setStatus(Orders.DELIVERY_IN_PROGRESS);
        List<Orders> ordersList = orderMapper.pageQuery(dto);

        //2. 如果找到了,遍历这些订单,直接把订单的状态修改成 ‘已完成’
        if(ordersList != null && ordersList.size() > 0){
            for (Orders orders : ordersList) {
                orders.setStatus(Orders.COMPLETED);
                orderMapper.update(orders);
                log.info("派送超时自动完成订单:{}",orders);
            }
        }
    }
}