苍穹外卖 Spring定时任务实践

7 阅读4分钟

先讲 Spring 定时任务的简洁实现思路,再结合苍穹外卖项目代码。

零基础入门

  1. 定时任务是什么:到了指定时间,系统自动执行一段代码,不需要用户点击按钮。
  2. 为什么需要定时任务:很多业务会“随时间自然变化”,比如超时未支付关单、长时间派送自动完成。
  3. @Scheduled 是什么:告诉 Spring 这个方法要按固定规则执行(如每分钟、每天凌晨)。
  4. cron 表达式是什么:用一串时间规则描述“什么时候执行”。

一个最小例子

@Component
public class DemoTask {
    // cron表达式:每分钟的第0秒执行一次
    @Scheduled(cron = "0 * * * * ?")
    public void runEveryMinute() {
        System.out.println("每分钟执行一次");
    }
}
@SpringBootApplication
@EnableScheduling // 开启Spring定时任务调度功能,让@Scheduled注解生效
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

业务代码负责定义时间到了要做什么,调度框架负责按时触发执行


一、Spring 定时任务的简洁实现思路

目标:让时间规则有统一执行入口

我们不希望在 Controller 里写“人工触发”的管理接口,而是把“时间到了自动收口”的逻辑放在 Task 层集中处理。

最小实现只需要 3 个点:

  1. 开启调度能力(@EnableScheduling
  2. 定义任务类(@Component
  3. 在方法上声明触发规则(@Scheduled(cron = "...")
sequenceDiagram
    title 任务触发流程
    participant Scheduler as SpringScheduler
    participant Task as OrderTask
    participant Service as 业务逻辑
    participant DB as 数据库

    Scheduler->>Task: 到达 cron 时间点
    Task->>Service: 执行任务方法
    Service->>DB: 查询符合条件数据
    Service->>DB: 更新目标状态
    Task-->>Scheduler: 本轮执行结束

最小代码骨架

@Component
public class OrderTimeoutTask {

    @Scheduled(cron = "0 * * * * ?")
    public void closeTimeoutOrders() {
        // 1) 查询“待支付且已超时”的订单
        // 2) 逐条改为“已取消”
        // 3) 记录取消时间和原因
    }
}

核心思想:定时任务是“时间维度的业务入口”,应集中治理,而不是散落在 Controller 或手工脚本里。


二、苍穹外卖项目的实现

业务场景

场景触发条件自动动作
超时未支付订单待付款且超过 15 分钟改为已取消,写取消原因和取消时间
长时间派送中订单派送中且超过阈值改为已完成

这些规则本质都是:按“状态 + 时间阈值”筛选订单,再做自动状态推进

找到任务入口(启动类)

@SpringBootApplication
@EnableTransactionManagement //开启注解方式的事务管理
@Slf4j
@EnableCaching//开发缓存注解功能
@EnableScheduling //开启任务调度
public class SkyApplication {
    public static void main(String[] args) {
        SpringApplication.run(SkyApplication.class, args);
        log.info("server started");
    }
}

如果没有 @EnableScheduling@Scheduled 方法不会被调度执行。

核心任务代码(OrderTask)

    @Scheduled(cron = "0 * * * * ? ") // 每分钟扫描超时未支付订单
    public void processTimeoutOrder(){
        log.info("定时处理超时订单:{}", LocalDateTime.now());

        LocalDateTime time = LocalDateTime.now().plusMinutes(-15);
        List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.PENDING_PAYMENT, time);

        if(ordersList != null && ordersList.size() > 0){
            for (Orders orders : ordersList) {
                orders.setStatus(Orders.CANCELLED);
                orders.setCancelReason("订单超时,自动取消");
                orders.setCancelTime(LocalDateTime.now());
                orderMapper.update(orders);
            }
        }
    }

    @Scheduled(cron = "0 0 1 * * ?") // 每天凌晨 1 点扫描派送中订单
    public void processDeliveryOrder(){
        log.info("定时处理处于派送中的订单:{}",LocalDateTime.now());

        LocalDateTime time = LocalDateTime.now().plusMinutes(-60);
        List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.DELIVERY_IN_PROGRESS, time);

        if(ordersList != null && ordersList.size() > 0){
            for (Orders orders : ordersList) {
                orders.setStatus(Orders.COMPLETED);
                orderMapper.update(orders);
            }
        }
    }

这段代码体现了 5 个关键点:

  1. 任务粒度清晰:一个方法只负责一种规则(超时取消 / 超时完成)。
  2. 筛选条件明确:按状态和时间阈值查库,避免误处理。
  3. 状态推进可追踪:取消单补齐取消原因和时间,便于排查。
  4. 可重复执行:同一轮任务多执行几次,最终结果仍收敛到正确状态。
  5. 日志可观测:每轮执行打点,便于追踪任务是否按时触发。

数据访问条件(Mapper)

Task应遵循幂等原则,即重复执行n次操作,结果是相同的。还要避免定时操作与用户操作互相覆盖。所以需要先判断,再访问数据。OrderTask 依赖 OrderMapper.getByStatusAndOrderTimeLT(status, orderTime),语义是:

  • 订单状态等于目标状态
  • 下单时间早于阈值时间

这就是时间任务里最常见的查询模型:状态机条件 + 时间窗口条件


三、定时任务与业务异常的边界(容易混淆)

类型典型来源表现形式
业务接口异常Controller / Service返回 Result.error(...) 给前端
定时任务执行异常Task 方法内部打日志、警告,之后重试或下轮继续处理

不要把定时任务当成“用户请求接口”去设计。它更像“后台守护线程”:重点不是前端提示,而是可恢复性和可观测性

四、注意事项

  1. 异常处理:任务里要记录完整异常堆栈,避免静默失败。
  2. 幂等意识:更新语句尽量带状态条件,防止和人工操作互相覆盖。
  3. 多实例防重:线上多节点部署时,要考虑分布式锁或统一调度平台。
  4. 执行时长监控:关注任务耗时、每轮处理数量,及时发现积压。
  5. 业务补偿联动:关单后若涉及库存、优惠券等,应同步补偿回滚。

总结

  1. @EnableScheduling + @Scheduled 把“时间到了要发生什么”集中到 Task 层治理。
  2. 在苍穹外卖里,OrderTask 用“状态 + 时间阈值”实现了超时关单和自动完成,让订单生命周期具备自动收口能力。

附录:相关源码路径

sky-server/src/main/java/com/sky/SkyApplication.java
sky-server/src/main/java/com/sky/task/OrderTask.java
sky-server/src/main/java/com/sky/mapper/OrderMapper.java

参考

苍穹外卖www.bilibili.com/video/BV1TP…

deekseek-v4