背景与应用场景
在多实例的 Spring Boot 应用中,定时任务的并发执行往往带来数据不一致和资源竞争的问题。ShedLock作为一个专门为分布式环境设计的锁实现,能够在集群中为定时任务提供唯一性保护,从而实现“同一时间只有一个实例执行任务”的效果。对于微服务架构下的计划任务、数据汇聚、批处理等场景,Spring Boot 与 ShedLock 的整合可以显著提升作业的稳定性和可观测性。
在实际落地中,分布式锁的可用性直接决定任务的可靠性:如果锁不可用,任务会延迟执行,甚至丢失执行机会;如果锁粒度过大,可能影响系统吞吐。通过 ShedLock,我们可以在数据库或其他存储中使用可控的锁寿命来实现互斥执行,并在多实例之间实现一致的行为。

原理解析
锁的工作原理
ShedLock 通过在集中存储中维护一个名为 shedlock 的锁表来实现分布式互斥。每个被标记为需要锁的定时任务都有一个唯一名称,当任务调度发生时,LockProvider 尝试写入锁表并获取锁。若锁未超过锁定期,将允许某一个实例继续执行;其他实例将会跳过本轮执行,直到锁自动释放。
在实现上,ShedLock 使用数据库事务确保对锁的获取与释放具有原子性,锁的持续时间由 lockAtMostFor 指定,最小锁定时间由 lockAtLeastFor 指定,从而避免因瞬时异常导致的重复执行。通过这种设计,只有一个实例能在同一时刻拥有锁并执行任务。
一致性与时钟依赖
时钟的一致性是 ShedLock 成功的关键点之一。若各实例的系统时钟存在偏差,可能导致锁的过早释放或错误获取。因此,生产环境中通常建议在集群中使用 NTP 保持时钟同步,或者在锁实现中引入时钟容错策略。数据库事务的原子性结合时钟信息,确保锁的正确性。
此外,ShedLock 支持多种存储后端(如 JDBC、Mongo、Redis 等),通过 LockProvider 封装具体实现细节,开发人员只需要关注注解和配置即可完成集成。
在 Spring Boot 中的实现要点
依赖与版本选择
要在 Spring Boot 项目中集成 ShedLock,通常需要引入 shedlock-spring 和一个具体的锁提供者实现,如 JDBC 模板提供者。正确的版本选择会影响兼容性与功能特性,请优先选择与 Spring Boot 版本匹配的版本。下面给出典型依赖示例:
<dependencies><dependency><groupId>net.javacrumbs.shedlock</groupId><artifactId>shedlock-spring</artifactId><version>4.39.0</version></dependency><dependency><groupId>net.javacrumbs.shedlock</groupId><artifactId>shedlock-provider-jdbc-template</artifactId><version>4.39.0</version></dependency>
</dependencies>
通过上述依赖,Spring Boot 应用即可在计划任务中使用 ShedLock 提供的分布式锁能力。ShedLock 与 Spring 的调度体系无缝协作,无需修改现有的 @Scheduled 逻辑。
数据库表结构与事务要求
在 JDBC 提供者场景下,需要创建一个锁表来保存锁信息。典型的表结构如下,字段含义为:锁名、锁到期时间、锁定时间、锁定人等信息。表结构应具备唯一性/主键约束,便于快速查询与乐观并发控制。
CREATE TABLE shedlock (name VARCHAR(64) NOT NULL,lock_until TIMESTAMP(3) NULL,locked_at TIMESTAMP(3) NULL,locked_by VARCHAR(255) NULL,PRIMARY KEY (name)
);
另外,在应用启动或任务执行时,事务边界需要覆盖锁的获取与任务执行,以保证在同一轮调度中不会出现并发冲突。建议在数据源层开启自动提交策略,确保锁的释放与任务执行之间具备明确的原子性边界。
核心集成代码示例
下面给出一个核心的使用示例,展示如何在 Spring Boot 的定时任务中开启 ShedLock,确保同一时间只有一个实例执行某个任务:
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;@Component
public class MyTask {@Scheduled(cron = "0 0/5 * * * *")@SchedulerLock(name = "myTask", lockAtMostFor = "PT5M", lockAtLeastFor = "PT1M")public void doWork() {// 业务逻辑}
}
为了将锁提供者接入到 Spring Boot,常见的做法是配置一个 LockProvider bean,供 ShedLock 使用。如下示例展示了使用 JDBC 数据源的配置思路:
import javax.sql.DataSource;
import net.javacrumbs.shedlock.provider.jdbc.JdbcLockProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;@Configuration
@EnableScheduling
public class ShedLockConfig {@Beanpublic JdbcLockProvider lockProvider(DataSource dataSource) {return new JdbcLockProvider(dataSource);}
}
实战指南:从零到落地
从零到落地的步骤
第一个步骤是明确任务的执行粒度与时效约束,确定锁名称与锁定时长,避免锁的粒度过细导致频繁争抢,或者过粗导致并发利用不足。随后在 Spring Boot 项目中引入 ShedLock 相关依赖,创建 shedlock 表,并实现 LockProvider 的注入。最后在需要的 @Scheduled 方法上添加 @SchedulerLock 注解,完成集成。
为了确保生产环境的鲁棒性,建议在多实例部署时保持时钟同步,监控锁的获取/释放情况,以便在出现超时或活锁情况时快速定位并修复。
完整代码示例与演示
下面给出一个完整的落地示例片段,涵盖依赖、表结构、配置与使用,帮助你快速上手:
<dependencies><dependency><groupId>net.javacrumbs.shedlock</groupId><artifactId>shedlock-spring</artifactId><version>4.39.0</version></dependency><dependency><groupId>net.javacrumbs.shedlock</groupId><artifactId>shedlock-provider-jdbc-template</artifactId><version>4.39.0</version></dependency>
</dependencies>
CREATE TABLE shedlock (name VARCHAR(64) NOT NULL,lock_until TIMESTAMP(3) NULL,locked_at TIMESTAMP(3) NULL,locked_by VARCHAR(255) NULL,PRIMARY KEY (name)
);
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;@Component
public class MyTask {@Scheduled(cron = "0 0/5 * * * *")@SchedulerLock(name = "myTask", lockAtMostFor = "PT5M", lockAtLeastFor = "PT1M")public void doWork() {// 业务逻辑}
}
import javax.sql.DataSource;
import net.javacrumbs.shedlock.provider.jdbc.JdbcLockProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;@Configuration
@EnableScheduling
public class ShedLockConfig {@Beanpublic JdbcLockProvider lockProvider(DataSource dataSource) {return new JdbcLockProvider(dataSource);}
}
运维与性能优化
监控与故障排查
在生产环境中,应对锁的获取与释放进行可观测性设计,记录锁的状态变更、锁的拥有者与锁的剩余时间,以便在出现并发错位或锁超时时快速定位原因。定时任务的日志中应当包含锁相关信息,帮助你确认某轮是否由同一个实例执行。
若出现“锁未释放导致后续任务无法执行”的情形,优先检查时钟同步、数据库锁表的事务处理、以及锁的 lockUntil、lockedAt 字段的业务逻辑是否符合预期。
高并发与锁超时策略
对于高并发场景,锁的最大持续时间应该与任务实际耗时相匹配,避免出现长时间阻塞导致其它实例无法获得锁。通过在 @SchedulerLock 中配置 lockAtMostFor 与 lockAtLeastFor,可以在确保任务不重复执行的前提下,优化吞吐和响应时间。若任务执行时间不可控,可考虑将锁时间设置为相对保守的长度,并结合任务分片策略提升并发吞吐。
最后,定期对 ShedLock 相关参数进行回顾,例如锁名称、表结构、数据库性能、以及驱动版本更新,以确保分布式锁在版本演进和集群扩展时仍然稳定可靠。


