spring boot使用@Scheduled实现定时任务

一.基本使用

使用前记得在Spring启动类中开启定时任务。

@EnableAsync

@scheduled注解支持不同方式的任务调度。

1.cron表达式

当方法的执行时间超过任务调度频率时,调度器会在下个周期执行。 例如:任务每3s执行一次,执行4s,则假设任务在第0s开始执行,下一次执行时间是第6s。

2.fixedRate

fixedRate是按照一定的速率执行,是从上一次方法执行开始的时间算起,如果上一次方法阻塞住了,下一次也是不会执行,但是在阻塞这段时间内累计应该执行的次数,当不再阻塞时,一下子把这些全部执行掉,而后再按照固定速率继续执行。

3.fixedDelay

fixedDelay控制方法执行的间隔时间,是以上一次方法执行完开始算起,如上一次方法执行阻塞住了,那么直到上一次执行完,并间隔给定的时间后,执行下一次。

二.线程配置

在实际项目中,一个应用实例中可能会使用@Scheduled会定义多个任务,在默认情况下,多个任务会共享同一个线程,当有一个任务阻塞时,所有的任务都无法得到执行。所以当存在多个任务时,需要做任务的线程配置。

1.任务内使用统一线程,任务间使用不同线程

每个定时任务会占用1个线程。但是相同的定时任务,执行的时候,还是在同一个线程中。例如:任务1和任务2开始运行,任务1卡死了,任务2正常运行结束,在下一个周期中,任务1继续卡死,需要等待上一次任务结束才会启动新的任务,而任务2正常运行。

1)实现SchedulingConfigurer接口。重写configureTasks方法

@Configuration
public class ScheduleConfig implements SchedulingConfigurer {
    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.setScheduler(Executors.newScheduledThreadPool(50));
    }
}

2)配置任务线程池

注意:此时任务方法上无需添加@Async注解。

@Configuration
@EnableAsync
public class ScheduleConfig {
    @Bean
    public TaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
        taskScheduler.setPoolSize(50);
        return taskScheduler;
    }
}

2.任务内和任务间都使用不同线程

如上所示,首先配置任务线程池,同时任务方法上需要添加@Async注解。此时每次运行一个任务,都会启动一个线程来运行,也就是说同一个任务也会启动多个线程。
例如:任务1和任务2一起执行,任务1卡住了,任务2正常运行结束,下一个周期,任务1会继续启动执行。但是任务1中的卡死线程越来越多,会导致50个线程池占满,还是会影响到定时任务。

@Async
@Scheduled(cron = "0/10 * * * * *")
public void task() { }

三.分布式任务锁

当应用以多实例的方式部署时,对于同一个任务,为了保证在同一时间,只有一个实例里的任务在运行,需要使用@SchedulerLock(name = "xxxx")
ShedLock的实现原理是采用公共存储实现的锁机制,使得同一时间点只有第一个执行定时任务的服务实例能执行成功,并在公共存储中存储"我正在执行任务,从什么时候(预计)执行到什么时候",其他服务实例执行时如果发现任务正在执行,则直接跳过本次执行,从而保证同一时间一个任务只被执行一次。
SchedLock目前支持的公共存储有:

  • Monogo
  • DynamoDB
  • JdbcTemplate
  • ZooKeeper
  • Redis
  • Hazelcast

笔者目前项目中使用的是JdbcTemplate,使用步骤如下:

1.引入依赖

spring-boot项目中引入maven依赖。

<dependency>
       <groupId>net.javacrumbs.shedlock</groupId>
       <artifactId>shedlock-spring</artifactId>
       <version>2.1.0</version>
   </dependency>
   <dependency>
       <groupId>net.javacrumbs.shedlock</groupId>
       <artifactId>shedlock-provider-jdbc-template</artifactId>
       <version>2.2.0</version>
   </dependency>

2.在Spring启动类中开启shedlock

@EnableSchedulerLock(defaultLockAtMostFor = "PT20M")

defaultLockAtMostFor表示成功获取锁,执行任务的节点所能拥有的独占锁的最长时间的字符串表达,该字段是为了防止获取锁的节点在执行任务时死掉,长时间不释放锁,该字段的值应大于任务正常运行的正常时间。如果获取锁执行任务的节点正常运行完任务,那么锁会被立刻释放。

3.新增shedlock配置类

@Configuration
public class ShedlockConfig {
    @Resource
    private DataSource dataSource;
    
    @Bean
    public LockProvider lockProvider() {
        return new JdbcTemplateLockProvider(dataSource);
    }
}

笔者的项目使用mysql作为数据库,dataSource表示mysql数据源,需要在mysql数据库中新建名为shedlock的表,作为分布式锁。建表语句如下:

CREATE TABLE shedlock(
    name VARCHAR(64) ,             -- 锁名称 ,name必须是主键
    lock_until TIMESTAMP(3) NULL,  -- 释放锁时间
    locked_at TIMESTAMP(3) NULL,   -- 获取锁时间
    locked_by  VARCHAR(255),       -- 上锁的应用实例
    PRIMARY KEY (name)
)

4.在定时任务上加锁

在使用@Schedule的定时任务上新增shedlock锁。

@SchedulerLock(name = "锁的名字")

name对应shedlock表中的name字段,注意:不同的定时任务不能重名。Spring应用启动后,应用实例获取到锁,会自动在数据库中insert数据,所以并不需要手动初始化数据。

5.获取锁/释放锁参数设置

shedlock锁主要涉及参数有:
lockAtMostFor:锁的最大时间单位为毫秒
lockAtMostForString:最大时间的字符串形式,例如:PT30S 代表30秒
lockAtLeastFor:锁的最小时间单位为毫秒
lockAtLeastForString:最小时间的字符串形式
汇总下来,主要是两个参数:最大锁定时间最小锁定时间,接下来笔者将从一个实例获取到锁为例,梳理一下这两个参数是如何作用的。
1)实例获取到锁,将locked_at设置为当前时间,将lock_until设置为当前时间+最大锁定时间;
2)实例运行完成定时任务后,更新lock_until字段为locked_at+max(最小锁定时间,实例运行任务时间),例如:最小锁定时间30s,实例获取锁时间只过了15s,那么就会取最小锁时间30s;否则用当前时间。这个就是为了保证最少也要锁最小锁定时间30s。如果实例死掉或者一直没有运行完,则lock_until不变;
3)当lock_until小于等于当前时间,表明原先实例已释放锁,其他实例可以通过sql来重新抢锁了

UPDATE  tableName  SET lock_until = 当前时间+最大锁定时间, locked_at = 当前时间, locked_by = 主机名 WHERE name = 锁名字 AND lock_until <= 当前时间

sql运行成功,表明该实例获取锁成功;
综上所述,最大锁定时间需要设置的小于定时任务的最长运行时间,该字段的设置也用于防止程序无法正常释放锁导致死锁。而最小锁定时间一般设置的很小,例如5-10s即可,用于解决各个实例间时钟不同步的问题。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值