spring调度注解@Scheduled方式(含分布式)

 更新时间:2024年11月20日 17:19:25   作者:sjzwangxufeng  
文章介绍了Java中任务调度的几种常见方法,包括JDK原生的Timer、ScheduledThreadPoolExecutor和Spring的@Scheduled注解,文章还讨论了如何在分布式环境中实现任务调度,并介绍了一些开源的分布式任务调度解决方案,如Quartz和XXL-JOB

简述

任务调度就是在给定的时间或固定频率,执行业务逻辑,是比较常见的功能需求。

解决方案有jdk原生的Timer、ScheduledThreadPoolExecutor等,这些类常适用于一些内嵌的业务逻辑场景。

本文主要介绍注解@Scheduled,以上都是单进程解决方案,经过适当改造,也可以适用于分布式场景,可以满足大多数调度业务场景,具体实现思路下面会做简单叙述。

配置

开启

项目开启调度功能,需要先添加注解@EnableScheduling,否则调度注解@Scheduled就不起作用。

线程池

既然是任务运行,就会涉及线程处理,如果有不同类型的任务,也会出现并行处理,对线程的合理管理,就离不开线程池,以下是线程池配置整理

(1) 不配置(默认)

如果不做任何配置处理,spring-boot 会默认自动构建一个ThreadPoolTaskScheduler线程池类bean, 来管理这些运行任务的线程,默认线程池的具体参数值,可参考TaskSchedulingProperties类定义的默认值,如下:

// pool
private int size = 1;

// thread
private String threadNamePrefix = "scheduling-";

通过源码知道,这个默认线程池,内部实际由jdk的ScheduledThreadPoolExecutor类处理,该类采用无限容量队列,这也就限制了它的最大线程数不会超过1个,如果有耗时的并行任务,就不能满足要求,通常情况下,需要根据业务场景重新配置这些参数。

(2) spring配置

spring-boot项目已提供TaskSchedulingAutoConfiguration类,由它自动加载线程池配置参数,并构建ThreadPoolTaskScheduler线程池类bean,以下是约定的配置项:

spring:
  task:
    scheduling:
      threadNamePrefix: my-scheduler-task-
      pool:
        size: 3

线程池的大小,依据配置调度注解@Scheduled任务的数量,原则上有几种任务就需要几个线程,否则就会出现相互影响,长耗时任务占用线程,导致短耗时任务不能正常运行。

(3) java代码配置

调度任务不像@Async异常处理,它只有一个线程池,一般情况不用这种配置方式,以下是简单例子。

@Configuration
public class ScheduleConfig {
	
    private static final String THREAD_NAME_PREFIX = "my-scheduler-task-";	

    @Bean("myTaskScheduler")
    public ThreadPoolTaskScheduler getThreadPoolTaskScheduler() {
    	ThreadPoolTaskScheduler result = new ThreadPoolTaskScheduler();
    	result.setThreadNamePrefix(THREAD_NAME_PREFIX);
    	result.setPoolSize(3);
    	return result;
    }
}

调度规则

@Scheduled包含参数:

  • cron:定时任务,按cron表达式规则,定时运行任务,例如,每5分钟运行一次: 0/5 * * * * ?
  • fixedDelay:按固定间隔执行,就是两个相邻任务,前一个任务结束到下一个任务开始的间隔时间,单位: 毫秒。
  • fixedRate:按固定频率执行任务,单位: 毫秒。
  • initialDelay:系统启动后,延时多长时间运行第一次任务,单位: 毫秒。

其中:cron, fixedDelay, fixedRate 配置参数,只能三选一。

分布式

现在系统大多在分布式环境部署,就需要考虑多实例部署如何协调执行任务问题,以下是常见的解决方案,以及个人的思考。

第三方

目前第三方的开源方案,有早期比较经典的Quartz,近几年版本迭代不太活跃,也有后起之秀XXL-JOB 版本迭代比较活跃,也是目前很多公司推崇的解决方案,对任务的管理、监控、日志等功能比较齐全,可以参考其官方,这里就不再多述。

自处理

尽管上面开源的第三方解决方案,已经足够成熟、完善,但相对来说,还是有些重,对于一些系统规模不是很大,一些简单的任务调度需求,完全可以进行简单改造来满足这些任务调度功能。

尽管简单,它一样可以很实用、很健壮,以下是2种借助redis的处理思路。

(1) @Scheduled为主,redis为辅

通过@Scheduled注解的调度任务,在分布式环境运行,一个明显的问题,就是同一个任务,可能会在多个机器同时并发执行,如何避免,很自然就想到通过redis分布式锁处理,来避免任务并发执行,锁定时间可以设置0.75个执行周期,以下是伪码

	@Scheduled(fixedDelay = 60000, initialDelay = 1000)
	public void task1() {
		
		// 锁定
		boolean isLock = redisLock.lock("my-task-1", 60000 * 0.75);
		if (!isLock) return;
		
		// 任务逻辑
		doSomething();
	}

可以看出,这种方式,任务周期误差比较大,比较粗糙,特点就是逻辑简单,适用于精度要求较低的场景。

(2) redis为主,@Scheduled为辅

由于通过@Scheduled来配置执行周期,在分布式环境,很难保证周期的精度,这时候可以把@Scheduled仅作为尝试申请执行的一个定时扫描任务,真实的执行周期由redis的过期时间来管理,这种方式,任务周期精度就会好很多,以下是伪码:

