一、前言
在实际开发中由于项目部署在分布式或集群服务器上,代码中如果使用spring-boot的schedule定时任务,那么就会导致定时任务多次触发。解决这个问题思路很简单,就是通过分布式锁,多个应用实例上的定时任务在执行前先去获取锁,哪个实例获取到了锁,哪个实例上的定时任务去执行。
二、Shedlock介绍
开源地址:Github - lukas-krecan/ShedLock
原文介绍(翻译):ShedLock只做一件事。它确保您的计划任务最多同时执行一次。如果正在一个节点上执行任务,它将获取一个锁,以防止从另一个节点(或线程)执行相同的任务。请注意,如果一个任务已在一个节点上执行,则其他节点上的执行不会等待,只会跳过它。
三、Shedlock使用
官方使用说明:
To use ShedLock, you do the following 1. Enable and configure Scheduled locking 2. Annotate your scheduled tasks 3. Configure a Lock Provider
我这里分为引入并配置Shedlock 、定时任务添加@SchedulerLock
的注解和配置LockProvider锁实现三个使用步骤,下面就实战一下吧。
四、使用步骤
引入并配置Shedlock
先引入依赖
# maven
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-spring</artifactId>
<version>4.36.0</version>
</dependency>
# gradle
implementation 'net.javacrumbs.shedlock:shedlock-spring:4.36.0'
然后启动类添加注解支持
@SpringBootApplication
@EnableScheduling //开启定时任务
@EnableSchedulerLock(defaultLockAtMostFor = "10m") //开启ShedLock 并配置锁默认最大持有时间为10分钟
public class StartApplication {
...
}
定时任务添加@SchedulerLock注解
@Scheduled(cron="0 0/5 * * * ? ")
@SchedulerLock(name = "testTask", lockAtLeastFor = "1m", lockAtMostFor = "4m")
public void testTask() {
...
}
@SchedulerLock
参数说明:
属性 | 类型 | 说明 |
name | String | 锁的名称,必须指定,每次只能执行一个具有相同名字的任务。 |
lockAtLeastFor | String | 配置规则参考下方时间配置。指定保留锁的最短时间。主要目的是在任务非常短的且节点之间存在时钟差异的情况下防止多个节点执行。这个属性是锁的持有时间。设置了多少就一定会持有多长时间,再此期间,下一次任务执行时,其他节点包括它本身是不会执行任务的。 |
lockAtMostFor | String | 配置规则参考下方时间配置。设置锁的最大持有时间,为了解决如果持有锁的节点挂了,无法释放锁,其他节点无法进行下一次任务。 |
配置LockProvider锁实现
Shedlock提供了以下锁实现:
我这里使用的Redis (using Spring RedisConnectionFactory) 实现LockProvider。
先引入Redis LockProvider 依赖
# maven
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-provider-redis-spring</artifactId>
<version>4.36.0</version>
</dependency>
# gradle
implementation 'net.javacrumbs.shedlock:shedlock-provider-redis-spring:4.36.0'
配置LockProvider
@Configuration
public class ShedLockConfiguration {
@Bean
public LockProvider lockProvider(RedisConnectionFactory connectionFactory) {
return new RedisLockProvider(connectionFactory);
}
}
或者 配置自定义 environment 和 keyPrefix
@Configuration
public class ShedLockConfiguration {
@Bean
public LockProvider lockProvider(RedisConnectionFactory connectionFactory) {
return new RedisLockProvider(connectionFactory, "default","job-lock");
}
}
environment 和 keyPrefix 主要用于Redis中任务LockKey的自定义名称,源代码如下:
// net.javacrumbs.shedlock.provider.redis.spring.RedisLockProvider
/**
* Uses Redis's `SET resource-name anystring NX PX max-lock-ms-time` as locking mechanism.
* See https://redis.io/commands/set
*/
public class RedisLockProvider implements LockProvider {
...
@Override
@NonNull
public Optional<SimpleLock> lock(@NonNull LockConfiguration lockConfiguration) {
String key = buildKey(lockConfiguration.getName());
Expiration expiration = getExpiration(lockConfiguration.getLockAtMostUntil());
if (TRUE.equals(tryToSetExpiration(redisTemplate, key, expiration, SET_IF_ABSENT))) {
return Optional.of(new RedisLock(key, redisTemplate, lockConfiguration));
} else {
return Optional.empty();
}
}
...
String buildKey(String lockName) {
return String.format("%s:%s:%s", keyPrefix, environment, lockName);
}
...
}
最后启动测试即可。
五、时间配置
defaultLockAtMostFor
、defaultLockAtLeastFor
、lockAtLeastFor
、lockAtMostFor
配置规则如下:
时间+单位 如:
1s
,5ms
,5m
,1d
(从 4.0.0 开始)以毫秒为单位的时间 如:
100
(仅限 Spring 集成)ISO-8601 如:
PT15M
(参见Duration.parse()文档)
六、注意事项
ShedLock中的锁具有过期时间,这可能会导致以下问题:
如果任务运行时间超过
lockAtMostFor
,则可以多次执行任务如果两个节点之间的时钟差大于
lockAtLeastFor
或最小的执行时间,则可以多次执行任务。
七、其他
可参考官方文档:https://github.com/lukas-krecan/ShedLock/blob/master/README.md
评论区