定时任务
定时任务
1. 源码分析
在实际项目开发中 Web 应用有一类不可缺少的,那就是定时任务。 定时任务的场景可以说非常广泛,比如某些视频网站,购买会员后,每天会给会员送成长值,每月会给会员送一些电影券; 比如在保证最终一致性的场景中,往往利用定时任务调度进行一些比对工作;比如一些定时需要生成的报表、邮件;比如一些需要定时清理数据的任务等。 所以我们提供方便友好的 web 界面,实现动态管理任务,可以达到动态控制定时任务启动、暂停、重启、删除、添加、修改等操作,极大地方便了开发过程。
1.1 表结构说明
sys_job
表:这是核心的定时任务表,用于存储定时任务的配置信息,如任务名称、任务组、执行的类全名、执行的参数、cron 表达式等
sys_job_log
表:用于记录定时任务的执行日志,包括任务的开始执行时间、结束执行时间、执行结果等
1.2 目录结构
1)后端代码
- ScheduleUtils 定时任务工具类
- 抽象 quartz 调用类 AbstractQuartzJob(实现Job接口)
- 定时任务处理类(禁止并发执行) 继承抽象类
- 定时任务处理类(允许并发执行) 继承抽象类
- 任务执行工具类 JobInvokeUtil
- cron 表达式工具类
2)前端代码
1.3 Quartz 体系结构
1.4 Quartz 核心 API
API | 描述 |
---|---|
Job | - 实际要执行的任务类 - 必须实现Quartz的 Job 接口。 |
JobDetail | - 代表一个Job 实例- 通过 JobBuilder 类创建。 |
JobBuilder | - 用于声明一个任务实例 - 可以定义关于该任务的详情,如任务名、组名等。 |
Trigger | - 触发器,用来触发并执行Job 实例的机制。 |
SimpleTrigger | - 用于简单重复执行作业的触发器 - 例如:每隔一定时间执行一次。 |
CronTrigger | - 使用Cron表达式定义执行计划的触发器 - 适用于定义复杂的执行时间。 |
TriggerBuilder | - 用于创建触发器Trigger 实例的构建器。 |
Scheduler | - Quartz中的核心组件 - 负责启动、停止、暂停和恢复任务。 |
1.5 定时任务执行
项目在启动时,初始化定时任务
流程图:
1.6 添加定时任务
在项目启动时,从数据库中查询任务配置列表,然后创建定时任务,并根据状态判断是否执行任务调度,那如果是新添加的定时任务该如何处理呢?为了解答这个问题,我们来对这部分的源码进行分析。
重点关注后端部分,入口在定时任务模块的 sysJobController 中:
/**
* 修改定时任务
*/
@PreAuthorize("@ss.hasPermi('monitor:job:edit')")
@Log(title = "定时任务", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody SysJob job) throws SchedulerException, TaskException
{
if (!CronUtils.isValid(job.getCronExpression()))
{
return error("修改任务'" + job.getJobName() + "'失败,Cron表达式不正确");
}
else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_RMI))
{
return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'rmi'调用");
}
else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.LOOKUP_LDAP, Constants.LOOKUP_LDAPS }))
{
return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'ldap(s)'调用");
}
else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.HTTP, Constants.HTTPS }))
{
return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'http(s)'调用");
}
else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), Constants.JOB_ERROR_STR))
{
return error("修改任务'" + job.getJobName() + "'失败,目标字符串存在违规");
}
else if (!ScheduleUtils.whiteList(job.getInvokeTarget()))
{
return error("修改任务'" + job.getJobName() + "'失败,目标字符串不在白名单内");
}
job.setUpdateBy(getUsername());
return toAjax(jobService.updateJob(job));
}
代码流程图:
1.7 定时任务状态修改
通过分析新增定时任务的源码,可以发现,任务在初始化时是处于暂停状态的。
如果要启动任务,可以在页面进行任务状态的开关控制,所以接下来对此功能的源码进行分析。
入口在定时任务模块的 sysJobController 中:
/**
* 定时任务状态修改
*/
@PreAuthorize("@ss.hasPermi('monitor:job:changeStatus')")
@Log(title = "定时任务", businessType = BusinessType.UPDATE)
@PutMapping("/changeStatus")
public AjaxResult changeStatus(@RequestBody SysJob job) throws SchedulerException
{
// 根据任务ID查询数据库,获取当前任务的配置信息
SysJob newJob = jobService.selectJobById(job.getJobId());
// 将前端的状态设置到任务配置对象中
newJob.setStatus(job.getStatus());
// 调用服务层的方法根据状态启动/暂停任务,并返回操作结果
return toAjax(jobService.changeStatus(newJob));
}
代码流程图:
2. 集群模式
2.1 集群介绍
为什么需要 Quartz 集群?
在单机模式下,默认所有的 jobDetail
和 trigger
都存储在内存中。这样做的好处是读取速度快,但缺点也很明显:一旦服务器故障,所有的任务数据就会丢失,这就是所谓的单点故障问题。
还有如果在一个高峰时段,比如上午 点,需要触发 个任务,这将给服务器带来巨大的负载压力。这不仅影响性能,还可能引发服务中断。
缺点:单点故障、负载压力大
为了解决这些问题,我们可以部署多个服务器节点,将任务信息存储到数据库中。这样,多个节点就可以通过共享数据库来协调任务的执行,形成 Quartz 集群模式。
这种方式不仅解决了单点故障问题,还能通过负载均衡提升效率。
集群模式的优势:
- 高可用性:即使某个节点出现问题,其他节点仍然可以正常运行。
- 负载均衡:将任务分散到不同的节点执行,避免单个节点过载。
通常在生产环境中,我们会部署多台服务器,所以采用集群模式不会产生额外的成本。
quartz集群所需数据库表:
表名 | 用途 |
---|---|
qrtz_triggers | 存储触发器的基本信息,如触发器名称、组、类型等。 |
qrtz_cron_triggers | 存储Cron触发器的额外信息,如Cron表达式。 |
qrtz_simple_triggers | 存储简单触发器的额外信息,如重复次数和间隔。 |
qrtz_blob_triggers | 存储BLOB类型触发器的额外信息,如持久化的数据。 |
qrtz_simprop_triggers | 存储具有单一触发器属性的触发器的详细信息。 |
qrtz_job_details | 存储作业详细信息,如作业名称、组、描述、作业类名等。 |
qrtz_scheduler_state | 存储调度器的状态信息,如当前主节点信息等。 |
qrtz_locks | 存储锁信息,用于控制并发和防止资源冲突。 |
qrtz_paused_trigger_grps | 存储被暂停的触发器组的信息。 |
qrtz_fired_triggers | 存储已触发的触发器的详细信息,包括执行历史。 |
qrtz_calendars | 存储日历信息,定义工作日和非工作日,用于调度时间约束。 |
2.2 实现
2.2.1 导入sql
将 ruoyi 提供的 quartz.sql
导入到数据库中:
2.2.2 开启配置
打开 dkd-quartz
模块中 ScheduleConfig
配置类注释:
2.2.3 节点复制
首先修改当前 SpringBoot 的启动类的名称
再添加(复制)一个 SpringBoot 的启动配置
-Dserver.port=8081
2.2.4 观察数据库
重启项目即可,观察数据库,已存入 jobDetail
和 trigger
,多个服务器节点可以实现共享。