按固定频率执行:

	/*
	 * redis为主,@Scheduled为辅(按固定频率执行任务)
	 * 
	 * note:
	 * a. @Scheduled注解中fixedDelay,该参数仅作为尝试申请执行任务, 通常可以设置小些。
	 * b. 任务执行周期或间隔,值为redisLock锁定的时间。
	 * 
	 */
	@Scheduled(fixedDelay = 5000, initialDelay = 1000)
	public void task2() {
		
		// 锁定
		boolean isLock = redisLock.lock("my-task-2", 真实任务周期);
		if (!isLock) return;
		
		// 任务逻辑
		doSomething();
		
	}

按固定间隔执行:

	/*
	 * redis为主,@Scheduled为辅(按固定间隔执行)
	 * 
	 * note:
	 * a. @Scheduled注解中fixedDelay,该参数仅作为尝试申请执行任务, 通常可以设置小些。
	 * b. 任务执行周期或间隔,值为redisLock锁定的时间。
	 * 
	 */
	@Scheduled(fixedDelay = 5000, initialDelay = 1000)
	public void task3() {
		
		// 锁定1: 避免任务并行
		boolean isLock = redisLock.lock("my-task-3", 真实任务间隔);
		if (!isLock) return;
		
		// 任务逻辑
		doSomething();
		
		// 锁定2: 间隔时间
		redisLock.expire("my-task-3", 真实任务间隔);
		
	}

按cron表达式执行:可通过注解@Scheduled参数fixedDelay,来调整周期精度。

	/*
	 * redis为主,@Scheduled为辅(cron表达)
	 * 
	 * note:
	 * a. @Scheduled注解中fixedDelay,该参数仅作为尝试申请执行任务, 通常可以设置小些。
	 * b. 任务执行周期或间隔,值为redisLock锁定的时间。
	 * c. 由CronHelper解析cron表达式,计算下一次运行间隔时间
	 */
	@Scheduled(fixedDelay = 5000, initialDelay = 1000)
	public void task4()  {
		
		// 锁定
		boolean isLock = redisLock.lock("my-task-4", CronHelper.getNextDelayTime());
		if (!isLock) return;
		
		// 任务逻辑
		doSomething();		
	}

以上只伪码,可以看出改造成本比较少,也足够灵活,其中RedisLock可以参考前面整理的文章:分布式锁-java,至于CronHelper类,网上应该有类似资源,也不妨自己实现一下,应该比排序算法有趣的多。

再就是任务的运行,不能保证负载均衡,如果的确有这方面需求,通过redis队列也可以实现,逻辑也不会太复杂。

个人认为:

这种自处理方式,借助redis还是可以保障它的高可用性、并发性能,它的主要缺陷,就是代码语义不够清晰,在维护上,容易受注解@Scheduled定时参数影响,实际业务场景,尽量封装一下,提高可读性。

常见问题

(1) 线程池的大小,建议几种任务就几个线程,多了也浪费,如果太小,任务耗时长时,就会出现任务间干扰。

(2) 如果任务有严格的并行限制,可以通过分布式锁防护一下。

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • java读取图片并显示方式

    java读取图片并显示方式

    这篇文章主要介绍了java读取图片并显示方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-11-11
  • SpringBoot Redis实现接口幂等性校验方法详细讲解

    SpringBoot Redis实现接口幂等性校验方法详细讲解

    这篇文章主要介绍了SpringBoot Redis实现接口幂等性校验方法,近期一个老项目出现了接口幂等性校验问题,前端加了按钮置灰,依然被人拉着接口参数一顿输出,还是重复调用了接口,通过复制粘贴,完成了后端接口幂等性调用校验
    2022-11-11
  • 解决Nacos成功启动但是无法访问 (Connection refused)

    解决Nacos成功启动但是无法访问 (Connection refused)

    这篇文章主要介绍了解决Nacos成功启动但是无法访问 (Connection refused)问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-06-06
  • spring boot(三)之Spring Boot中Redis的使用

    spring boot(三)之Spring Boot中Redis的使用

    这篇文章主要介绍了spring boot(三)之Spring Boot中Redis的使用,非常不错,具有参考借鉴价值,需要的朋友可以参考下
    2017-05-05
  • 使用springboot实现上传文件时校验文件是否有病毒

    使用springboot实现上传文件时校验文件是否有病毒

    在SpringBoot中实现文件上传时的病毒校验,可以使用ClamAV、Metascan或VirusTotal等工具,这些工具通过扫描上传的文件,可以有效地检测和阻止恶意软件的传播,安装和配置ClamAV服务的步骤如下:下载并安装ClamAV二进制文件,配置clamd.conf文件
    2025-01-01
  • 排序算法的Java实现全攻略

    排序算法的Java实现全攻略

    这篇文章主要介绍了排序算法的Java实现,包括Collections.sort()的使用以及各种经典算法的Java代码实现方法总结,超级推荐!需要的朋友可以参考下
    2015-08-08
  • Spring Cloud Alibaba Nacos两种检查机制

    Spring Cloud Alibaba Nacos两种检查机制

    这篇文章主要介绍了Spring Cloud Alibaba Nacos两种检查机制,作为注册中心不止提供了服务注册和服务发现功能,它还提供了服务可用性监测的机制,下面我们就一起进入文章了解具体详情吧
    2022-05-05
  • 简单了解JavaBean作用及常用操作

    简单了解JavaBean作用及常用操作

    这篇文章主要介绍了简单了解JavaBean作用及常用操作,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-05-05
  • Java 时间日期详细介绍及实例

    Java 时间日期详细介绍及实例

    这篇文章主要介绍了Java 时间日期详细介绍及实例的相关资料,需要的朋友可以参考下
    2017-01-01
  • 关于synchronized的参数及其含义

    关于synchronized的参数及其含义

    这篇文章主要介绍了synchronized的参数及其含义详解,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-10-10

最新评论