前言
在分布式系统中,定时任务的执行往往需要考虑到多个实例的并发执行问题。假设一个定时任务会在多个节点上并发执行,可能导致重复执行,甚至引发数据异常或系统不一致问题。为了解决这一问题,ShedLock 是一个简单而有效的解决方案,它可以确保在分布式环境中,只有一个节点在某一时刻执行指定的定时任务
什么是 ShedLock?
ShedLock 是一个轻量级的 Java 库,用于解决分布式系统中定时任务的重复执行问题。它的核心思想是在数据库中加锁,确保在分布式环境下,只有一个节点能够在指定时间执行某个任务。ShedLock 可以与 Spring Scheduler、Quartz 等定时任务框架结合使用。
github开源地址:https://github.com/lukas-krecan/ShedLock 目前拥有 3.7K Star,小伙伴们也可以针对文档更系统性的学习如何应用ShedLock。
ShedLock 的工作原理:
- 定时任务执行时,ShedLock 会尝试在数据库中为该任务获取锁。
- 如果当前节点成功获取到锁,则执行定时任务。
- 如果当前节点未能获取到锁(其他节点已获取锁),则该任务跳过执行,等待下次调度。
定时任务重复执行的问题
在分布式应用中,定时任务往往通过多个实例运行,每个实例都会按照自己的调度计划执行任务。例如,假设有一个定时任务负责同步某个外部系统的数据。如果该任务在多个实例上同时执行,就会导致重复的数据同步,进而造成数据不一致、重复处理等问题。
典型场景
我们来模拟一个场景,假设在一个电商系统中,定时任务负责将库存数据同步到第三方库存管理系统。如果该任务在多个节点上同时执行,可能会导致:
1、重复的库存更新请求。
2、数据不同步或数据丢失。
3、系统负载过高,造成性能瓶颈。
使用 ShedLock 解决定时任务重复执行问题
接下来,将通过 Spring Boot 和 ShedLock 来演示如何解决定时任务的重复执行问题。希望能给大家一个参考!
添加依赖
首先确保我们的的 pom.xml 中包含必要的依赖。需要添加 Spring Boot Starter、Spring Scheduling 和 ShedLock 相关的依赖。
java
<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Scheduler -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-scheduled</artifactId>
</dependency>
<!-- ShedLock Core -->
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-spring</artifactId>
<version>6.2.0</version>
</dependency>
<!-- ShedLock JDBC -->
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-provider-jdbc-template</artifactId>
<version>6.2.0</version>
</dependency>
<!-- MySQL JDBC Driver -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
对应版本兼容性依赖:

配置数据库
ShedLock 需要在数据库中存储锁的信息。这里博主使用 MySQL 来存储锁。首先,创建一个名为 shedlock 的表来存储锁数据。
java
CREATE TABLE shedlock (
name VARCHAR(64) PRIMARY KEY,
lock_until TIMESTAMP(3) NOT NULL,
locked_at TIMESTAMP(3) NOT NULL,
locked_by VARCHAR(255) NOT NULL
);
该表的字段意义如下:
● name: 锁的名称,用于区分不同的任务。
● lock_until: 锁的过期时间,任务完成后应释放锁。
● locked_at: 锁的创建时间。
● locked_by: 锁被哪个节点持有。
配置 ShedLock
接下来,我们在 Spring Boot 配置类中进行 ShedLock 的配置。以下是一个基本的配置类:
java
import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
@Configuration
@EnableScheduling
@EnableSchedulerLock(defaultLockAtMostFor = "30m") //默认锁最大过期时间为 30 秒
public class ShedLockConfig {
}
注解说明:
@EnableSchedulerLock: 启用 ShedLock 功能,defaultLockAtMostFor 参数指定锁的最大过期时间。如果任务执行超过此时间,锁将会被释放。
@EnableScheduling: 启用 Spring 的调度功能。
创建定时任务
接下来我们创建一个定时任务,并为其添加 ShedLock 锁,确保在分布式环境中只有一个实例会执行该任务。
java
package com.example;
import net.javacrumbs.shedlock.spring.annotation.SchedulerLock;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
@Service
public class DataSyncService {
/**
* 使用 @SchedulerLock 注解来确保该任务只会在一个节点上执行
*/
@Scheduled(fixedRate = 5000) // 每 5 秒执行一次
@SchedulerLock(name = "syncInventoryTask", lockAtMostFor = "5m", lockAtLeastFor = "5m")
public void syncInventory() {
System.out.println("同步库存数据...");
// 模拟库存同步操作
try {
Thread.sleep(3000); // 模拟执行耗时 3 秒
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("库存同步完成!");
}
}
上述代码中,我们在在 syncInventory 方法上,我们使用了 @SchedulerLock 注解,指定锁的名称 syncInventoryTask,并设置锁的最大持续时间为 5 秒。
- name: 锁的名称,确保每个定时任务有唯一的名称。
- lockAtMostFor: 锁的最大过期时间,防止任务因为异常而无法释放锁。
- lockAtLeastFor: 锁的最小持续时间,确保锁至少保持一定时间。
配置数据库连接
最后 application.properties 或 application.yml 配置我们的数据库连接信息
java
spring.datasource.url=jdbc:mysql://localhost:3306/your_database
spring.datasource.username=root
spring.datasource.password=your_password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.hibernate.ddl-auto=update