前言
在业务生产中,定时器 Scheduler 的使用频率非常高。Spring 也为我们提供了默认的定时器。只需要加上 @Scheduled 和 @EnableScheduling 两个注解,即可快速运行。在单实例的服务中,官方的提供的定时任务可以非常方便的使用。
但是在如今分布式多实例的环境中,使用这种定时任务,则每个实例都定时并发执行,做着相同的事情,这必然不是我们想要的效果。这时你一定会想办法,让定时任务每次只在一个服务中运行,例如:每个服务通过配置文件来做为定时的开关、或是通过数据库实现分布式锁,这都是非常不错的选择。
本文将介绍一个更方便的组件,在 SpringBoot 工程中,你仅需要通过少量的代码即可实现分布式定时器功能。
还等什么呢,即刻开始!
ShedLock 简介
ShedLock 的作用,确保任务在同一时刻最多执行一次。如果一个任务正在一个节点上执行,则它将获得一个锁,该锁将阻止从另一个节点(或线程)执行同一任务。如果一个任务已经在一个节点上执行,则在其他节点上的执行不会等待,只需跳过它即可 。
ShedLock 使用 Mongo,JDBC 数据库,Redis,Hazelcast,ZooKeeper 或其他外部存储进行协调,即通过外部存储来实现锁机制。
官方传送门:lukas-krecan/ShedLock: Distributed lock for your scheduled tasks
ShedLock 快速集成
本文将以 shedlock + mysql 为例,在 SpringBoot 中快速集成分布式定时任务。如果想
**核心思想:**通过对多个实例公共的数据库的 shedlock 表进行添加数据库锁,使得同一个定时任务在同一个时间点只有一个实例执行。
1. 引入依赖
这里引入了 shedlock-spring / shedlock-provider-redis-spring 两个依赖,shedlock-provider 也提供了丰富的 Lock Providers,例如:Redis、JdbcTemplate、Mongo 等等
1 2 3 4 5 6 7 8 9 10
   | <dependency>     <groupId>net.javacrumbs.shedlock</groupId>     <artifactId>shedlock-spring</artifactId>     <version>4.29.0</version> </dependency> <dependency>     <groupId>net.javacrumbs.shedlock</groupId>     <artifactId>shedlock-provider-jdbc-template</artifactId>     <version>4.29.0</version> </dependency>
   | 
 
2. 配置数据库连接信息
resources/application.yml
1 2 3 4 5 6 7
   | spring:   datasource:     url: jdbc:mysql://127.0.0.1:3306/vote_app?useSSL=false&serverTimezone=UTC&characterEncoding=UTF8     username: ******     password: ******     driver-class-name: com.mysql.cj.jdbc.Driver     type: com.mysql.cj.jdbc.MysqlDataSource
   | 
 
如果 SpringBoot 工程已经集成了 MySQL 则可以跳过这一步。
3. 创建 MySQL 数据表
1 2 3
   |  CREATE TABLE shedlock(name VARCHAR(64) NOT NULL, lock_until TIMESTAMP(3) NOT NULL,     locked_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), locked_by VARCHAR(255) NOT NULL, PRIMARY KEY (name));
 
  | 
 
4. ShedLockConfig 配置类,配置 lockProvider
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
   | import net.javacrumbs.shedlock.core.LockProvider; import net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider; import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.scheduling.annotation.EnableScheduling;
  import javax.sql.DataSource;
 
 
 
 
 
 
 
 
  @Configuration
  @EnableScheduling
  @EnableSchedulerLock(defaultLockAtMostFor = "PT30S") public class ShedlockJdbcConfig {     
 
      @Bean     public LockProvider lockProvider(DataSource dataSource) {         return new JdbcTemplateLockProvider(                 JdbcTemplateLockProvider.Configuration.builder()                         .withJdbcTemplate(new JdbcTemplate(dataSource))                         .withTableName("system_shedlock")                          .usingDbTime()                         .build()         );     } }
   | 
 
5. MainApplication 启动类配置
1 2 3 4 5 6 7 8
   | @EnableScheduling public class MainApplication {
      public static void main(String[] args) {         SpringApplication.run(LatticyApplication.class, args);     }
  }
   | 
 
6. 创建定时任务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
   | import lombok.extern.slf4j.Slf4j; import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component;
 
 
 
 
 
 
 
 
 
  @Component @Slf4j public class TaskJobDemo {
      private static Integer count = 1;
                          
      
 
 
 
      @Scheduled(cron = "0/5 * * * * ? ")     @SchedulerLock(name = "testJob1", lockAtLeastFor = "20000", lockAtMostFor = "30000")     public void scheduledTask1() {         log.info(Thread.currentThread().getName() + "->>>任务1执行第:" + (count++) + "次");     }
      
 
      @Scheduled(cron = "0/5 * * * * ?")     @SchedulerLock(name = "shedlock-demo")     public void scheduledTask2() {         log.info(Thread.currentThread().getName() + "->>>任务2执行第:" + (count++) + "次");     }
  }
   | 
 
@SchedulerLock 注解有五个参数
- name:定时任务的名字,就是数据库中的内个主键
 
- lockAtMostFor:锁的最大时间单位为毫秒
 
- lockAtMostForString:最大时间的字符串形式,例如:PT30S 代表30秒
 
- lockAtLeastFor:锁的最小时间单位为毫秒
 
- lockAtLeastForString:最小时间的字符串形式
 
* 7. 让你的定时任务并行执行
这里有个小坑,默认 schedule 是单线程。如果你在多个函数上使用了 @Scheduled,定时任务是顺序执行,只有等定时任务 1 执行完成才执行任务 2。若其中一个定时任务阻塞,会影响其他的定时任务。因此,我们必须对定时任务进行配置,使定时任务互相不干扰。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
   | import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.SchedulingConfigurer; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.scheduling.config.ScheduledTaskRegistrar;
  import java.util.concurrent.Executors;
  @Configuration public class ScheduleConfig implements SchedulingConfigurer {
 
 
 
 
      @Override     public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {         taskRegistrar.setScheduler(this.getTaskScheduler());     }
      private ThreadPoolTaskScheduler getTaskScheduler() {         ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();         taskScheduler.setPoolSize(5);         taskScheduler.setThreadNamePrefix("schedule-pool-");         taskScheduler.afterPropertiesSet();         return taskScheduler;     } }
   | 
 
至此 @Scheduled 可以并发执行了,最高并发度是 20,但是同一个 @Schedule 不会并发执行。
参考资料