烦恼:日常开发时经常要用到定时器
- 虽然SpringBoot提供的Scheduled还不错,但是增加了业务系统复杂度,想着要不独立出来一个系统运作吧,而且要复用性够强才行。
思考:日常业务中有哪些类型的定时任务呐
- 定时修改数据状态。需要操作数据库能力。但是又不能依赖具体数据库,要像个工具一样独立。
- 定时触发某个任务。需要处理业务能力。但是不能依赖于具体业务,不然就对某个项目耦合了。
思考:系统核心能力有哪些,以及工具选用
- 最核心的能力
- 操作数据库,但可以不依赖数据库,连接与sql执行可操作空间大:JDBC数据库!
- 定时发送请求,让业务系统执行业务:SpringBoot中的RestTemplate!
- 动态添加定时任务:Quartz!
- 提供一定程度的任务恢复能力:redis!
代码实现:
- 若直接投入生产环境,请尽量让定时服务系统保持服务无状态性。
- 系统代码多,提供下载链接:
https://download.csdn.net/download/k295330167/18353287?spm=1001.2014.3001.5501
系统存在的缺陷,未实现,提供以下建议:
- 高可用性问题
- 构建集群,quartz是支持集群部署。
- 一致性问题
- 可将任务信息持久化,避免因系统宕机,定时任务丢失。
- 异常处理
- 当前系统没有提供定时任务处理失败后的异常处理。
核心实现代码解释:
定时数据库数据处理:
- 客户端将数据库的信息以及sql语句发送到定时服务器中。
- 时间到了后,构造JDBC数据库连接,提交执行对应的SQL语句即可。
package com.miracle.timer.job;
import com.miracle.timer.common.exception.BusinessErrorEnums;
import com.miracle.timer.common.exception.BusinessException;
import com.miracle.timer.domain.RequestJobData;
import com.miracle.timer.services.RedisTaskService;
import com.mysql.cj.jdbc.Driver;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.dbcp2.BasicDataSource;
import org.apache.commons.dbcp2.DataSourceConnectionFactory;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.transaction.annotation.Transactional;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Objects;
/**
* @author Miracle
* @date 2019/7/9 13:22
*/
@Data
@Slf4j
public class DatabaseHandleJob extends QuartzJobBean {
/**
* 数据库账号
*/
private String username;
/**
* 数据库库密码
*/
private String password;
/**
* 连接地址
*/
private String url;
/**
* SQL语句
*/
private String handleString;
@Autowired
private RedisTaskService redisTaskService;
@Override
@Transactional(rollbackFor = Exception.class)
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
DataSourceConnectionFactory connectionFactory;
try (BasicDataSource dataSource = new BasicDataSource()){
dataSource.setUsername(username);
dataSource.setPassword(password);
dataSource.setUrl("jdbc:mysql://" + url + "?useUnicode=yes&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8");
dataSource.setDriver(new Driver());
connectionFactory = new DataSourceConnectionFactory(dataSource);
} catch (Exception e) {
e.printStackTrace();
JobExecutionException e2 = new JobExecutionException(e);
// true 表示 Quartz 会自动取消所有与这个 job 有关的 trigger,从而避免再次运行 job
// e2.setUnscheduleAllTriggers(true);
e2.setUnscheduleFiringTrigger(true);
throw e2;
}
try(Connection connection = connectionFactory.createConnection();
PreparedStatement preparedStatement = connection.prepareStatement(handleString);) {
preparedStatement.execute();
log.info(jobExecutionContext.getJobDetail().getKey().getName() + "执行了Database操作");
}catch (Exception e) {
log.error(e.getMessage());
JobExecutionException e2 = new JobExecutionException(e);
// true 表示 Quartz 会自动取消所有与这个 job 有关的 trigger,从而避免再次运行 job
// e2.setUnscheduleAllTriggers(true);
e2.setUnscheduleFiringTrigger(true);
}
// 判断是否已经运算完毕
if (Objects.isNull(jobExecutionContext.getNextFireTime())){
redisTaskService.deleteJob(RequestJobData.REDIS_KEY, jobExecutionContext.getJobDetail().getKey().getName());
}
}
}
定时请求任务
- 客户端将请求的信息发送到定时服务系统中。
- 时间到达后,利用SpringBoot的RestTemplate构造请求发送到业务系统。
package com.miracle.timer.job;
import com.miracle.timer.domain.RequestJobData;
import com.miracle.timer.services.RedisTaskService;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.web.client.RestTemplate;
import java.util.*;
/**
* @author Miracle
* @date 2019/7/9 13:32
*/
@Slf4j
@Data
public class RequestHandleJob extends QuartzJobBean {
/**
* 请求方法
*/
private String method;
/**
* 请求链接
*/
private String url;
/**
* 参数
*/
private Map<String, Object> parameter;
@Autowired
private RedisTaskService redisTaskService;
private static final RestTemplate REST_TEMPLATE = new RestTemplate();
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
if (Objects.isNull(parameter)){
parameter = new HashMap<>(0);
}
try{
switch (method.toUpperCase()){
case "GET":
// request访问
REST_TEMPLATE.getForObject(getParameterUrl(),String.class);
break;
case "PUT":
REST_TEMPLATE.put(url, parameter);
break;
case "POST":
REST_TEMPLATE.postForEntity(url, parameter, String.class);
break;
case "DELETE":
REST_TEMPLATE.delete(getParameterUrl());
break;
default:
log.error("未知的方法处理");
throw new RuntimeException("未知的方法处理");
}
// 判断是否已经运算完毕
if (Objects.isNull(jobExecutionContext.getNextFireTime())){
redisTaskService.deleteJob(RequestJobData.REDIS_KEY, jobExecutionContext.getJobDetail().getKey().getName());
}
log.info(jobExecutionContext.getJobDetail().getKey().getName() + "执行了Request操作");
}catch (Exception e){
JobExecutionException e2 = new JobExecutionException(e);
// true 表示 Quartz 会自动取消所有与这个 job 有关的 trigger,从而避免再次运行 job
// e2.setUnscheduleAllTriggers(true);
e2.setUnscheduleFiringTrigger(true);
throw e2;
}
}
private String getParameterUrl() {
StringBuilder getUrlString = new StringBuilder();
getUrlString.append(url);
// 判断是否有参数
if (parameter.size() > 0){
getUrlString.append("?");
// 遍历所有参数
for (Map.Entry<String, Object> map : parameter.entrySet()){
getUrlString.append(map.getKey()).append("=").append(map.getValue().toString()).append("&");
}
// 删除最后一个"&"
getUrlString.delete(getUrlString.length() - 1,getUrlString.length());
}
return getUrlString.toString();
}
}