SpringBoot 集成 ShedLock,完美解决定时任务重复执行难题

16
21·一、背景
在分布式系统中,定时任务的执行是常见需求,例如生成定期报表、清理过期数据或执行系统维护脚本。然而,随着微服务架构的普及,应用往往部署为多个实例,这带来了一个问题:如何确保同一时间仅有一个实例执行特定任务。如果未能妥善处理,将导致重复执行任务,可能造成资源浪费、数据不一致等问题,甚至对业务逻辑造成严重影响。
ShedLock 是一种轻量级解决方案,它通过使用锁机制,确保在分布式环境中只有一个实例能够执行某一任务。它既避免了重复执行任务的风险,又保持了系统的高可用性和一致性。
二、ShedLock的典型应用场景
分布式微服务:多个服务实例部署在不同节点时,确保定时任务不会被多个实例同时执行。
高可用保障:即使某些实例发生宕机或重启,任务调度依然能够按预期运行。
避免资源竞争:在负载均衡环境下,防止不同实例对同一任务的竞争执行。
ShedLock的官方文档地址如下:
https://gitee.com/jyycool/ShedLock/#usage三、引入依赖
特别说明当前Spring Boot的版本为2.7.6,需要引入下述依赖:
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.6</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.bc</groupId><artifactId>demo</artifactId><version>0.0.1-SNAPSHOT</version><name>demo</name><description>Demo project for Spring Boot</description><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.hibernate</groupId><artifactId>hibernate-validator</artifactId><version>6.0.1.Final</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.0</version><scope>provided</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><version>2.7.6</version></dependency><dependency><groupId>net.javacrumbs.shedlock</groupId><artifactId>shedlock-spring</artifactId><version>4.1.0</version></dependency><dependency><groupId>net.javacrumbs.shedlock</groupId><artifactId>shedlock-provider-redis-spring</artifactId><version>4.1.0</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>四、配置文件
在resources目录下新建一个名为application.yml的文件,其配置信息如下所示:
server: port: 8849spring: application: name: demo redis: host: 127.0.0.1 port: 6379五、启用ShedLock
在应用的主类中,添加 ShedLock 和定时任务的相关注解:
import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.scheduling.annotation.EnableScheduling;@SpringBootApplication@EnableScheduling@EnableSchedulerLock(defaultLockAtMostFor ="PT30S")public class DemoApplication {public static void main(String[] args) {SpringApplication.run(DemoApplication.class, args);}}六、构建定时任务
通过 @SchedulerLock 和 @Scheduled 注解来定义任务及其调度规则:
import net.javacrumbs.shedlock.spring.annotation.SchedulerLock;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.scheduling.annotation.Scheduled;import org.springframework.stereotype.Component;@Componentpublic class ScheduledTask { private static final Logger logger = LoggerFactory.getLogger(ScheduledTask.class); @Scheduled(cron = "0/10 * * * * ?") @SchedulerLock(name = "scheduledTaskName", lockAtLeastFor = "PT5S", lockAtMostFor = "PT30S") public void scheduledTask() { logger.info("Scheduled task is running..."); }}@SchedulerLock注解主要参数如下:
name:锁的名称,必须保证唯一
lockAtLeastFor:成功执行任务的节点所能拥有的独占锁的最短时间,其主要目的是任务执行时间可能很短,执行后如果发上释放锁,可能会造成多个节点执行。
lockAtMostFor:成功执行任务的节点所能拥有的独占锁的最长时间,设置的值要保证比定时任务正常执行完成的时间大一些,此属性保证了如果task节点突然宕机,也能在超过设定值时释放任务锁;
七、构建配置类
import net.javacrumbs.shedlock.core.LockProvider;import net.javacrumbs.shedlock.provider.redis.spring.RedisLockProvider;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.data.redis.core.RedisTemplate;import javax.annotation.Resource;@Configurationpublic class ScheduledLockConfig { @Resource RedisTemplate redisTemplate; @Bean public LockProvider lockProvider() { return new RedisLockProvider(redisTemplate.getConnectionFactory()); }}八、测试
构建两个实例,一个端口号为88,另一个为8080,同时启动两个实例。观察日志输出。每个任务执行间隔为 10 秒,但日志表明在任何时间点只有一个实例执行任务。
2025-02-08 10:35:10.993 [scheduling-1] INFO [com.example.demo.schedule.ScheduledTask:17] - Scheduled task is running...2025-02-08 10:35:20.004 [scheduling-1] INFO [com.example.demo.schedule.ScheduledTask:17] - Scheduled task is running...2025-02-08 10:35:50.019 [scheduling-1] INFO [com.example.demo.schedule.ScheduledTask:17] - Scheduled task is running...2025-02-08 10:36:00.017 [scheduling-1] INFO [com.example.demo.schedule.ScheduledTask:17] - Scheduled task is running...2025-02-08 10:35:30.036 [scheduling-1] INFO [com.example.demo.schedule.ScheduledTask:17] - Scheduled task is running...2025-02-08 10:35:40.024 [scheduling-1] INFO [com.example.demo.schedule.ScheduledTask:17] - Scheduled task is running...2025-02-08 10:36:10.013 [scheduling-1] INFO [com.example.demo.schedule.ScheduledTask:17] - Scheduled task is running...通过引入 Redis 作为锁的存储后端,系统不仅实现了任务执行的唯一性,还能在高并发和负载均衡的环境下保持稳定。
以下是本文解决的核心问题:
避免了任务的重复执行,提升了系统的一致性和可靠性。
通过 ShedLock 与 Redis 的结合,实现了任务锁的分布式存储,确保了高可用性。
在任务执行失败或锁超时时,系统能自动释放锁,避免了资源占用问题。
在实际业务中,ShedLock 可用于报表生成、批量任务处理、资源清理等场景。其易用性和可扩展性,使其成为处理分布式任务的重要工具。
九、个人思考
通过redisson中的分布式锁进行硬编码也是可以实现上述类似功能,需要注意的是合理评估任务执行时间和锁持有的时间。
import lombok.extern.slf4j.Slf4j;import org.redisson.api.RLock;import org.redisson.api.RedissonClient;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.scheduling.annotation.Scheduled;import org.springframework.stereotype.Component;import java.util.concurrent.TimeUnit;@Component@Slf4jpublic class ScheduledTask { @Autowired private RedissonClient redissonClient; private final String key = "alarm_notification_key"; @Scheduled(cron = "0 0 10 * * ?") public void alarmNotification() throws Exception { RLock lock = redissonClient.getLock(key); final int lockExpireTime = 10; final int lockWaitTime = 2; if (lock.tryLock(lockWaitTime, lockExpireTime, TimeUnit.SECONDS)) { try { log.info("告警通知检测"); Thread.sleep(7000); /** * 业务处理 */ } catch (Exception e) { log.error("告警通知处理出现异常:", e); } finally { if (lock.isLocked() && lock.isHeldByCurrentThread()) { lock.unlock(); } } } }}

















1161
































































