SpringBoot 集成 ShedLock @SchedulerLock 分布式锁(基于Redis的方式)
1. 为什么需要分布式定时任务锁在微服务架构中我们经常会部署多个相同的服务实例来实现高可用。这时候如果服务中有定时任务就会遇到一个典型问题所有实例的定时任务会在同一时间触发导致任务被重复执行。比如每天凌晨的报表生成任务如果部署了3个实例就可能生成3份相同的报表这显然不是我们想要的结果。我去年就遇到过这样的生产事故。当时一个数据同步任务被部署在5个节点上由于没有加锁导致相同的数据被反复同步了5次不仅造成资源浪费还引发了数据一致性问题。后来我们引入了ShedLock这个轻量级解决方案完美解决了分布式环境下的定时任务重复执行问题。ShedLock的工作原理很简单它通过在Redis中设置一个临时锁来标记当前正在执行的任务。其他节点在执行相同任务时会先检查这个锁是否存在。如果锁存在就直接跳过执行避免重复劳动。这种机制就像多人协作时使用的会议室预定系统——第一个预定会议室的人会把会议室标记为已占用其他人看到这个标记就会选择其他时间。2. 环境准备与依赖配置2.1 基础环境要求在开始集成之前请确保你的开发环境满足以下条件JDK 1.8或更高版本Maven 3.5SpringBoot 2.xRedis服务器可以是单机或集群我建议使用SpringBoot 2.3.4.RELEASE版本这个版本经过长期验证比较稳定。如果你用的是SpringBoot 3.x需要注意部分API可能有变化。2.2 添加Maven依赖首先需要在pom.xml中添加以下依赖!-- ShedLock核心库 -- dependency groupIdnet.javacrumbs.shedlock/groupId artifactIdshedlock-spring/artifactId version4.44.0/version /dependency !-- Redis锁实现 -- dependency groupIdnet.javacrumbs.shedlock/groupId artifactIdshedlock-provider-redis-spring/artifactId version4.44.0/version /dependency !-- Spring Data Redis -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency !-- 连接池 -- dependency groupIdorg.apache.commons/groupId artifactIdcommons-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.port8081同时启动两个实例观察日志输出应该只有一个实例会执行定时任务。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 自定义锁提供者如果需要更复杂的锁逻辑可以实现自己的LockProviderpublic class CustomLockProvider implements LockProvider { Override public OptionalSimpleLock lock(LockConfiguration lockConfig) { // 自定义加锁逻辑 } }7.2 与Spring Retry集成对于可能失败的任务可以结合Spring Retry实现重试机制Retryable(maxAttempts3, backoffBackoff(delay1000)) SchedulerLock(name retryableTask) public void retryableTask() { // 可能失败的任务逻辑 }7.3 动态任务配置通过配置中心可以实现动态调整任务参数Scheduled(cron ${reports.cron}) SchedulerLock( name dynamicTask, lockAtLeastForString ${reports.minLockTime}, lockAtMostForString ${reports.maxLockTime} ) public void dynamicTask() { // 任务逻辑 }在实际项目中这种动态配置特别有用可以不用重启服务就能调整任务执行策略。