收到任务需求:
由于某些原因 需要缓存一些站点到Redis,且需要一条一条查找并缓存;
Easy!由于业务逻辑要执行4h,那不就是Job增加一个定时任务实现业务逻辑,话不多说,开搞!
一、创建定时任务
基本依赖
<!-- Spring Scheduling -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-scheduling</artifactId>
<version>${spring.version}</version>
</dependency>
创建定时任务类:
/**
* @author xiumu
*/
@Configuration
@EnableScheduling
public class AutoCacheSiteRegular {
private static final Logger LOGGER = LoggerFactory.getLogger(AutoCacheSiteRegular.class);
@Autowired
private SiteService siteService;
/**
* 自动缓存站点信息
*/
@Scheduled(cron = "0/30 * * * * ?")
public void autoCacheSiteInfo() {
LOGGER.info("======autoCacheSiteInfo缓存站点信息开始======");
siteService.cacheSiteInfo();//业务逻辑
LOGGER.info("======autoCacheSiteInfo缓存站点信息结束======");
}
}
执行结果
[2024-07-31 13:10:27.927][INFO][taskScheduler-10]
======autoCacheSiteInfo缓存站点信息开始======
[2024-07-31 17:00:00.007][INFO][taskScheduler-10]
======autoCacheSiteInfo缓存站点信息结束======
[2024-07-31 17:00:30.285][INFO][taskScheduler-10]
======autoCacheSiteInfo缓存站点信息结束======
[2024-07-31 20:50:53.136][INFO][taskScheduler-10]
======autoCacheSiteInfo缓存站点信息结束======
程序正在平稳的运行🙂🙂🙂结束!
结束?
同事A:我的订单怎么提交不上了?
同事B:不应该啊,我测着没问题啊,怎么突然推不过去了????
大伙的目光开始照向了我
二、问题:单线程执行
按照上面代码中给定的cron表达式@Scheduled(cron = “0/30 * * * * ?”)每30秒执行一次,那么最近五次的执行时间应当为:
- 5s+执行结果时间
由于之前的任务执行时间很短,所以不会有什么明显问题
但是朽木同学新增的任务的业务逻辑约为4h,那么就出现了问题
举个例子:
@Slf4j
@Component
@EnableScheduling
public class ScheduleService {
// 每五秒执行一次,cron的表达式就不再多说明了
@Scheduled(cron = "0/5 * * * * ? ")
public void testSchedule() {
log.info("当前执行任务的线程号ID===>{}", Thread.currentThread().getId());
}
}
执行结果应该为:
2024-08-01 10:10:10
2024-08-01 10:10:15
2024-08-01 10:10:20
2024-08-01 10:10:25
2024-08-01 10:10:30
如果定时任务中是执行非常快的任务的,时间非常非常短,确实不会有什么的延迟性。
那么,我主动让线程睡上10秒,让我们再来看看输出结果是如何的吧
@Scheduled(cron = "0/5 * * * * ? ")
public void testSchedule() {
try {
Thread.sleep(10000);
log.info("当前执行任务的线程号ID===>{}", Thread.currentThread().getId());
} catch (Exception e) {
e.printStackTrace();
}
}
输出结果:
[2024-08-31 10:46:50.019][INFO][taskScheduler-10]
当前执行任务的线程号ID===>64
[2024-08-31 10:47:05.024][INFO][taskScheduler-10]
当前执行任务的线程号ID===>64
[2024-08-31 10:47:20.016][INFO][taskScheduler-10]
当前执行任务的线程号ID===>64
[2024-08-31 10:47:35.005][INFO][taskScheduler-10]
当前执行任务的线程号ID===>64
[2024-08-31 10:47:50.006][INFO][taskScheduler-10]
当前执行任务的线程号ID===>64
请注意两个问题:
- 执行时间延迟: 从时间上可以明显看出,不再是每五秒执行一次,执行时间延迟很多,造成任务的
- 单线程执行: 从始至终都只有一个线程在执行任务,造成任务的堵塞.
三、为什么会出现上述问题?
问题的根本:线程阻塞式执行,执行任务线程数量过少。
那到底是为什么呢?
回到启动类上,我们在启动上标明了一个@EnableScheduling注解。
大家在看到诸如@Enablexxxx这样的注解的时候,就要知道它一定有一个xxxxxConfiguration的自动装配的类。
@EnableScheduling也不例外,它的自动装配的类是SchedulingConfiguration。
我们来看看它到底做了一些什么设置?我们如何修改?
@Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class SchedulingConfiguration {
@Bean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public ScheduledAnnotationBeanPostProcessor scheduledAnnotationProcessor() {
return new ScheduledAnnotationBeanPostProcessor();
}
}
可以看到它也是构造了一个 线程池注入到Spring 中
一直往下跟,一直到这里(直接上图片):
主要逻辑在这里,创建线程池的时候,默认的参数线程数为1
而这默认参数是不行的,生产环境的大坑,阿里的 Java 开发手册中也明确规定,要手动创建线程池,并给定合适的参数值~是为什么呢?
因为默认的线程池中, 池中允许的最大线程数和最大任务等待队列都是Integer.MAX_VALUE.
大家都懂的,如果使用这玩意,只要出了问题,必定挂~
configure(new ThreadPoolTaskScheduler())这里就是构造,略过~
如果已经较为熟悉SpringBoot的朋友,现在已然明白解决当前问题的方式~
四、解决方式
可以手动异步编排,交给某个线程池来执行,同时增加分布式锁,协调并发操作。
1、修改定时任务线程池大小:
/**
* @author xiumu
*/
@Configuration
public class TaskSchedulerConfig {
/**
* 创建并配置一个TaskScheduler实例,使用线程池来调度任务。
*
* @return TaskScheduler 返回配置好的TaskScheduler实例,可以用于异步任务的调度。
*/
@Bean
public TaskScheduler taskScheduler() {
// 创建ThreadPoolTaskScheduler实例,它是一个基于线程池的任务调度器
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
// 设置线程池的大小为10,这意味着最多可以同时执行10个任务
taskScheduler.setPoolSize(10);
return taskScheduler;
}
}
2、自定义注解
/**
* @author xiumu
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DistributedLock {
String name();
long timeout() default 30000; // 默认超时时间为 30 秒
}
2、创建分布式锁拦截器
@Aspect
@Component
public class DistributedLockInterceptor {
private static final Logger LOGGER = LoggerFactory.getLogger(DistributedLockInterceptor.class);
@Autowired
private RedisTemplate redisTemplate;
/**
* 使用分布式锁来确保方法的线程安全。
* 在方法执行前尝试获取锁,如果获取成功,则执行方法;如果获取失败,则抛出异常。
* 使用Redis作为分布式锁的存储介质。
*
* @param joinPoint 切面连接点,表示被拦截的方法。
* @param distributedLock 分布式锁注解,包含锁的名称和超时时间。
* @return 返回被拦截方法的执行结果。
* @throws Throwable 如果执行过程中发生异常,则抛出。
*/
@Around("@annotation(distributedLock)")
public void distributedLock(ProceedingJoinPoint joinPoint, DistributedLock distributedLock) throws Throwable {
String lockKey = "LOCK:" + distributedLock.name(); // 锁的键名
long timeout = distributedLock.timeout(); // 锁的超时时间
// 尝试获取分布式锁
boolean locked = false;
try {
locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", timeout, TimeUnit.MILLISECONDS);
if (locked) {
// 成功获取锁,执行目标方法
joinPoint.proceed();
} else {
// 未获取到锁不执行
LOGGER.error("Failed to acquire lock!");
}
}catch (Exception e){
throw e;
}finally {
if (locked) {
// 执行方法结束后释放锁
redisTemplate.delete(lockKey);
}
}
}
}
3、定时任务增加注解
/**
* @author xiumu
*/
@Configuration
@EnableScheduling
public class AutoCacheSiteRegular {
private static final Logger LOGGER = LoggerFactory.getLogger(AutoCacheSiteRegular.class);
@Autowired
private SiteService siteService;
/**
* 自动缓存站点信息
*/
@Scheduled(cron = "0/30 * * * * ?")
@DistributedLock(name = "autoCacheSiteInfo", timeout = 4 * 60 * 60 * 1000)
public void autoCacheSiteInfo() {
LOGGER.info("======autoCacheSiteInfo缓存站点信息开始======");
siteService.cacheSiteInfo();//业务逻辑
LOGGER.info("======autoCacheSiteInfo缓存站点信息结束======");
}
}
完美解决!🎉🎉🎉
分析:
@EnableAsync注解相应的也有一个自动装配类为TaskExecutionAutoConfiguration
也有一个TaskExecutionProperties配置类,可以在yml文件中对参数进行设置,这里的话是可以配置线程池最大存活数量的。
它的默认核心线程数为8,这里我不再进行演示了,同时它的线程池中最大存活数量以及任务等待数量也都为Integer.MAX_VALUE,这也是不建议大家使用默认线程池的原因。
小结:
/**
* 定时任务
* 1、@EnableScheduling 开启定时任务
* 2、@Scheduled开启一个定时任务
* 3、自动装配类 TaskSchedulingAutoConfiguration
*
* 异步任务
* 1、@EnableAsync:开启异步任务
* 2、@Async:给希望异步执行的方法标注;或者增加配置类
* 3、自动装配类 TaskExecutionAutoConfiguration
*
*分布式锁
* 1、自定义注解
* 2、拦截器
*/
后记
但实际上,我所阐述的这种方式,只能说适用于简单的单体项目,一旦牵扯到动态定时任务,使用这种方式就不再那么方便了。
大部分都是使用定时任务框架集成了,尤其是分布式调度远比单体项目需要考虑多的多。
希望大家有所收获!