一、业务背景
1.1 为什么需要使用定时任务调度
**(1)时间驱动处理场景:**整点发送优惠券,每天更新收益,每天刷新标签数据和人群数据。
**(2)批量处理数据:**按月批量统计报表数据,批量更新短信状态,实时性要求不高。
**(3)异步执行解耦:**活动状态刷新,异步执行离线查询,与内部逻辑解耦。
例如:
/**
* 自动更新阅读数,每天凌晨四点更新数据
* @author blue
* @date: 2021/8/18 17:58
*/
public void updateReadQuantity(){
// 获取带阅读量的前缀key集合
List<Article> articles = new ArrayList<>();
Map<String, Object> map = redisService.getCacheMap(ARTICLE_READING);
// 取出所有数据更新到数据库
for (Map.Entry<String, Object> stringEntry : map.entrySet()) {
String id = stringEntry.getKey();
List<String> list = (List<String>) stringEntry.getValue();
Article article = Article.builder()
.id(Long.parseLong(id)).quantity(list.size())
.build();
articles.add(article);
}
articleService.updateBatchById(articles);
}
/**
* 删除redis当天验证码通过的ip
*
*/
public void removeCodePassInIp(){
redisService.deleteObject(RedisConstants.CHECK_CODE_IP);
}
/**
* 每天定时修改标签的点击量
*
*/
public void updateTagsClickVolume(){
Map<String, Object> map = redisService.getCacheMap(TAG_CLICK_VOLUME);
List<Tags> tagsList = new ArrayList<>();
for (Map.Entry<String, Object> stringEntry : map.entrySet()) {
String id = stringEntry.getKey();
Integer value = (Integer) stringEntry.getValue();
Tags tags = new Tags(Long.parseLong(id),value);
tagsList.add(tags);
}
tagsService.updateBatchById(tagsList);
}
1.2 使用需求和痛点
(1)任务执行监控告警能力。
(2)任务可灵活动态配置,无需重启。
(3)业务透明,低耦合,配置精简,开发方便。
(4)易测试。
(5)高可用,无单点故障。
(6)任务不可重复执行,防止逻辑异常。
(7)大任务的分发并行处理能力。
Quartz框架是Java领域最著名的开源任务调度工具,也是目前事实上的定时任务标准,几乎全部的开源定时任务框架都是基于Quartz核心调度构建而成。
2.3.2 原理解析
Quartz框架是Java领域最著名的开源任务调度工具,也是目前事实上的定时任务标准,几乎全部的开源定时任务框架都是基于Quartz核心调度构建而成
核心组件和架构
关键概念
(1)Scheduler:任务调度器,是执行任务调度的控制器。本质上是一个计划调度容器,注册了全部Trigger和对应的JobDetail, 使用线程池作为任务运行的基础组件,提高任务执行效率。
(2)Trigger:触发器,用于定义任务调度的时间规则,告诉任务调度器什么时候触发任务,其中CronTrigger是基于cron表达式构建的功能强大的触发器。
(3)Calendar:日历特定时间点的集合。一个trigger可以包含多个Calendar,可用于排除或包含某些时间点。
(4)JobDetail:是一个可执行的工作,用来描述Job实现类及其它相关的静态信息,如Job的名称、监听器等相关信息。
(5)Job:任务执行接口,只有一个execute方法,用于执行真正的业务逻辑。
(6)JobStore:任务存储方式,主要有RAMJobStore和JDBCJobStore,RAMJobStore是存储在JVM的内存中,有丢失和数量受限的风险,JDBCJobStore是将任务信息持久化到数据库中,支持集群。
小demo:
public static void main(String[] args) throws SchedulerException {
SchedulerFactory factory = new StdSchedulerFactory();
// 调度器创建
Scheduler scheduler = factory.getScheduler();
// 具体定时任务需要执行的代码
JobDetail jobDetail = newJob(RamJob.class)
.withIdentity("job2", "group1")
.withIdentity("这是一个作业类案例")
.build();
Trigger trigger = (Trigger) TriggerBuilder.newTrigger()
// 每10s执行一次
.withSchedule(CronScheduleBuilder.cronSchedule("0/10 * * * * ?"))
// 触发器标识
.withIdentity("trigger2", "group1")
.withDescription("这是一个触发器")
.build();
// 调度工厂绑定作业类及触发器
scheduler.scheduleJob(jobDetail, trigger);
scheduler.start();
}
框架结构:
从下到上,完成定时任务的框架构建
//调度器,策略
public class ScheduleUtil {
/**
* 得到quartz任务类
*
* @param job 执行计划
* @return 具体执行任务类
*/
private static Class<? extends org.quartz.Job> getQuartzJobClass(Job job) {
//是否并发执行,Quartz框架配置中体现它是多线程的
boolean isConcurrent = "0".equals(job.getConcurrent());
return isConcurrent ? QuartzJobExecution.class : QuartzDisallowConcurrentExecution.class;
}
/**
* 构建任务触发对象
*/
public static TriggerKey getTriggerKey(Long jobId, String jobGroup) {
//根据jobId和分组ID设置所属的触发器trigger
return TriggerKey.triggerKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup);
}
/**
* 构建任务键对象
*/
public static JobKey getJobKey(Long jobId, String jobGroup) {
//根据jobId和分组ID返回任务的标识JobKey,用于后面再次禁用/启用该任务的标识
return JobKey.jobKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup);
}
/**
* 创建定时任务
*/
public static void createScheduleJob(Scheduler scheduler, Job job) throws SchedulerException, TaskException {
Class<? extends org.quartz.Job> jobClass = getQuartzJobClass(job);
// 构建job信息
Long jobId = job.getJobId();
String jobGroup = job.getJobGroup();
JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(getJobKey(jobId, jobGroup)).build();
// 表达式调度构建器
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(job.getCronExpression());
cronScheduleBuilder = handleCronScheduleMisfirePolicy(job, cronScheduleBuilder);
// 按新的cronExpression表达式构建一个新的trigger
CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(getTriggerKey(jobId, jobGroup))
.withSchedule(cronScheduleBuilder).build();
// 放入参数,运行时的方法可以获取
jobDetail.getJobDataMap().put(ScheduleConstants.TASK_PROPERTIES, job);
// 判断是否存在
if (scheduler.checkExists(getJobKey(jobId, jobGroup))) {
// 防止创建时存在数据问题 先移除,然后在执行创建操作
scheduler.deleteJob(getJobKey(jobId, jobGroup));
}
//开始执行任务,按照Cron语法来执行策略
scheduler.scheduleJob(jobDetail, trigger);
// 暂停任务
if (job.getStatus().equals(ScheduleConstants.Status.PAUSE.getValue())) {
scheduler.pauseJob(ScheduleUtil.getJobKey(jobId, jobGroup));
}
//triggerJob只执行一次,scheduleJob按照策略来执行,可能一次,或按照时间安排一直执行,resumeJob恢复执行,pauseJob暂停执行
}
/**
* 设置定时任务策略
*/
public static CronScheduleBuilder handleCronScheduleMisfirePolicy(Job job, CronScheduleBuilder cb)
throws TaskException {
//任务执行失败后如何做
switch (job.getMisfirePolicy()) {
case ScheduleConstants.MISFIRE_DEFAULT:
return cb;
case ScheduleConstants.MISFIRE_IGNORE_MISFIRES:
return cb.withMisfireHandlingInstructionIgnoreMisfires();
case ScheduleConstants.MISFIRE_FIRE_AND_PROCEED:
return cb.withMisfireHandlingInstructionFireAndProceed();
case ScheduleConstants.MISFIRE_DO_NOTHING:
return cb.withMisfireHandlingInstructionDoNothing();
default:
throw new TaskException("The task misfire policy '" + job.getMisfirePolicy()
+ "' cannot be used in cron schedule tasks", TaskException.Code.CONFIG_ERROR);
}
}
}
如何调用任务 的接口
Job的属性:
@Component("blogQuartz")
@RequiredArgsConstructor
public class BlogQuartz {
public void ryMultipleParams(String s, Boolean b, Long l, Double d, Integer i){}
}
对照下面的方法实现,很容易看懂
public class JobInvokeUtil {
/**
* invoke --执行方法
*
* @param job 系统任务
*/
public static void invokeMethod(Job job) throws Exception
{
String invokeTarget = job.getInvokeTarget();
String beanName = getBeanName(invokeTarget);
String methodName = getMethodName(invokeTarget);
List<Object[]> methodParams = getMethodParams(invokeTarget);
// 如果传入的是类名则获取方法实例化后执行;或者传入的是Bean名字,通过Bean名字获取Bean实例化后执行
if (!isValidClassName(beanName))
{
Object bean = SpringUtil.getBean(beanName);
invokeMethod(bean, methodName, methodParams);
}
else
{
Object bean = Class.forName(beanName).newInstance();
invokeMethod(bean, methodName, methodParams);
}
}
/**
* 调用任务方法
*
* @param bean 目标对象
* @param methodName 方法名称
* @param methodParams 方法参数
*/
private static void invokeMethod(Object bean, String methodName, List<Object[]> methodParams)
throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException,
InvocationTargetException
{
if (methodParams != null && methodParams.size() > 0)
{
Method method = bean.getClass().getDeclaredMethod(methodName, getMethodParamsType(methodParams));
method.invoke(bean, getMethodParamsValue(methodParams));
}
else
{
Method method = bean.getClass().getDeclaredMethod(methodName);
method.invoke(bean);
}
}
/**
* 校验是否为为class包名
*
* @return true是 false否
*/
public static boolean isValidClassName(String invokeTarget)
{
return StringUtils.countMatches(invokeTarget, ".") > 1;
}
/**
* 获取bean名称
*
* @param invokeTarget 目标字符串
* @return bean名称
*/
public static String getBeanName(String invokeTarget)
{
String beanName = StringUtils.substringBefore(invokeTarget, "(");
return StringUtils.substringBeforeLast(beanName, ".");
}
/**
* 获取bean方法
*
* @param invokeTarget 目标字符串
* @return method方法
*/
public static String getMethodName(String invokeTarget)
{
String methodName = StringUtils.substringBefore(invokeTarget, "(");
return StringUtils.substringAfterLast(methodName, ".");
}
/**
* 获取method方法参数相关列表
*
* @param invokeTarget 目标字符串
* @return method方法相关参数列表
*/
public static List<Object[]> getMethodParams(String invokeTarget)
{
String methodStr = StringUtils.substringBetween(invokeTarget, "(", ")");
if (StringUtils.isEmpty(methodStr))
{
return null;
}
String[] methodParams = methodStr.split(",(?=(?:[^\']*\"[^\']*\')*[^\']*$)");
List<Object[]> classs = new LinkedList<>();
for (int i = 0; i < methodParams.length; i++)
{
String str = StringUtils.trimToEmpty(methodParams[i]);
// String字符串类型,包含'
if (StringUtils.contains(str, "'"))
{
classs.add(new Object[] { StringUtils.replace(str, "'", ""), String.class });
}
// boolean布尔类型,等于true或者false
else if (StringUtils.equals(str, "true") || StringUtils.equalsIgnoreCase(str, "false"))
{
classs.add(new Object[] { Boolean.valueOf(str), Boolean.class });
}
// long长整形,包含L
else if (StringUtils.containsIgnoreCase(str, "L"))
{
classs.add(new Object[] { Long.valueOf(StringUtils.replaceIgnoreCase(str, "L", "")), Long.class });
}
// double浮点类型,包含D
else if (StringUtils.containsIgnoreCase(str, "D"))
{
classs.add(new Object[] { Double.valueOf(StringUtils.replaceIgnoreCase(str, "D", "")), Double.class });
}
// 其他类型归类为整形
else
{
classs.add(new Object[] { Integer.valueOf(str), Integer.class });
}
}
return classs;
}
/**
* 获取参数类型
*
* @param methodParams 参数相关列表
* @return 参数类型列表
*/
public static Class<?>[] getMethodParamsType(List<Object[]> methodParams)
{
Class<?>[] classs = new Class<?>[methodParams.size()];
int index = 0;
for (Object[] os : methodParams)
{
classs[index] = (Class<?>) os[1];
index++;
}
return classs;
}
/**
* 获取参数值
*
* @param methodParams 参数相关列表
* @return 参数值列表
*/
public static Object[] getMethodParamsValue(List<Object[]> methodParams)
{
Object[] classs = new Object[methodParams.size()];
int index = 0;
for (Object[] os : methodParams)
{
classs[index] = (Object) os[0];
index++;
}
return classs;
}
}
triggerJob只执行一次,createscheduleJob按照策略来执行,可能一次,或按照时间安排一直执行,resumeJob恢复执行,pauseJob暂停执行,deleteJob删除任务,因为存在数据一致性问题,所以有些时候需要先删再修改或者添加
JobKey jobKey = ScheduleUtil.getJobKey(jobId, jobGroup);
JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(getJobKey(jobId, jobGroup)).build();
scheduler.deleteJob(jobKey);
scheduler.triggerJob(ScheduleUtil.getJobKey(jobId, jobGroup), dataMap);
scheduler.resumeJob(ScheduleUtil.getJobKey(jobId, jobGroup));
然后quart中写要定时完成的任务接口
行,pauseJob暂停执行,deleteJob删除任务,因为存在数据一致性问题,所以有些时候需要先删再修改或者添加
JobKey jobKey = ScheduleUtil.getJobKey(jobId, jobGroup);
JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(getJobKey(jobId, jobGroup)).build();
scheduler.deleteJob(jobKey);
scheduler.triggerJob(ScheduleUtil.getJobKey(jobId, jobGroup), dataMap);
scheduler.resumeJob(ScheduleUtil.getJobKey(jobId, jobGroup));
然后quart中写要定时完成的任务接口
在service和controller中完成定时任务的CRUD