【SpringBoot项目】解决分布式定时任务重复执行的问题

参考 shedlock 官网:https://github.com/lukas-krecan/ShedLock

在分布式系统中部署定时任务时,所有的定时任务会在不同的节点上都执行一遍,以下是使用 shedlock 的解决方案:

要求:不使用 Redis。

第一步:引入 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 分钟。


作者:DougLeaMrConcurrency

来源链接:

https://blog.csdn.net/qq_43265673/article/details/108348919