定时器Scheduler在平时使用比较频繁,比如定时数据整理,定时向客户发送问候信息等…,定时任务的配置比较简单,比如在springboot中,配置好@Scheduled和@EnableScheduling之后,定时器就能正常执行,实现定时任务的功能。
但是在这样的情况下:如果开发的服务需要水平部署实现负载均衡,那么定时任务就会同时在多个服务实例上运行,那么一方面,可能由于定时任务的逻辑处理需要访问公共资源从而造成并发问题;另一方面,就算没有并发问题,那么一个同样的任务多个服务实例同时执行,也会造成资源的浪费。因此需要一种机制来保证多个服务实例之间的定时任务正常、合理地执行。
ShedLock的出现就是为了解决上述问题,它可以保证多个一个定时任务在多个服务实例之间最多只执行一次,是一个在分布式环境中保证定时任务合理执行的框架。
Github项目地址:https://github.com/lukas-krecan/ShedLock.
ShedLock的实现原理是采用公共存储实现的锁机制,使得同一时间点只有第一个执行定时任务的服务实例能执行成功,并在公共存储中存储"我正在执行任务,从什么时候(预计)执行到什么时候",其他服务实例执行时如果发现任务正在执行,则直接跳过本次执行,从而保证同一时间一个任务只被执行一次。
上面提到的公共存储目前支持的有:
值得注意的是,ShedLock不是一个分布式的定时任务框架,只是一个锁,用于保证分布式环境中的定时任务合理执行。贴一段原话:Please note that ShedLock is not and will never be full-fledged scheduler, it’s just a lock. If you need a distributed scheduler, please use another project. ShedLock is designed to be used in situations where you have scheduled tasks that are not ready to be executed in parallel, but can be safely executed repeatedly. For example if the task is fetching records from a database, processing them and marking them as processed at the end without using any transaction. In such case ShedLock may be right for you.
依赖说明:
以SpringBoot为例。
<dependency>
<groupId>net.javacrumbs.shedlockgroupId>
<artifactId>shedlock-springartifactId>
<version>2.3.0version>
dependency>
<dependency>
<groupId>net.javacrumbs.shedlockgroupId>
<artifactId>shedlock-provider-jdbc-templateartifactId>
<version>2.3.0version>
dependency>
<dependency>
<groupId>com.zaxxergroupId>
<artifactId>HikariCPartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
这里以 mysql 和 JdbcTemplate 为例。
@Component
@Slf4j
public class TestTask {
@Scheduled(cron = "0/5 * * * * ?")
public void doTask() throws InterruptedException {
log.info(Thread.currentThread().getName()+"===task run");
Thread.sleep(6000);
log.info(Thread.currentThread().getName()+"===task end");
}
@Scheduled(initialDelay = 3000, fixedDelay = 5000)
public void doTask2() throws InterruptedException {
log.info(Thread.currentThread().getName()+"===task2 run");
// Thread.sleep(6000);
// log.info(Thread.currentThread().getName()+"===task2 end");
}
}
在主类或者@EnableScheduling的地方添加@EnableSchedulerLock(defaultLockAtMostFor = “PT30S”)
@SpringBootApplication
@EnableScheduling
@EnableSchedulerLock(defaultLockAtMostFor = "PT30S")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@Component
public class ShedLockConfig {
@Bean
public LockProvider lockProvider(DataSource dataSource) {
return new JdbcTemplateLockProvider(dataSource);
}
@Bean
public ScheduledLockConfiguration scheduledLockConfiguration(LockProvider lockProvider) {
return ScheduledLockConfigurationBuilder.withLockProvider(lockProvider)
.withPoolSize(10)
.withDefaultLockAtMostFor(Duration.ofMinutes(10))
.build();
}
}
此处的DataSource即为当前应用的数据源。
注意,如果使用SqlServer需要设置schema,可以在JdbcTemplateLockProvider的构造方法中设置:
new JdbcTemplateLockProvider(dataSource, "schema.shedlock")
MySQL
CREATE TABLE shedlock(
name VARCHAR(64),
lock_until TIMESTAMP(3) NULL,
locked_at TIMESTAMP(3) NULL,
locked_by VARCHAR(255),
PRIMARY KEY (name)
)
SQLServer
CREATE TABLE [dbo].[shedlock](
[name] [varchar](64) NOT NULL,
[lock_until] [datetime] NULL,
[locked_at] [datetime] NULL,
[locked_by] [varchar](255) NOT NULL,
CONSTRAINT [PK_shedlock] PRIMARY KEY CLUSTERED
(
[name] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
Oracle
CREATE TABLE SHEDLOCK (
name VARCHAR2(64 CHAR),
lock_until TIMESTAMP,
locked_at TIMESTAMP,
locked_by VARCHAR2(255 CHAR),
PRIMARY KEY (name)
);
然后在@Scheduled(…)的地方添加@SchedulerLock(name = “scheduledTaskName”)
package com.johnfnash.learn.springboot.schedule.task;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
import net.javacrumbs.shedlock.core.SchedulerLock;
@Component
@Slf4j
public class TestTask {
@Scheduled(cron = "0/5 * * * * ?")
@SchedulerLock(name = "testJob", lockAtLeastFor = 6000)
public void doTask() throws InterruptedException {
log.info(Thread.currentThread().getName()+"===task run");
Thread.sleep(6000);
log.info(Thread.currentThread().getName()+"===task end");
}
@SchedulerLock(name = "testJob2", lockAtLeastFor = 4000)
@Scheduled(initialDelay = 3000, fixedDelay = 5000)
public void doTask2() throws InterruptedException {
log.info(Thread.currentThread().getName()+"===task2 run");
// Thread.sleep(6000);
// log.info(Thread.currentThread().getName()+"===task2 end");
}
}
@SchedulerLock的作用是保证当前定时任务的方法执行时获得锁,忽略其他相同任务的执行。但是,name必须要指定,ShedLock就是根据这个name进行相同任务判定的(后面会细说)。注解中还有两个参数:
使用不同的端口启动服务,结果如下:
其中一个节点:
2019-06-02 17:31:25.005 INFO 4264 --- [pool-2-thread-1] c.j.l.springboot.schedule.task.TestTask : pool-2-thread-1===task run
2019-06-02 17:31:31.005 INFO 4264 --- [pool-2-thread-1] c.j.l.springboot.schedule.task.TestTask : pool-2-thread-1===task end
2019-06-02 17:31:31.010 INFO 4264 --- [Async-1-thread8] c.j.l.springboot.schedule.task.TestTask : Async-1-thread8===task2 run
2019-06-02 17:31:36.018 INFO 4264 --- [Async-1-thread9] c.j.l.springboot.schedule.task.TestTask : Async-1-thread9===task2 run
2019-06-02 17:31:45.004 INFO 4264 --- [pool-2-thread-1] c.j.l.springboot.schedule.task.TestTask : pool-2-thread-1===task run
2019-06-02 17:31:51.005 INFO 4264 --- [pool-2-thread-1] c.j.l.springboot.schedule.task.TestTask : pool-2-thread-1===task end
2019-06-02 17:31:51.031 INFO 4264 --- [sync-1-thread10] c.j.l.springboot.schedule.task.TestTask : Async-1-thread10===task2 run
2019-06-02 17:31:55.003 INFO 4264 --- [pool-2-thread-1] c.j.l.springboot.schedule.task.TestTask : pool-2-thread-1===task run
2019-06-02 17:32:01.004 INFO 4264 --- [pool-2-thread-1] c.j.l.springboot.schedule.task.TestTask : pool-2-thread-1===task end
2019-06-02 17:32:01.020 INFO 4264 --- [Async-1-thread1] c.j.l.springboot.schedule.task.TestTask : Async-1-thread1===task2 run
另外一个节点:
2019-06-02 17:31:26.029 INFO 10372 --- [Async-1-thread7] c.j.l.springboot.schedule.task.TestTask : Async-1-thread7===task2 run
2019-06-02 17:31:35.007 INFO 10372 --- [pool-2-thread-1] c.j.l.springboot.schedule.task.TestTask : pool-2-thread-1===task run
2019-06-02 17:31:41.007 INFO 10372 --- [pool-2-thread-1] c.j.l.springboot.schedule.task.TestTask : pool-2-thread-1===task end
2019-06-02 17:31:41.013 INFO 10372 --- [Async-1-thread8] c.j.l.springboot.schedule.task.TestTask : Async-1-thread8===task2 run
2019-06-02 17:31:46.020 INFO 10372 --- [Async-1-thread9] c.j.l.springboot.schedule.task.TestTask : Async-1-thread9===task2 run
2019-06-02 17:31:56.031 INFO 10372 --- [sync-1-thread10] c.j.l.springboot.schedule.task.TestTask : Async-1-thread10===task2 run
2019-06-02 17:32:06.039 INFO 10372 --- [Async-1-thread1] c.j.l.springboot.schedule.task.TestTask : Async-1-thread1===task2 run
2019-06-02 17:32:16.052 INFO 10372 --- [Async-1-thread2] c.j.l.springboot.schedule.task.TestTask : Async-1-thread2===task2 run
2019-06-02 17:32:25.004 INFO 10372 --- [pool-2-thread-1] c.j.l.springboot.schedule.task.TestTask : pool-2-thread-1===task run
2019-06-02 17:32:31.006 INFO 10372 --- [pool-2-thread-1] c.j.l.springboot.schedule.task.TestTask : pool-2-thread-1===task end
从输出日志中我们可以看到,两个节点在交替执行任务,没有重复执行。
来看一下表里的数据:
再次强调,ShedLock不是一个定时任务框架,而是一个保证定时任务在分布式环境中的合理执行的辅助框架,保证定时任务在分布式环境中同一时间最多只执行一次。同时一个任务在执行时,另一个任务无法获取锁时会跳过当前任务的执行。
最后关于ShedLock的版本说明(直接贴原话了):
Version 1.x.x is compiled and tested with Spring 5 and Spring Data 2. It should be safe to use ShedLock 1.x.x with Spring 4 if you are not using Spring Redis lock provider which introduced incompatibility. In other words
本文转自: ShedLock日常使用