spring boot 多线程定时器

时间:2022-02-08 23:18:32

前言

我们在做实际项目中经常会需要用到定时任务用来定时处理些需要后台自动处理的逻辑,实现定时器的的方式也有很多种:

1. Quartz:Quartz的使用相当广泛,它是一个功能强大的调度器,当然使用起来也相对麻烦;

2. java.util包里的Timer,它也可以实现定时任务但是功能过于单一所有使用很少。

3. 就是我们今天要介绍的Spring自带的定时任务Schedule,其实可以把它看作是一个简化版的,轻量级的Quartz,使用起来也相对方便很多。

实现定时任务

网上有很多介绍spring boot定时任务的例子,也很简单,这里简单的介绍一下。

方法一:通过springboot自带入口来开启定时器。

该方法就是在spring boot的启动类Application上开启定时器注解,让spring boot知道你在用定时器,增加@EnableScheduling 注解即可。

@SpringBootApplication  
@EnableScheduling  
public class DemoApplication {  
 
    public static void main(String[] args) {  
        SpringApplication.run(DemoApplication.class, args);  
    }  
} 

添加完注解后就需要使用了,使用也是直接在用的方法上增加注解,spring boot支持多种注解方式运行定时器,不过通常使用cron表达式的方式。

@Component
public class Jobs {
    public final static long ONE_Minute =  60 * 1000;
    
    @Scheduled(fixedDelay=ONE_Minute)
    public void fixedDelayJob(){
        System.out.println(Dates.format_yyyyMMddHHmmss(new Date())+" >>fixedDelay执行....");
    }
    
    @Scheduled(fixedRate=ONE_Minute)
    public void fixedRateJob(){
        System.out.println(Dates.format_yyyyMMddHHmmss(new Date())+" >>fixedRate执行....");
    }

    @Scheduled(cron="0 15 3 * * ?")
    public void cronJob(){
        System.out.println(Dates.format_yyyyMMddHHmmss(new Date())+" >>cron执行....");
    }
}

cron表达式后面详细讲解下。

方法二:直接给类添加配置。

其实这个方法跟方法一类似,方法一是把@EnableScheduling 注解配置放在启动类上,方法二是把这个配置直接放在定时器的service上,功能一样推荐使用第一种。

    @Configuration          //证明这个类是一个配置文件  
    @EnableScheduling       //打开quartz定时器总开关  
    public class Timer {  
    }  

上面只是简单的介绍了下spring boot实现定时器的方法,网上有好多例子。

我们可以在上面定时器执行的方法里面增加一段代码获取当前线程(Thread.currentThread().getId())并输出到控制台,我们就会发现每次都是同一个线程执行,说明spring boot的定时器是单线程的,但是我们实际上的项目中可能会有多个定时器启动这种单线程的情况下就不能满足我们的需求,如果一个定时器里面的数据量过大或者是处理逻辑复杂耗费时间很长就会出现后面的定时要等这个定时器处理完才执行,这就可能造成定时器执行延迟,所以这个时候我们就要使用多线程启动定时器。

多线程定时任务

增加一个定时器配置文件,里面配置多线程信息,我们设置线程池支持20个线程,并且使用完后60s自动结束。

/**
 * 类或方法的功能描述 :定时器多线程配置
 *
 * @author: logan.zou
 * @date: 2018-05-09 15:25
 */
@Configuration
@EnableAsync
public class ScheduleConfig implements SchedulingConfigurer, AsyncConfigurer {

    /**
     * 并行任务
     * @param scheduledTaskRegistrar
     */
    @Override
    public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
        TaskScheduler taskScheduler = taskScheduler();
        scheduledTaskRegistrar.setTaskScheduler(taskScheduler);
    }

    /**
     * 多线程配置
     * @return
     */
    @Bean(destroyMethod = "shutdown")
    public ThreadPoolTaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(20);
        // 设置线程名前缀
        scheduler.setThreadNamePrefix("task-");
        // 线程内容执行完后60秒停在
        scheduler.setAwaitTerminationSeconds(60);
        // 等待所有线程执行完
        scheduler.setWaitForTasksToCompleteOnShutdown(true);
        return scheduler;
    }

    /**
     * 异步任务
     * @return
     */
    @Override
    public Executor getAsyncExecutor() {
        Executor executor = taskScheduler();
        return executor;
    }

    /**
     * 异常处理
     * @return
     */
    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new SimpleAsyncUncaughtExceptionHandler();
    }

}

