文章目录
一、 ShedLock简介
ShedLock 是一个用于 Java 和 Spring Boot 应用的开源分布式锁库,旨在确保在分布式环境下定时任务能够被安全地调度和执行。它的主要目的是防止在多节点环境中定时任务的重复执行,通过使用锁机制来实现这一目标。ShedLock 的设计哲学是轻量级和易于集成,特别适合那些不需要复杂调度逻辑的场景,但需要确保定时任务的执行不会发生竞态条件或资源冲突。
ShedLock 的特点:
1、分布式锁:ShedLock 使用外部存储(如数据库或 Redis)来实现分布式锁,确保即使在多台服务器上运行相同的服务,同一时刻只有一个实例执行特定的定时任务。
2、轻量级:相对于像 Quartz 这样的全面调度框架,ShedLock 更专注于解决分布式定时任务的互斥执行问题,提供了简单的接口和较少的配置选项。
3、易用性:ShedLock 与 Spring Boot 的集成非常平滑,通过注解和自动配置简化了定时任务的管理,只需要使用@SchedulerLock即可。
4、Cron 和固定间隔支持:虽然 ShedLock 的调度功能相对简单,但它仍然支持 Cron 表达式和固定间隔的定时任务调度。
5、健壮性:ShedLock 能够处理任务失败的情况,提供重试机制,并且能够优雅地处理节点故障。
二、 @SchedulerLock
@SchedulerLock 注解是一个分布式锁的框架,结合@Scheduled 注解,可以保证任务同一时间,在多个节点上只会执行一次。该框架支持多种分布式锁的实现,比如Jdbc、Zookeeper、Redis等。
原理图如下:
三、基于Mysql方式使用步骤
1.建表
建表语句代码如下:
# 建表sql
CREATE TABLE shedlock(
name VARCHAR(64) NOT NULL,
lock_until TIMESTAMP(3) NOT NULL,
locked_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
locked_by VARCHAR(255) NOT NULL,
PRIMARY KEY (name)
);
2.引入依赖
<!-- shedlock的依赖 -->
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-spring</artifactId>
<version>4.23.0</version>
</dependency>
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-provider-jdbc-template</artifactId>
<version>4.23.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
3.Mysql连接配置
# 应用服务 WEB 访问端口
server.port=8080
spring.datasource.url=jdbc:mysql://192.168.10.26:19131/ch?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=sa123456
spring.datasource.driver-class=com.mysql.cj.jdbc.Driver
4.ScheduledLock配置
package com.example.schedulerlock.config;
import net.javacrumbs.shedlock.core.LockProvider;
import net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.sql.DataSource;
import java.util.TimeZone;
/**
* @Title: ScheduledLockConfig
* @Author ch
* @Date 2024/7/9 11:13
* @description:
*/
@Configuration
public class ScheduledLockConfig {
@Autowired
private DataSource dataSource;
@Bean
public LockProvider lockProvider() {
return new JdbcTemplateLockProvider(JdbcTemplateLockProvider.Configuration.builder()
.withJdbcTemplate(new JdbcTemplate(dataSource))
.withTimeZone(TimeZone.getTimeZone("UTC"))
.build());
}
}
5.启动类配置
package com.example.schedulerlock;
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 = "3m")
public class MySchedulerLockApplication {
public static void main(String[] args) {
SpringApplication.run(MySchedulerLockApplication.class, args);
}
}
6.创建定时任务
package com.example.schedulerlock.job;
import lombok.extern.slf4j.Slf4j;
import net.javacrumbs.shedlock.spring.annotation.SchedulerLock;
import org.joda.time.DateTime;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* @Title: SpringJob
* @Author ch
* @Date 2024/7/9 11:19
* @description:
*/
@Slf4j
@Component
public class SpringJob {
/**
* 每5分钟跑一次
*/
@Scheduled(cron = "0 */5 * * * ?")
@SchedulerLock(name = "SpringJob.job1", lockAtMostFor = "2m", lockAtLeastFor = "1m")
public void job1() {
log.info("time=" + DateTime.now().toString("YYYY-MM-dd HH:mm:ss") + " do job1...");
}
/**
* 每5秒跑一次
*/
@Scheduled(fixedRate = 5000)
@SchedulerLock(name = "SpringJob.job2", lockAtMostFor = "4s", lockAtLeastFor = "4s")
public void job2() {
log.info("time=" + DateTime.now().toString("YYYY-MM-dd HH:mm:ss") + " do job2...");
}
/**
* 上次跑完之后隔5秒再跑
* @throws InterruptedException
*/
@Scheduled(fixedDelay = 5000)
@SchedulerLock(name = "SpringJob.job3", lockAtMostFor = "4s", lockAtLeastFor = "4s")
public void job3() throws InterruptedException {
log.info("time=" + DateTime.now().toString("YYYY-MM-dd HH:mm:ss") + " do job3...");
Thread.sleep(10000);
}
}
7.启动多个项目服务进行测试
8.SchedulerLock注解说明
name:用来标注一个定时服务的名字,被用于写入数据库作为区分不同服务的标识,如果有多个同名定时任务则同一时间点只有一个执行成功。
lockAtMostFor: 这个属性指定了锁最多可以持有的时间长度。如果任务执行时间超过了给定时间,锁也将强制释放,以便其他等待的任务有机会获取锁并执行。但是,这也意味着如果任务执行时间超过锁的持有时间,可能会出现并发执行的情况。
lockAtLeastFor: 这个属性指定了锁至少可以持有的时间长度。这可以防止在任务执行结束后立即有另一个任务尝试获取相同的锁,从而产生不必要的锁争用。
四、使用注意事项
1、@SchedulerLock结合@Scheduled(cron = …):
利用 Cron 表达式来精确控制任务的执行时间点,同时利用 SchedulerLock 来保证任务执行的互斥性。Cron 表达式提供了更精细的时间控制,可以避免任务堆叠,而 SchedulerLock 则确保了即使在多个节点上部署,同一时刻也只有一个任务实例在运行。
2、@SchedulerLock结合@Scheduled(fixedRate = …)和(fixedDelay = …):
fixedRate 和fixedDelay 它们分别按照固定速率和固定延迟执行任务。这意味着无论上一次任务执行是否完成,到达设定的时间间隔后都会触发下一次执行。这种模式下,如果任务执行时间超过设定的间隔,可能会出现任务堆叠的情况,即多个任务实例同时运行。
SchedulerLock 的作用机制在于它会在任务开始执行前尝试获取一个锁,只有获取到锁的任务实例才能继续执行。执行完成后,锁会被释放,下一个任务实例才有机会获取锁并执行。这一机制非常适合保证任务的互斥执行,但在 fixedRate 或 fixedDelay 的场景下,由于任务的触发不依赖于前一个任务的完成,而是严格基于时间间隔,所以 SchedulerLock 只能保证每次任务开始执行时的互斥性,而无法直接控制任务的触发频率。
在分布式环境中使用 fixedRate 或 fixedDelay 时,如果不加额外控制,可能会导致多个节点上的任务实例几乎同时触发,从而违反了分布式定时任务的基本要求——确保同一时刻只有一个任务实例运行。此时,即使使用了 SchedulerLock,也可能因为多个节点几乎同时尝试获取锁而增加锁的争用,进而影响性能。
总之,SchedulerLock 与 fixedRate 或 fixedDelay 的结合使用需要谨慎,因为两者的设计目的和工作原理不同,可能不会达到预期的效果。在分布式定时任务的场景下,推荐使用 SchedulerLock 结合 Cron 表达式的方式,以获得更好的互斥性和时间控制。