最近有个需求,定时任务中的定时时间是从数据库中读取出来的,但是要求数据库修改需要立即生效。
之前使用定时器中的这个(实现SchedulingConfigurer接口) 会出现下一次执行才可以生效问题,就是说当前这次需要执行完成后才可以再次查询数据库获得定时器的下一次执行时间。
实现SchedulingConfigurer接口的核心代码如下:
import com.bigdata.bigdata.mapper.QuartzMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.Trigger;
import org.springframework.scheduling.TriggerContext;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Service;
import java.text.SimpleDateFormat;
import java.util.Date;
@Lazy(false)
@Service
@EnableScheduling
public class QuarzMessageTaskCron implements SchedulingConfigurer{
private final Logger logger = LoggerFactory.getLogger(QuarzMessageTaskCron.class);
@Autowired
private QuartzMapper quartzMapper;
private String cron = "0 */1 * * * ?";
private String reportSendHour; //运营日报的时间
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
logger.info("***进入定时发送运营日报方法*****");
Runnable task = new Runnable() {
@Override
public void run() {
// 定时任务的业务逻辑
SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dateStr = simpleDateFormat.format(new Date());
String reportContent = "测试内容";
logger.info(dateStr+"***运营日报发送内容****" +reportContent);
}
};
Trigger trigger = new Trigger() {
@Override
public Date nextExecutionTime(TriggerContext triggerContext) {
// 定时任务触发,可修改定时任务的执行周期
logger.info("*****判断cron之前*****"+cron);
getReportSettion();
if(reportSendHour!=null&&!"".equals(reportSendHour)&&!"null".equalsIgnoreCase(reportSendHour)){
cron = "0 */" + reportSendHour + " * * * ?";
}
SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dateStr = simpleDateFormat.format(new Date());
logger.info("*******当前时间:"+dateStr+",运营日报定时发送时间:" + cron+"********");
CronTrigger trigger = new CronTrigger(cron);
Date nextExecDate = trigger.nextExecutionTime(triggerContext);
return nextExecDate;
}
};
taskRegistrar.addTriggerTask(task, trigger);
}
// 获取短信系统设置
public void getReportSettion() {
//运营日报的发送时间
String tempReportSendHour = quartzMapper.queryByKey("report_sendhour");
if(tempReportSendHour.startsWith("0")){
reportSendHour = tempReportSendHour.substring(1);
}else {
reportSendHour = tempReportSendHour;
}
}
}
mapper接口:
@Mapper
public interface QuartzMapper {
public String queryByKey(String key);
}
QuartzMapperSql.xml文件:
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bigdata.bigdata.mapper.QuartzMapper">
<select id="queryByKey" parameterType="String" resultType="String">
select setvalue from quartz where setkey=#{key}
</select>
</mapper>
连接数据库配置application.properties文件
server:
port: 9999
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/test?serverTimezone=Hongkong&useUnicode=true&characterEncoding=UTF-8&useSSL=false&autoReconnect=true
username: root
password: 123456
driverClassName: com.mysql.cj.jdbc.Driver
mybatis:
mapper-locations: classpath*:mapper/*.xml
此时 会出现不会立即生效问题。
于是,查看前辈们的帖子(Spring update scheduler - Stack Overflow)解决了这个问题。
上源码:
定义接口(定时任务启动及停止)
public interface SchedulerObjectInterface {
void start();
void stop();
}
接口实现该接口
import com.bigdata.bigdata.config.SchedulerObjectInterface;
import com.bigdata.bigdata.mapper.QuartzMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.Trigger;
import org.springframework.scheduling.TriggerContext;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.support.CronTrigger;
import java.util.Date;
import java.util.concurrent.ScheduledFuture;
@Configuration
@EnableScheduling
public class MyFirstJob implements SchedulerObjectInterface {
private static final Logger log = LoggerFactory.getLogger(MyFirstJob.class);
private String cron="0 */1 * * * ?";
@Autowired
private QuartzMapper quartzMapper;
private ScheduledFuture future;
@Autowired
private TaskScheduler scheduler;
@Override
public void start() {
log.info("进入start方法");
future = scheduler.schedule(new Runnable() {
@Override
public void run() {
System.out.println( " Hello World! " + new Date());
}
}, new Trigger() {
@Override
public Date nextExecutionTime(TriggerContext triggerContext) {
String cron = cronConfig();
System.out.println("cron++++"+cron);
CronTrigger trigger = new CronTrigger(cron);
return trigger.nextExecutionTime(triggerContext);
}
});
}
@Override
public void stop() {
if (future != null) {
future.cancel(false);
}
}
// 读取数据库
private String cronConfig() {
try {
Thread.currentThread().sleep(1000*10);
} catch (InterruptedException e) {
e.printStackTrace();
}
String tempReportSendHour = quartzMapper.queryByKey("report_sendhour");
String reportSendHour="";
if(tempReportSendHour.startsWith("0")){
reportSendHour = tempReportSendHour.substring(1);
}else {
reportSendHour = tempReportSendHour;
}
if(reportSendHour!=null&&!"".equals(reportSendHour)&&!"null".equalsIgnoreCase(reportSendHour)){
cron = "0 */" + reportSendHour + " * * * ?";
}
log.info("查询cron"+cron);
return cron;
}
}
让其在项目启动的时候 就开始启动该定时任务
import com.bigdata.bigdata.quartz.MyFirstJob;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* 继承Application接口后项目启动时会按照执行顺序执行run方法
* 通过设置Order的value来指定执行的顺序
*/
@Component
@Order(value = 1)
public class StartService implements ApplicationRunner {
@Autowired
private MyFirstJob myFirstJob;
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println(new Date());
myFirstJob.start();
}
}
然后写一个controller验证是否修改数据库就会立即生效
import com.bigdata.bigdata.quartz.MyFirstJob;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestQuartzController {
@Autowired
private MyFirstJob myFirstJob;
@GetMapping("start")
public Object start(){
myFirstJob.start();
return "ok";
}
@GetMapping("stop")
public Object stop(){
myFirstJob.stop();
return "ok";
}
@GetMapping("updateCron")
public Object updateCron(){
myFirstJob.stop();
myFirstJob.start();
return "ok";
}
}
将数据库进行修改,然后执行updateCron即可发现定时任务立即停止并启动。
而自己最初的需求 解决办法就是在页面中修改数据库的信息时,将该任务停止并重新启动。
【*****重磅来袭*****】
最初项目只是一台机器,定时任务动态修改发送时间,是没问题的。但是后面又添加了三台机器(nginx负载),于是出现发生了问题。
【问题分析】
最初只是一台机器,定时任务是每天九点发送报告,是正常的。但是后来添加了三台,导致每天九点发送四条报告消息。
因为每台服务器都部署了这定时服务,当界面操作发送时间时,会通过nginx随机停止一台服务器,另外三台则保持正常,还是原来的发送时间点。
若是不修改时间点,相当于每台机器都跑了一个定时任务,则执行四次定时任务,于是乎发送4遍。
【解决办法】
在定时任务的run加一个redis锁,只让一台机器中获取到锁,正常执行定时任务。同时在修改时间点后,需要将全部机器都停止掉定时任务。
【代码】
pom.xml文件添加redis集群依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.3</version>
</dependency>
application.yml文件配置redis集群
# redis配置
redis:
sentinel: redis://192.168.1.20:6000,redis://192.168.1.21:6000
master: mymaster
创建redis集群bean在spring容器启动时读取
@Bean(name="redissonClient")
public RedissonClient getRedisConfig() {
Config config = new Config();
/单机
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
/集群
// SentinelServersConfig ssc = config.useSentinelServers().setMasterName(master);
// Set<String> redisSet = StringUtils.commaDelimitedListToSet(redisAddress);
// for (String address : redisSet) {
// ssc.addSentinelAddress(address);
// ssc.setPassword("123456");
// }
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
定时任务的run方法 添加redis锁
@Override
public void run() {
RLock lock = null;
boolean isLocked = false;
try {
//当前类的名字
String className = "com.bigdata.bigdata.quartz.QuarzMessageTaskCron";
lock = redissonClient.getFairLock(className);
isLocked = lock.tryLock();
if(isLocked) {
logger.info("获取锁成功,开始执行定时任务发送");
//业务逻辑处理 todo
System.out.println( " Hello World! " + new Date());
}else {
logger.info("获取定时任务锁失败");
}
} catch (Throwable e) {
logger.error(e.getMessage());
} finally {
if(isLocked) {
lock.unlock(); //释放锁
logger.info("释放定时任务锁");
}
}
}
其次是 修改发送时间后,如何关闭nginx代理的全部服务器的定时任务呢?
我是这样解决的,将nginx代理的服务器ip在配置文件写好,然后通过curl的形式(restTemplate形式)调用接口,这个接口就是停止定时在重新打开定时任务。
代码或者逻辑有不足之处,还请大家发表意见,共同进步!