前因
在springboot中常使用Scheduler做定时任务,只需要配置好@Scheduled和@EnableScheduling之后,按照cron表达式进行计划任务。
但是我们生产环境中,为了防止单点问题,必然需要实现集群部署,通过代理形成负载均衡的集群。那么如果集群中的计划任务需要访问共享资源形成并发问题,因此需要一种机制来保证集群中同一时间仅有一个服务实例执行计划任务。
解决方案
这种场景下我们就需要考虑一种解决方案了,作者曾考虑集群中每个服务实例的cron表达式均匀划分到不同的时间点,以此防止并发问题,不过这种方案针对某一任务执行时间点还是有单点问题,同时,集群扩张后难以管理。
作者接着考虑借助redis共享缓存实现分布式锁机制,用以解决并发问题,这就是一种比较好的解决方案了。
后面,偶然间了解到业界已有比较成熟的分布式定时任务锁实现框架:ShedLock。
ShedLock介绍
ShedLock的实现原理是采用公共存储实现的分布式锁机制,使得同一时间点只有第一个执行定时任务的服务实例能执行成功,并在公共存储中存储"我正在执行任务,从什么时候(预计)执行到什么时候",其他服务实例执行时如果发现任务正在执行,则直接跳过本次执行,从而保证同一时间一个任务只被执行一次。
当前支持的公共存储很多,如:
- Redis
- JdbcTemplate
- Zookeeper
- Monogo
- ...
相对于jdbc的公共存储方式,作者还是比较建议使用Redis作为公共存储的。
使用Redis作为公共存储
maven依赖
<!-- 分布式定时任务锁 -->
<!-- https://mvnrepository.com/artifact/net.javacrumbs.shedlock/shedlock-spring -->
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-spring</artifactId>
<version>4.0.4</version>
</dependency>
<!-- 使用redis做分布式任务 -->
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-provider-redis-spring</artifactId>
<version>2.5.0</version>
</dependency>
配置信息
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.core.RedisTemplate;/**
* 分布式定时调度配置
* <p>
* SchedulerLock 注解一共支持五个参数,分别是
* name 用来标注一个定时服务的名字,被用于写入数据库作为区分不同服务的标识,如果有多个同名定时任务则同一时间点只有一个执行成功
* lockAtMostFor 成功执行任务的节点所能拥有独占锁的最长时间,单位是毫秒ms
* lockAtMostForString 成功执行任务的节点所能拥有的独占锁的最长时间的字符串表达,例如“PT14M”表示为14分钟
* lockAtLeastFor 成功执行任务的节点所能拥有独占所的最短时间,单位是毫秒ms
* lockAtLeastForString 成功执行任务的节点所能拥有的独占锁的最短时间的字符串表达,例如“PT14M”表示为14分钟
* @author kwin
*/
@Configuration
@EnableSchedulerLock(defaultLockAtMostFor = "PT30S")
public class ShedLockConfig {
@Bean
public LockProvider lockProvider(RedisTemplate redisTemplate) {
return new RedisLockProvider(redisTemplate.getConnectionFactory());
}}
计划任务样例
import lombok.extern.slf4j.Slf4j;
import net.javacrumbs.shedlock.spring.annotation.SchedulerLock;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;/**
* @author : kwin
* @version V1.0
* @Package
* @Description:
* @date 2021年05月13日 10:16
**/
@Slf4j
@Component
public class PoducerSched {
@Scheduled(cron = "0 */10 * * * ?")
@SchedulerLock(name = "test", lockAtMostFor = "PT5M")
public void test() {
log.info("Start run schedule to test");
}}
这里需要注意,使用计划任务,需要使用@EnableScheduling注解启用计划任务