定时器Scheduler在平时使用比较频繁(Spring定时任务:https://www.jianshu.com/p/570d6820b054 )在springboot中,配置好@Scheduled和@EnableScheduling之后,定时器就能正常执行,实现定时任务的功能。
但是在这样的情况下:如果开发的服务需要水平部署实现负载均衡,那么定时任务就会同时在多个服务实例上运行,那么一方面,可能由于定时任务的逻辑处理需要访问公共资源从而造成并发问题;另一方面,就算没有并发问题,那么一个同样的任务多个服务实例同时执行,也会造成资源的浪费。因此需要一种机制来保证多个服务实例之间的定时任务正常、合理地执行。
ShedLock的出现就是为了解决上述问题,它可以保证多个一个定时任务在多个服务实例之间最多只执行一次,是一个在分布式环境中保证定时任务合理执行的框架,我们可以叫它分布式定时任务锁。
ShedLock的实现原理是采用公共存储实现的锁机制,使得同一时间点只有第一个执行定时任务的服务实例能执行成功,并在公共存储中存储"我正在执行任务,从什么时候(预计)执行到什么时候",其他服务实例执行时如果发现任务正在执行,则直接跳过本次执行,从而保证同一时间一个任务只被执行一次。
上面提到的公共存储目前支持的有:
Monogo
DynamoDB
JdbcTemplate
ZooKeeper (using Curator)
Redis (using Spring RedisConnectionFactory)
Redis (using Jedis)
Hazelcast
值得注意的是,ShedLock不是一个分布式的定时任务框架,只是一个锁,用于保证分布式环境中的定时任务合理执行。
这里使用JdbcTemplate,新建springboot项目,pom文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>poc</artifactId>
<groupId>com.hkj</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>shedlock</artifactId>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- mysql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<version>2.2.2.RELEASE</version>
</dependency>
<!-- shedlock start -->
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-spring</artifactId>
<version>4.11.1</version>
</dependency>
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-provider-jdbc-template</artifactId>
<version>4.11.1</version>
</dependency>
<!-- shedlock end -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
shedlock配置类如下:
package com.hkj.config;
import net.javacrumbs.shedlock.core.LockProvider;
import net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.annotation.Resource;
import javax.sql.DataSource;
import java.util.TimeZone;
/**
* @descrition shedlock配置类
* @since 2020-06-01 15:19
*/
@Configuration
public class ShedlockConfig {
@Resource
private DataSource dataSource;
/**
* @description 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 )
* );
* @date 2020/6/1 15:19
*/
@Bean
public LockProvider lockProvider() {
return new JdbcTemplateLockProvider(
JdbcTemplateLockProvider.Configuration.builder()
.withJdbcTemplate(new JdbcTemplate(dataSource))
.withTimeZone(TimeZone.getTimeZone("GMT+8"))
.build()
);
}
}
在MySQL中添加一张名为shedlock的表
通过修改数据库表中的数据实现锁。
表字段说明:
主键name:每个定时任务的一个名字
locked_at:锁的开始时间
lock_until:锁的结束时间
再定时开始时,会更新这两个时间,在时间之内的定时是不会被执行的。
yml配置
server:
port: 8080
spring:
datasource:
name: test
url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: lovehkj
driver-class-name: com.mysql.cj.jdbc.Driver
启动类上开启shedlock和定时任务
package com.hkj;
import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
// 指定锁默认持有时间
@EnableSchedulerLock(defaultLockAtMostFor = "10m")
public class ShedLockStarter {
public static void main(String[] args) {
SpringApplication.run(ShedLockStarter.class, args);
}
}
新建定时任务
package com.hkj.job;
import net.javacrumbs.shedlock.spring.annotation.SchedulerLock;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @descrition
* @since 2020-06-01 15:09
*/
@Component
public class TestJob {
/**
* @description 每隔1min打印一次
* @date 2020/6/1 15:10
*/
@Scheduled(cron = "0 0/1 * * * ?")
// lockAtMostFor为锁默认持有时间,会覆盖启动类中的默认持有时间
@SchedulerLock(name = "print", lockAtMostFor = "3m")
public void print() {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//设置日期格式
System.err.println(df.format(new Date()));// new Date()为获取当前系统时间
}
}
@Scheduled注解有五个参数
name:定时任务的名字,就是数据库中的内个主键
lockAtMostFor:锁的最大时间单位为毫秒
lockAtMostForString:最大时间的字符串形式,例如:PT30S 代表30秒
lockAtLeastFor:锁的最小时间单位为毫秒
lockAtLeastForString:最小时间的字符串形式
分别启动两个实例8080和8081
观察console输出结果:
此时已经不会发生同一时间段多个实例执行同一调度任务的情况了。