动态配置cron配置

实际项目中我们定时器的执行时间频率可能会改变,这个时候我们就需要把这个参数放到配置文件application.yml中,修改配置时可以不需要启动服务器。

我们定义个动态配置类并实现SchedulingConfigurer重写里面的方法,设置cron表达式。其中doBiz()方法就是定时器里面需要处理的逻辑。

/**
 * 类或方法的功能描述 :定时器配置
 *
 * @author: logan.zou
 * @date: 2018-05-09 12:05
 */
@Component
public class DynamicScheduledTask implements SchedulingConfigurer {
    // 定时器执行频率,默认隔10s执行一次
    private static final String DEFAULT_CRON = "0/10 * * * * ?";
    private String cron = DEFAULT_CRON;

    @Override
    public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
        scheduledTaskRegistrar.addTriggerTask(new Runnable() {
            @Override
            public void run() {
                doBiz();
            }
        }, new Trigger() {
            @Override
            public Date nextExecutionTime(TriggerContext triggerContext) {
                // 定时任务触发,可修改定时任务的执行周期
                CronTrigger trigger = new CronTrigger(cron);
                Date nextExecDate = trigger.nextExecutionTime(triggerContext);
                return nextExecDate;
            }
        });
    }

    /**
     * 定时器执行方法
     */
    public void doBiz() {

    }

    public String getCron() {
        return cron;
    }

    public void setCron(String cron) {
        this.cron = cron;
    }
}
我们新建的定时器类只要继承这个DynamicScheduledTask这个类,并重写里面的doBiz()方法即可。比如我新建一个OrderUnPayTimerService定时器处理未支付订单自动取消功能。

/**
 * 类或方法的功能描述 :订单未支付定时器
 *
 * @author: logan.zou
 * @date: 2018-05-09 10:24
 */
@Component
@Service
public class OrderUnPayTimerService extends DynamicScheduledTask {
    private static Logger logger = LoggerFactory.getLogger(OrderUnPayTimerService.class);
    @Autowired
    TimerConfig timerConfig;
    @Autowired
    ShopOrderExMapper shopOrderExMapper;
    @Autowired
    ShopOrderStatusHistoryMapper shopOrderStatusHistoryMapper;

    /**
     * 更新未支付订单的状态为已取消
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void doBiz() {
        logger.info("OrderUnPayTimerService >>>>>>>>>> 定时器开始执行 " + DateUtil.getDateTimeString1(new Date()));
        try {
            /**
             * 设置定时器执行频率
             */
            setCron(timerConfig.getOrderUnPayCron());

            /**
             * 1.获取3天未支付的订单ID
             */
            Map<String, Object> param = new HashMap<>();
            param.put("orderStatus", Constants.OrderStatus.STATUS_PRE_PAY);
            param.put("intevalTime", timerConfig.getOrderUnPayCondition());

            List<Integer> ids = shopOrderExMapper.getOrderByStatusAndIntevalTime(param);
            // 订单状态历史记录
            List<ShopOrderStatusHistory> orderStatusHistoryList = new ArrayList<>();
            // 订单
            List<ShopOrder> orderList = new ArrayList<>();
            if (null != ids && ids.size() > 0) {
                for (Integer orderId : ids) {
                    /**
                     * 2.更新订单状态为已取消
                     */
                    ShopOrder shopOrder = updateShopOrder(orderId);
                    if (null != shopOrder) {
                        orderList.add(shopOrder);
                    }

                    /**
                     * 3.记录订单状态历史记录
                     */
                    ShopOrderStatusHistory orderStatusHistory = saveOrderStatusHistory(orderId);
                    if (null != orderStatusHistory) {
                        orderStatusHistoryList.add(orderStatusHistory);
                    }
                }
            }

            /**
             * 4.操作数据库做保存和更新操作
             */
            if (null != orderList && orderList.size() > 0) {
                shopOrderExMapper.updateAllStatus(orderList);
            }

