参考shedlock官网:https://github.com/lukas-krecan/ShedLock
在分布式系统中部署定时任务时,所有的定时任务会在不同的节点上都执行一遍,以下是使用shedlock的解决方案:
要求:不使用Redis。
方法二(推荐):使用xxl-Job,你值得拥有!
第一步:引入shedlock包
maven中pom文件添加如下配置:
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-spring</artifactId>
<version>2.2.1</version>
</dependency>
第二步:添加shedlock-provider-jdbc-template依赖(以JDBC为例)
maven中pom文件添加如下配置:
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-provider-jdbc-template</artifactId>
<version>2.2.1</version>
</dependency>
若使用的是其他类型的数据库,需要添加的依赖也不同,以MongoDB为例:MongoDB的依赖如下:
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-provider-mongo</artifactId>
<version>4.14.0</version>
</dependency>
具体的依赖请参考shedlock官网:https://github.com/lukas-krecan/ShedLock#jdbctemplate
第三步:向数据库中插入表shedlock(必须)
建表语句如下:
CREATE TABLE shedlock(
NAME VARCHAR(64),
lock_until TIMESTAMP(3) NULL,
locked_at TIMESTAMP(3) NULL,
locked_by VARCHAR(255),
PRIMARY KEY (NAME)
)
若没有创建该表则会报错。
正常情况下完成后续操作以后查看shedlock表可以看到数据:
第四步:添加配置类
代码如下:
import net.javacrumbs.shedlock.core.LockProvider;
import net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider;
import net.javacrumbs.shedlock.spring.ScheduledLockConfiguration;
import net.javacrumbs.shedlock.spring.ScheduledLockConfigurationBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.time.Duration;
/**
* Created by
* Created 2020-09-01-17:10
* Modify:
*/
@Component
public class ShedLockConfig {
@Bean
public LockProvider lockProvider(DataSource dataSource) {
return new JdbcTemplateLockProvider(dataSource);
}
@Bean
public ScheduledLockConfiguration scheduledLockConfiguration(LockProvider lockProvider) {
return ScheduledLockConfigurationBuilder.withLockProvider(lockProvider)
.withPoolSize(10)
.withDefaultLockAtMostFor(Duration.ofMinutes(10))
.build();
}
}
第五步:在启动类上添加@EnableSchedulerLock启动注解
否则SchedulerLock不会生效,注解如下:
@EnableSchedulerLock(defaultLockAtMostFor = "PT50S")
第六步:添加@SchedulerLock到定时器业务方法入口
在定时任务的方法中添加如下注解:
@SchedulerLock(name = "scheduledTask", lockAtMostFor = ?, lockAtLeastFor = ?)
相关参数说明:
name属性:锁名称,必须指定,每次只能执行一个具有相同名字的任务,锁名称应该是全局唯一的;
lockAtMostFor属性:设置锁的最大持有时间,为了解决如果持有锁的节点挂了,无法释放锁,其他节点无法进行下一次任务;
lockAtMostForString属性:成功执行任务的节点所能拥有的独占锁的最长时间的字符串表达,例如“PT14M”表示为14分钟
lockAtLeastFor属性:指定保留锁的最短时间。主要目的是在任务非常短的且节点之间存在时钟差异的情况下防止多个节点执行。这个属性是锁的持有时间。设置了多少就一定会持有多长时间,再此期间,下一次任务执行时,其他节点包括它本身是不会执行任务的
lockAtLeastForString属性:成功执行任务的节点所能拥有的独占锁的最短时间的字符串表达,例如“PT14M”表示为14分钟
第七步:测试
本次遇到的问题是使用Scheduled定时发送邮件,在分布式系统中会根据节点数使每个节点发送重复的邮件,这样明显不符合业务要求。本次测试的定时任务代码如下:
@Scheduled(fixedDelay = 3 * 60 * 1000, initialDelay = 60 * 1000)
@SchedulerLock(name = "scheduledTask", lockAtMostFor = 60000, lockAtLeastFor = 60000)
public void downloadScheduled() {
System.err.println("localhost:8080 已发送测试邮件,请检查是否收到两封?");
artificialPeekService.artificialPeekAudit(361655195741552640L, false);
}
本地启动两个相同的项目来模仿分布式系统,不同的是两个项目的端口号不同,相关的yml中端口号配置如下:
同时启动两个项目:
一段时间后,查看控制台输出,发现两个节点都执行了该定时任务,其中一个节点执行了1次另一个节点执行了3次:
在看看各节点执行时间间隔:
localhost:8080节点的执行时间分别为
2020-09-01 19:58:28、2020-09-01 20:01:31、2020-09-01 20:04:32
localhost:8081节点的执行时间为2020-09-01 20:07:32
一段时间后,localhost:8080执行了第4次,时间为2020-09-01 20:10:32
可以看到两个节点中任意两次任务的执行时间间隔为3分钟。
再检查邮件发送时间间隔,也均为3分钟。