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

文章介绍了Spring中使用@Scheduled注解进行任务调度的配置和规则,包括开启调度、线程池配置和不同调度参数的使用。在分布式环境中,文章探讨了如何通过第三方库如Quartz和XXL-JOB,以及结合Redis实现简单的分布式任务协调,提供了两种基于Redis的解决方案,以避免任务并发执行。
摘要由CSDN通过智能技术生成

1 简述

任务调度就是在给定的时间或固定频率,执行业务逻辑,是比较常见的功能需求。解决方案有jdk原生的Timer、ScheduledThreadPoolExecutor等,这些类常适用于一些内嵌的业务逻辑场景,本文主要介绍注解@Scheduled,以上都是单进程解决方案,经过适当改造,也可以适用于分布式场景,可以满足大多数调度业务场景,具体实现思路下面会做简单叙述。

2 配置

2.1 开启

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

2.2 线程池

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

(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;
    }
}

2.3 调度规则

@Scheduled包含参数:

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

initialDelay:系统启动后,延时多长时间运行第一次任务,单位: 毫秒。

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

3 分布式

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

3.1 第三方

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

3.2 自处理

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

尽管简单,它一样可以很实用、很健壮,以下是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定时参数影响,实际业务场景,尽量封装一下,提高可读性。

4 常见问题

(1) 线程池的大小,建议几种任务就几个线程,多了也浪费,如果太小,任务耗时长时,就会出现任务间干扰。
(2) 如果任务有严格的并行限制,可以通过分布式锁防护一下。

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
@Scheduled注解Spring框架提供的一种方式,用于实现定时任务的调度。它可以配合分布式任务调度框架来实现分布式定时器的功能。 要实现分布式定时器,可以使用以下步骤: 1. 首先,需要引入Spring框架和相关的依赖。可以在项目的pom.xml文件中添加spring-boot-starter和spring-boot-starter-web依赖。 ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> ``` 2. 创建一个定时任务类,使用@Scheduled注解标记需要定时执行的方法。该方法会根据注解中的cron表达式或固定的时间间隔来触发执行。 ```java @Component public class MyScheduler { @Scheduled(cron = "0 0 12 * * ?") // 每天中午12点触发执行 public void myTask() { // 定时任务逻辑 } } ``` 3. 在Spring Boot的启动类上添加@EnableScheduling注解,启用定时任务的调度功能。 ```java @SpringBootApplication @EnableScheduling public class MyApplication { public static void main(String[] args) { SpringApplication.run(MyApplication.class, args); } } ``` 4. 配置分布式任务调度框架,例如Quartz或Elastic-Job,来实现分布式定时任务的调度和执行。具体的配置方式和使用方法可以参考对应框架的文档。 通过以上步骤,就可以使用@Scheduled注解实现分布式定时器的功能了。定时任务会在指定的时间触发执行,无论是单节点还是多节点部署,都能够按照设定的规则进行执行。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值