            if (null != orderStatusHistoryList && orderStatusHistoryList.size() > 0) {
                shopOrderStatusHistoryMapper.insertBatch(orderStatusHistoryList);
            }

            logger.info("OrderUnPayTimerService >>>>>>>>>> 定时器结束执行 " + DateUtil.getDateTimeString1(new Date()));
        } catch (Exception e) {
            logger.error("定时器更新未支付订单异常!{}" + e.getMessage());
            Thread.currentThread().interrupt();
        }
    }

    /**
     * 订单状态历史记录
     * @param orderId
     * @return
     */
    private ShopOrderStatusHistory saveOrderStatusHistory(Integer orderId) {
        if (null == orderId) {
            return null;
        }
        ShopOrderStatusHistory shopOrderStatusHistory = new ShopOrderStatusHistory();
        shopOrderStatusHistory.setOrderId(orderId);
        shopOrderStatusHistory.setOrderStatusId(Constants.OrderStatus.STATUS_CANCEL);
        shopOrderStatusHistory.setRemark("timer auto cancel");
        shopOrderStatusHistory.setCreateBy(1);
        shopOrderStatusHistory.setCreateDate(new Date());
        shopOrderStatusHistory.setUpdateBy(1);
        shopOrderStatusHistory.setUpdateDate(new Date());

        return shopOrderStatusHistory;
    }

    /**
     * 订单状态更新
     * @param orderId
     * @return
     */
    private ShopOrder updateShopOrder(Integer orderId) {
        if (null == orderId) {
            return null;
        }
        ShopOrder shopOrder = new ShopOrder();
        shopOrder.setId(orderId);
        shopOrder.setOrderStatus(Constants.OrderStatus.STATUS_CANCEL);
        shopOrder.setUpdateDate(new Date());

        return shopOrder;
    }

}
cron表达式

Cron表达式是一个字符串,是由空格隔开的6或7个域组成,每一个域对应一个含义(秒 分 时 每月第几天 月 星期 年)其中年是可选字段。
(1)各域支持的字符类型

秒:可出现", - * /"四个字符,有效范围为0-59的整数

分:可出现", - * /"四个字符,有效范围为0-59的整数

时:可出现", - * /"四个字符,有效范围为0-23的整数

每月第几天:可出现", - * / ? L W C"八个字符,有效范围为0-31的整数

月:可出现", - * /"四个字符,有效范围为1-12的整数或JAN-DEc

星期:可出现", - * / ? L C #"四个字符,有效范围为1-7的整数或SUN-SAT两个范围。1表示星期天,2表示星期一, 依次类推

(2)特殊字符含义

* : 表示匹配该域的任意值,比如在秒*, 就表示每秒都会触发事件。;

? : 只能用在每月第几天和星期两个域。表示不指定值,当2个子表达式其中之一被指定了值以后,为了避免冲突,需要将另一个子表达式的值设为“?”;

- : 表示范围,例如在分域使用5-20,表示从5分到20分钟每分钟触发一次

/ : 表示起始时间开始触发,然后每隔固定时间触发一次,例如在分域使用5/20,则意味着5分,25分,45分,分别触发一次.

, : 表示列出枚举值。例如:在分域使用5,20,则意味着在5和20分时触发一次。

L : 表示最后,只能出现在星期和每月第几天域,如果在星期域使用1L,意味着在最后的一个星期日触发。

W : 表示有效工作日(周一到周五),只能出现在每月第几日域,系统将在离指定日期的最近的有效工作日触发事件。注意一点,W的最近寻找不会跨过月份

LW : 这两个字符可以连用,表示在某个月最后一个工作日,即最后一个星期五。

# : 用于确定每个月第几个星期几,只能出现在每月第几天域。例如在1#3,表示某月的第三个星期日。

cron常用表达式

0 0 12 * * ? 每天中午十二点触发
0 5 3 * * ? 每天3点5分执行
*/5 * * * * ?

每隔5秒执行一次

0 */1 * * * ? 每隔1分钟执行一次
0 0 1 * * ? 每天凌晨1点执行一次
0 0 1 1 * ? 每月1号凌晨1点执行一次
0 0 23 L * ? 每月最后一天23点执行一次
0 5/10 3 * * ? 每天3点的 5分,15分,25分,35分,45分,55分这几个时间点执行