在集群上,每台主机部署的代码一样,会出现定时任务重复的情况,考虑过三种实现。
第一种方式采用过固定一台IP的方式,牺牲了其他几台主机的作用;
第二种是在分配任务时加了随机时间,先休眠结束的任务,抢到任务并将job的状态改成RUNNING,确实每台主机都用到了,但这样做有两个问题,首先,不能完全避免多个地方同时结束Thread.sleep(),另外增加了处理的时间间隔。
下面这个方式是通过让集群每个主机节点争夺数据库行锁来实现的。
第一步:将所有定时任务配置在job表
关键字段:JOB_NAME任务名称、JOB_RUN_STATUS任务状态、IP_ADDRESS执行的IP
第二步:执行任务前,加入判断 JobService.canExecute(job)
执行定时任务前,需判断本任务是否可执行
@Scheduled(fixedDelayString = "${report.task.time}")
public void runBorrowDataUpdateJob() {
Map job =new HashMap();
job.put("jobName", "generateJob");
try {
//获取主机IP
InetAddress address;
address = InetAddress.getLocalHost();
String currentIP = address.getHostAddress();
job.put("ipAddress", currentIP);
//等待状态则执行,把本机IP记录在sys_batch_job
if(JobService.canExecute(job)){ //判断是否可执行
//目标job
generateService.generateReport();
job.put("jobStatus", "ONWAIT");
job.put("beginTime", null);
job.put("runCount", "调用次数+1");
job.put("endTime", "记录结束时间");
JobService.updateJob(job);
}
} catch (Exception e) {
job.put("jobStatus", "ONWAIT");
job.put("beginTime", null);
job.put("runCount", "调用次数+1");
job.put("endTime", "记录结束时间");
JobService.updateJob(job); //如果出现异常,当前job置于等待状态
LoggerUtil.info("GenerateJob失败",e);
}
第三步:判断是否可执行
①Mapping
JobService.canExecute(job)中需要从库表中查询当前job是否可执行,当集群中一台主机占据这个行锁,其他主机只能等待,加了nowait则抛出(Caused by: java.sql.SQLException: ORA-00054: resource busy and acquire with NOWAIT specified or timeout expired)直接返回,getRunningJob对应的mapping如下
<select id="getRunningJob" parameterType="String" resultType="String">
SELECT job_name FROM sys_batch_job
WHERE job_name = #{jobName}
AND job_run_status = 'ONWAIT'
for update nowait
</select>
②SERVICE
由于链接默认是自动commit的,需要关闭自动提交事务,最后在finally中记得sqlSession.commit和close();
canExecute()对应的代码如下
@Transactional
@Override
public Boolean canExecute(Map job){
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
SqlSession sqlSession=null;
try {
factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:/mapper/report/jobMapper.xml"));
factoryBean.setDataSource(institutionDataSource);
sqlSession = factoryBean.getObject().openSession(false); //手动事务(重点)
//不在RUNNING状态,启动服务,更改IP、状态成RUNNING
if (null != getRunningJob(job.get("jobName")+"")){ //调用①部分,加锁
job.put("jobStatus", "RUNNING");
job.put("beginTime", "记录起始时间");
//commit后,当前JOB更新成RUNNING状态,其他进程无法获取当前数据行
jobDao.updateJob(job);
return true;
}
//在当前job在RUNNING状态
LoggerUtil.info(job.get("jobName")+"已被其他服务器执行");
return false;
} catch (IOException e) {
LoggerUtil.error("读取xml文件出错", e);;
} catch (Exception e) {
LoggerUtil.error("表锁住了", e);
} finally{
sqlSession.commit(true);
sqlSession.close();
}
return false;
}
第四步: 验证效果
某个时间
一段时间后
执行次数和IP发生了变化,且定时任务数据在同一时间没有出现重复的情况