前言
在业务生产中,定时器 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 不会并发执行。
参考资料