前言
最近业务上有个需求是将当天的直播进行录制,这里录制技术我们不做过多研究,主要使用的是ffmpeg,有兴趣的可以下来去研究研究。其中的录制要求有两点。一是能够在录制中进行更改录制时间,二是能够实现周期录制。个人对上述的要求在业务上采用了定时器来进行实现。主要使用的是ThreadPoolTaskScheduler。
一、为什么使用ThreadPoolTaskScheduler?
使用ThreadPoolTaskScheduler的好处是由于需要通过手动操作对定时任务task进行开启和关闭,当然Timer也可以实现,但是Timer对复杂要求的时刻执行无法处理(比如固定某个时间段执行)。
二、使用步骤
1.启动类
代码如下(示例):
package com.yd.data.mrs;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.ApplicationPidFileWriter;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@SpringBootApplication
@EnableScheduling
public class MRSApplication {
private static ApplicationContext ctx;
public static void main(String[] args) {
SpringApplication application = new SpringApplication(MRSApplication.class);
application.addListeners(new ApplicationPidFileWriter("./mrs.pid"));
ctx = application.run(args);
//钩子
Runtime.getRuntime().addShutdownHook(new Thread(MRSApplication::shutdown));
}
private static void shutdown() {
//关闭操作
System.out.println("shutdown");
SpringApplication.exit(ctx);
}
@GetMapping("/")
public String index() {
return "录制服务运行中......";
}
}
2.编写定时任务配置类
代码如下(示例):
package com.yd.data.mrs.core.engine;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.concurrent.ScheduledFuture;
/**
* @author hjh
* @date 2021/06/29
*/
@Component
public class ScheduledJobTask extends ThreadPoolTaskScheduler {
public ScheduledFuture<?> schedule(Runnable baseTask, String expression){
return this.schedule(baseTask, new CronTrigger(expression));
}
public ScheduledFuture<?> schedule(Runnable task, LocalDateTime startTime){
return this.schedule(task, Date.from(startTime.atZone(ZoneId.systemDefault()).toInstant()));
}
public ScheduledJobTask() {
this.setPoolSize(50);
this.setThreadNamePrefix("scheduledJobTask-");
this.setWaitForTasksToCompleteOnShutdown(true);
this.setAwaitTerminationSeconds(300);
//初始化
this.initialize();
}
}
这里简要说明一下线程池不同情况说明:
- 若线程数 < corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
- 若线程数 = corePoolSize,但是缓冲队列workQueue未满,那么任务被放入缓冲队列。
- 若线程数 > corePoolSize,缓冲队列workQueue满,并且线程数 < maxPoolSize,创建新的线程来处理被添加的任务。
- 若线程数 > corePoolSize,缓冲队列workQueue满,并且线程数 = maxPoolSize,那么通过handler所指定的策略来处理此任务。
处理任务的优先级为:核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize。如果三者都满了,使用handler处理被拒绝的任务。
若线程数 > corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。
3.编写工具类实现定时任务的增删改查。
代码如下(示例):
package com.yd.data.mrs.core.engine;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
/**
* @author hjh
* @date 2021/06/29
*/
@Lazy(false)
@Component
@Transactional(rollbackFor = Exception.class)
@Slf4j
public class JobHandlerEngine {
//使用volatile的目的:将任务的状态操作同步刷新
private volatile ConcurrentHashMap<String, ScheduledFuture<?>> jobsMap = new ConcurrentHashMap<>(32);
private final ApplicationContext applicationContext;
private final ScheduledJobTask threadPoolTaskScheduler;
public JobHandlerEngine(ApplicationContext applicationContext, ScheduledJobTask threadPoolTaskScheduler) {
this.applicationContext = applicationContext;
this.threadPoolTaskScheduler = threadPoolTaskScheduler;
}
/**
* 获取所有定时任务
* @return
*/
public Map<String, ScheduledFuture<?>> getJobsMap() {
return jobsMap;
}
/**
* 停止定时任务
* @param jobId
*/
public void stop(String jobId) {
ScheduledFuture<?> scheduledFuture = jobsMap.get(jobId);
if (scheduledFuture != null) {
scheduledFuture.cancel(true);
jobsMap.remove(jobId);
log.info("定时任务:[jobId={}]停止...", jobId);
} else {
log.info("定时任务:[jobId={}]已处于停止状态", jobId);
}
}
/**
* 添加作业任务
* @param jobId
* @param future
*/
public void add(String jobId, ScheduledFuture<?> future) {
ScheduledFuture<?> scheduledFuture = jobsMap.get(jobId);
if (scheduledFuture != null) {
log.error("任务:[jobId={}]已存在...", jobId);
} else {
jobsMap.put(jobId, future);
log.info("任务:[jobId={}]已提交", jobId);
}
}
/**
* 更新定时任务
* @param jobId
* @param endTime
*/
@Transactional(rollbackFor = Exception.class)
public void resetRecordEndTime(String jobId,LocalDateTime endTime) {
stop(jobId);
ScheduledFuture<?> endSchedule = threadPoolTaskScheduler.schedule(() -> {
//终止录制任务
this.stop(jobId);
}, endTime);
this.add(jobId, endSchedule);
}
}
业务中的核心代码
ScheduledFuture<?> schedule = threadPoolTaskScheduler.schedule(() -> {
try {
this.startRecord(record);
this.stop(k);
//结束录制任务
ScheduledFuture<?> endSchedule = threadPoolTaskScheduler.schedule(() -> {
//终止录制任务
this.endRecord(record);
this.stop(k);
}, endTime);
this.add(k, endSchedule);
log.info("录制结束任务:[{}] 提交延时队列...", task.getK());
} catch (Exception e) {
e.printStackTrace();
}
}, startTime);
this.add(k, schedule);
log.info("录制开始任务:[{}] 提交延时队列...", task.getK());
上述代码通过根据k值判断是否是同一个定时任务,定时任务执行后需要将任务放置在队列中进行统计以及判错。这样一个大致的录制定时框架就完成了,其中考虑的要素还是挺多的,比如对录制的时间进行判断,周期录制的结束时间抽取等等。
欢迎大家添加个人公众号,一起进步努力。