1. 为什么需要分布式定时任务锁
在微服务架构中,我们经常会部署多个相同的服务实例来实现高可用。这时候如果服务中有定时任务,就会遇到一个典型问题:所有实例的定时任务会在同一时间触发,导致任务被重复执行。比如每天凌晨的报表生成任务,如果部署了3个实例,就可能生成3份相同的报表,这显然不是我们想要的结果。
我去年就遇到过这样的生产事故。当时一个数据同步任务被部署在5个节点上,由于没有加锁,导致相同的数据被反复同步了5次,不仅造成资源浪费,还引发了数据一致性问题。后来我们引入了ShedLock这个轻量级解决方案,完美解决了分布式环境下的定时任务重复执行问题。
ShedLock的工作原理很简单:它通过在Redis中设置一个临时锁来标记当前正在执行的任务。其他节点在执行相同任务时,会先检查这个锁是否存在。如果锁存在就直接跳过执行,避免重复劳动。这种机制就像多人协作时使用的"会议室预定系统"——第一个预定会议室的人会把会议室标记为"已占用",其他人看到这个标记就会选择其他时间。
2. 环境准备与依赖配置
2.1 基础环境要求
在开始集成之前,请确保你的开发环境满足以下条件:
- JDK 1.8或更高版本
- Maven 3.5+
- SpringBoot 2.x
- Redis服务器(可以是单机或集群)
我建议使用SpringBoot 2.3.4.RELEASE版本,这个版本经过长期验证比较稳定。如果你用的是SpringBoot 3.x,需要注意部分API可能有变化。
2.2 添加Maven依赖
首先需要在pom.xml中添加以下依赖:
<!-- ShedLock核心库 --> <dependency> <groupId>net.javacrumbs.shedlock</groupId> <artifactId>shedlock-spring</artifactId> <version>4.44.0</version> </dependency> <!-- Redis锁实现 --> <dependency> <groupId>net.javacrumbs.shedlock</groupId> <artifactId>shedlock-provider-redis-spring</artifactId> <version>4.44.0</version> </dependency> <!-- Spring Data Redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- 连接池 --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency>这里我推荐使用4.44.0版本,这是目前最新的稳定版。如果你项目已经使用了Jedis或Lettuce,可以省略spring-boot-starter-data-redis依赖。
3. Redis锁配置详解
3.1 基础配置类
创建一个配置类来设置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; @Configuration @EnableSchedulerLock(defaultLockAtMostFor = "PT30S") public class ShedLockConfig { @Bean public LockProvider lockProvider(RedisConnectionFactory connectionFactory) { return new RedisLockProvider(connectionFactory, "my-app"); } }这里有几个关键点需要注意:
@EnableSchedulerLock注解开启了ShedLock功能defaultLockAtMostFor设置了默认的最大锁持有时间为30秒- RedisLockProvider构造函数的第二个参数是环境标识,建议使用应用名
3.2 锁时间参数详解
ShedLock使用ISO8601持续时间格式来设置时间参数,这种格式看起来可能有点奇怪,但其实很容易理解:
- PT10S = 10秒
- PT5M = 5分钟
- PT1H = 1小时
在实际项目中,我建议这样设置时间参数:
lockAtLeastFor:设置为任务平均执行时间的80%lockAtMostFor:设置为任务最长可能执行时间的120%
这样可以避免任务执行时间波动导致的锁问题。
4. 定时任务实现
4.1 启用定时任务
首先确保在启动类上添加@EnableScheduling注解:
@SpringBootApplication @EnableScheduling public class MyApplication { public static void main(String[] args) { SpringApplication.run(MyApplication.class, args); } }4.2 编写带锁的定时任务
下面是一个完整的定时任务示例:
import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @Component public class ReportGenerationTask { @Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行 @SchedulerLock( name = "dailyReportGeneration", lockAtLeastFor = "PT20M", // 最少锁定20分钟 lockAtMostFor = "PT30M" // 最多锁定30分钟 ) public void generateDailyReport() { // 这里写报表生成逻辑 System.out.println("开始生成每日报表..."); // 模拟耗时操作 try { Thread.sleep(1000 * 60 * 15); // 15分钟 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println("每日报表生成完成"); } }4.3 参数调优建议
根据我的经验,设置锁时间时有几个最佳实践:
- 对于短周期任务(如每分钟执行),
lockAtLeastFor可以设置为周期的一半 - 对于长周期任务(如每天执行),
lockAtLeastFor可以设置为预估执行时间的1.5倍 - 总是设置
lockAtMostFor,这是防止节点崩溃导致锁无法释放的安全机制
5. 测试与验证
5.1 本地多实例测试
为了验证分布式锁是否生效,我们可以启动多个实例来测试:
- 在IDEA中,复制一个启动配置
- 在VM options中添加
-Dserver.port=8081 - 同时启动两个实例
观察日志输出,应该只有一个实例会执行定时任务。
5.2 查看Redis中的锁
可以通过Redis客户端查看锁的状态:
redis-cli KEYS shedlock*你会看到类似这样的键:
shedlock:dailyReportGeneration可以使用TTL命令查看锁的剩余时间:
TTL shedlock:dailyReportGeneration5.3 常见问题排查
如果发现锁不生效,可以检查以下几点:
- Redis连接是否正常
- 任务名称是否唯一
- 时间参数设置是否合理
- 系统时钟是否同步(在分布式环境中非常重要)
6. 生产环境最佳实践
6.1 Redis高可用配置
在生产环境中,建议使用Redis哨兵或集群模式:
@Bean public LockProvider lockProvider(RedisConnectionFactory connectionFactory) { return new RedisLockProvider.Builder(connectionFactory) .environment("prod") .build(); }6.2 监控与告警
建议对以下指标进行监控:
- 任务执行成功率
- 任务执行时间
- 锁等待时间
可以使用Spring Boot Actuator来暴露这些指标。
6.3 性能优化
对于高频任务,可以考虑以下优化:
- 减小锁的粒度(为不同任务设置不同的锁)
- 适当缩短锁的持有时间
- 使用本地缓存减少Redis访问
我在一个电商项目中通过优化锁配置,将定时任务的吞吐量提升了40%。
7. 高级用法与扩展
7.1 自定义锁提供者
如果需要更复杂的锁逻辑,可以实现自己的LockProvider:
public class CustomLockProvider implements LockProvider { @Override public Optional<SimpleLock> lock(LockConfiguration lockConfig) { // 自定义加锁逻辑 } }7.2 与Spring Retry集成
对于可能失败的任务,可以结合Spring Retry实现重试机制:
@Retryable(maxAttempts=3, backoff=@Backoff(delay=1000)) @SchedulerLock(name = "retryableTask") public void retryableTask() { // 可能失败的任务逻辑 }7.3 动态任务配置
通过配置中心可以实现动态调整任务参数:
@Scheduled(cron = "${reports.cron}") @SchedulerLock( name = "dynamicTask", lockAtLeastForString = "${reports.minLockTime}", lockAtMostForString = "${reports.maxLockTime}" ) public void dynamicTask() { // 任务逻辑 }在实际项目中,这种动态配置特别有用,可以不用重启服务就能调整任务执行策略。