文章目录
概述
在分布式系统中,定时任务的重复执行是一个常见而棘手的问题。当多个服务实例同时运行同一个定时任务时,如果没有适当的协调机制,就会导致数据不一致、资源浪费甚至更严重的业务问题。幸运的是,ShedLock 为我们提供了一个优雅的解决方案,它是一个开源库,旨在帮助我们轻松地在分布式环境中实现分布式锁,确保定时任务只被执行一次。
Github
https://github.com/lukas-krecan/ShedLock
什么是 ShedLock?
ShedLock 是一个轻量级的 Java 库,它通过利用外部存储(如数据库、Redis、ZooKeeper 等)来协调多个应用实例之间的定时任务执行。它的核心思想是:在任务执行前尝试获取一个锁,如果获取成功则执行任务,否则跳过。这样就保证了在给定时间内,无论有多少个服务实例,只有一个实例能够成功执行该任务。
ShedLock 的工作原理
ShedLock 的工作原理非常直观:
- 定义锁: 在需要加锁的定时任务方法上添加
@SchedulerLock
注解,并指定一个唯一的name
。这个name
将作为锁的标识。 - 尝试获取锁: 当定时任务被触发时,ShedLock 会根据
name
尝试在配置的外部存储中创建一个锁记录。 - 成功获取锁: 如果创建成功,则表示当前实例获得了锁,任务开始执行。ShedLock 会在锁记录中记录执行时间、锁定时间等信息。
- 失败获取锁: 如果创建失败(即其他实例已经获取了该锁),则当前实例会跳过任务执行。
- 释放锁(可选): 任务执行完成后,ShedLock 会根据配置自动释放锁(通过删除或更新锁记录)。你也可以配置锁在一定时间后自动过期,防止死锁。
ShedLock 的主要特点
- 易于集成: ShedLock 可以与 Spring Scheduler、QuartZ 等主流的定时任务框架无缝集成。
- 多种存储支持: 支持多种后端存储,包括但不限于:
- JDBC: 最常用也是最推荐的方式,利用数据库表的唯一性约束实现锁。
- Redis: 适用于对性能要求较高,且已经使用 Redis 的项目。
- ZooKeeper: 基于 ZooKeeper 的分布式协调能力实现锁。
- MongoDB: 同样可以利用其文档特性实现锁。
- 灵活的配置: 可以配置锁的名称、锁定时间、最小执行间隔等。
- 幂等性支持: 即使任务在执行过程中发生故障,ShedLock 也能保证在下一个调度周期内重新获取锁并尝试执行,从而实现任务的幂等性。
- 健康检查: 可以配置在获取锁失败时抛出异常,以便进行监控和告警。
如何使用 ShedLock (Redis 存储)
1. 添加依赖
首先,在 pom.xml
文件中添加 ShedLock 的 Spring 和 Redis 依赖:
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-spring</artifactId>
<version>5.x.x</version> </dependency>
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-provider-redis-spring</artifactId>
<version>5.x.x</version> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
最新版本为 6.x 以上(需要 JDK 17+ 对应 Spring 6+)
2. 配置 ShedLock
在 Spring Boot 应用中,创建一个配置类来启用 ShedLock,并配置 Redis 作为锁的提供者:
import net.javacrumbs.shedlock.core.LockProvider;
import net.javacrumbs.shedlock.provider.redis.spring.RedisLockProvider;
import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.scheduling.annotation.EnableScheduling;
@Configuration
@EnableScheduling // 启用 Spring 的定时任务
@EnableSchedulerLock(defaultLockAtMostFor = "PT30S") // 启用 ShedLock,并设置默认锁最长持有时间为30秒
public class ShedLockConfig {
@Bean
public LockProvider lockProvider(RedisConnectionFactory connectionFactory) {
return new RedisLockProvider(connectionFactory);
}
}
@EnableSchedulerLock(defaultLockAtMostFor = "PT30S")
配置了默认的锁最长持有时间为 30 秒(ISO 8601 Duration 格式)。这意味着即使任务失败,锁也会在 30 秒后自动释放,防止死锁。
注意: 确保 application.properties
或 application.yml
中已经配置了 Redis 连接信息,例如:
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=
3. 在定时任务中使用 ShedLock
在定时任务方法上添加 @SchedulerLock
注解:
import net.javacrumbs.shedlock.spring.annotation.SchedulerLock;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class MyScheduledTasks {
@Scheduled(cron = "0 * * * * ?") // 每分钟执行一次
@SchedulerLock(name = "myRedisTaskName", lockAtMostFor = "PT5M", lockAtLeastFor = "PT1M")
public void executeMyTask() {
// 只有获取到锁的实例才能执行到这里
System.out.println("Executing my task at: " + System.currentTimeMillis());
// 模拟任务执行
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("My task finished at: " + System.currentTimeMillis());
}
}
name = "myRedisTaskName"
:这是锁的唯一标识符,ShedLock 会在 Redis 中以这个名称作为 Key 来操作。lockAtMostFor = "PT5M"
:指定该锁最多持有 5 分钟。如果任务执行时间超过 5 分钟,锁也会自动释放。lockAtLeastFor = "PT1M"
:指定该锁最少持有 1 分钟。即使任务很快完成,锁也会保持 1 分钟,避免在极短时间内重复执行。
ShedLock 的优势与适用场景
优势:
- 简单易用: 通过注解即可轻松实现分布式锁。
- 低侵入性: 无需修改现有的定时任务代码逻辑。
- 可靠性高: 借助外部存储的原子操作,保证了锁的可靠性。
- 高可扩展性: 支持多种后端存储,可以根据项目需求灵活选择。
- 性能优越(Redis): 使用 Redis 作为存储,通常能提供比关系型数据库更高的锁操作性能,适用于对并发要求较高的场景。
适用场景:
- 批量数据处理: 确保批量任务只被一个实例执行,避免数据重复处理。
- 定时数据同步: 保证数据同步任务的唯一性,防止数据混乱。
- 消息队列消费: 在消费者组模式下,防止消息被重复消费(虽然许多MQ自带幂等性)。
- 资源清理: 确保资源清理任务只被执行一次,避免误删或重复操作。
适合场景:简单分布式环境下对定时任务做互斥执行。
不适合:
-
对强一致性、高可用要求极高的任务调度(非完整调度系统): 某些复杂场景下推荐 Quartz/XxlJob 等分布式调度框架
-
时间配置谨慎: lockAtMostFor 必须大于任务最长执行时间;lockAtLeastFor 用于防止频繁重试产生重复执行。
-
时钟同步要求: 若集群节点时钟漂移大,可能锁判断失效。
总结
ShedLock 是一个非常实用的分布式锁解决方案,它能够有效地解决分布式系统中定时任务重复执行的问题,从而提升系统的稳定性和数据的一致性。通过简单配置和注解,就可以轻松地为你的定时任务加上一把“安全锁”,告别重复执行的烦恼!当性能和并发量是重要考虑因素时,选择 Redis 作为 ShedLock 的存储后端是一个非常明智的选择。
如果你正在构建分布式系统,并且遇到了定时任务的并发执行问题,ShedLock 绝对值得你尝试!