Quartz作为开源作业调度中的佼佼者,是作业调度的首选。但是集群环境中Quartz采用API的方式对任务进行管理,从而可以避免上述问题,但是同样存在以下问题:
问题一:调用API的的方式操作任务,不人性化;
问题二:需要持久化业务QuartzJobBean到底层数据表中,系统侵入性相当严重。
问题三:调度逻辑和QuartzJobBean耦合在同一个项目中,这将导致一个问题,在调度任务数量逐渐增多,同时调度任务逻辑逐渐加重的情况下,此时调度系统的性能将大大受限于业务;
问题四:quartz底层以“抢占式”获取DB锁并由抢占成功节点负责运行任务,会导致节点负载悬殊非常大;而XXL-JOB通过执行器实现“协同分配式”运行任务,充分发挥集群优势,负载各节点均衡。
XXL-JOB弥补了quartz的上述不足之处。

由于公司服务器时区使用的是标准时区,不是北京时区,导致很多问题,比如格式化日期对象时,得到的结果,不是北京时间。
首先发现的问题是Task任务程序打印的日志,不是北京时间,看了一下Logback配置,确实是简单的标准配置,没有指定时间:
<property name="log.pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%-4relative:%thread] %-5level %logger{56} - %msg%n" />修改为:
<property name="log.pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS,CTT} [%-4relative:%thread] %-5level %logger{56} - %msg%n" />官方说明:Chapter 6: Layouts (qos.ch)
CTT代表是上海,那么其他的去哪里查?其实在JDK的java.time.ZoneId文件里面就有:
public static final Map<String, String> SHORT_IDS = Map.ofEntries(
entry("ACT", "Australia/Darwin"),
entry("AET", "Australia/Sydney"),
entry("AGT", "America/Argentina/Buenos_Aires"),
entry("ART", "Africa/Cairo"),
entry("AST", "America/Anchorage"),
entry("BET", "America/Sao_Paulo"),
entry("BST", "Asia/Dhaka"),
entry("CAT", "Africa/Harare"),
entry("CNT", "America/St_Johns"),
entry("CST", "America/Chicago"),
entry("CTT", "Asia/Shanghai"),
entry("EAT", "Africa/Addis_Ababa"),
entry("ECT", "Europe/Paris"),
entry("IET", "America/Indiana/Indianapolis"),
entry("IST", "Asia/Kolkata"),
entry("JST", "Asia/Tokyo"),
entry("MIT", "Pacific/Apia"),
entry("NET", "Asia/Yerevan"),
entry("NST", "Pacific/Auckland"),
entry("PLT", "Asia/Karachi"),
entry("PNT", "America/Phoenix"),
entry("PRT", "America/Puerto_Rico"),
entry("PST", "America/Los_Angeles"),
entry("SST", "Pacific/Guadalcanal"),
entry("VST", "Asia/Ho_Chi_Minh"),
entry("EST", "-05:00"),
entry("MST", "-07:00"),
entry("HST", "-10:00")
);其实,CTT是简写,实际上就是代表Asia/Shanghai,注意这里没有北京,上海就是代表北京时区。
看了一下Cron表达式,定制的是
CRON:0 15 0 * * ? *
按设想也就是每天凌晨15分执行,由于服务器使用了标准时间,成了北京时间每天8点才执行,但是查看调度日志,发现打印时间却是凌晨15分,由于已经配置Logback怎么时间还是对不上?
查看源码后发现,XXLJOB的业务日志,是通过XxlJobHelper来记录的,但是他不是通过Logback打印的,而是将每次操作的日志独立成文件,存放到服务器,然后调度中心查看日志时,是拿的节点日志文件内容。
怎么看的,调度中心请求路径是:/xxl-job-admin/joblog/logDetailCat,然后跟源码就可以看到。
然后跟XxlJobHelper的操作即可,看到
private static boolean logDetail(StackTraceElement callInfo, String appendLog) {
XxlJobContext xxlJobContext = XxlJobContext.getXxlJobContext();
if (xxlJobContext == null) {
return false;
}
/*// "yyyy-MM-dd HH:mm:ss [ClassName]-[MethodName]-[LineNumber]-[ThreadName] log";
StackTraceElement[] stackTraceElements = new Throwable().getStackTrace();
StackTraceElement callInfo = stackTraceElements[1];*/
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(DateUtil.formatDateTime(new Date())).append(" ")
.append("["+ callInfo.getClassName() + "#" + callInfo.getMethodName() +"]").append("-")
.append("["+ callInfo.getLineNumber() +"]").append("-")
.append("["+ Thread.currentThread().getName() +"]").append(" ")
.append(appendLog!=null?appendLog:"");
String formatAppendLog = stringBuffer.toString();
// appendlog
String logFileName = xxlJobContext.getJobLogFileName();
if (logFileName!=null && logFileName.trim().length()>0) {
XxlJobFileAppender.appendLog(logFileName, formatAppendLog);
return true;
} else {
logger.info(">>>>>>>>>>> {}", formatAppendLog);
return false;
}
}刚才已经说了,如果你的服务器时区不是北京时间的话,使用Date时会有问题,再看其源码DateUtil中内容:
private static final ThreadLocal<Map<String, DateFormat>> dateFormatThreadLocal = new ThreadLocal<Map<String, DateFormat>>();
private static DateFormat getDateFormat(String pattern) {
if (pattern==null || pattern.trim().length()==0) {
throw new IllegalArgumentException("pattern cannot be empty.");
}
Map<String, DateFormat> dateFormatMap = dateFormatThreadLocal.get();
if(dateFormatMap!=null && dateFormatMap.containsKey(pattern)){
return dateFormatMap.get(pattern);
}
synchronized (dateFormatThreadLocal) {
if (dateFormatMap == null) {
dateFormatMap = new HashMap<String, DateFormat>();
}
dateFormatMap.put(pattern, new SimpleDateFormat(pattern));
dateFormatThreadLocal.set(dateFormatMap);
}
return dateFormatMap.get(pattern);
}为了效率使用了ThreadLocal来存储时间格式化对象,但是其没有配置时区,这样我们业务在北京,但是服务器使用标准时区,就会造成时间差异。
因此修改为:
private static final ThreadLocal<Map<String, DateFormat>> dateFormatThreadLocal = new ThreadLocal<Map<String, DateFormat>>();
private static DateFormat getDateFormat(String pattern) {
if (pattern==null || pattern.trim().length()==0) {
throw new IllegalArgumentException("pattern cannot be empty.");
}
Map<String, DateFormat> dateFormatMap = dateFormatThreadLocal.get();
if(dateFormatMap!=null && dateFormatMap.containsKey(pattern)){
return dateFormatMap.get(pattern);
}
synchronized (dateFormatThreadLocal) {
if (dateFormatMap == null) {
dateFormatMap = new HashMap<String, DateFormat>();
}
SimpleDateFormat bjSdf = new SimpleDateFormat(pattern); // 北京
bjSdf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai")); // 设置北京时区
dateFormatMap.put(pattern, bjSdf);
dateFormatThreadLocal.set(dateFormatMap);
}
return dateFormatMap.get(pattern);这样,无论Logback打印,还是实际的业务日志,都成了北京时间,更加方便我们调试日志了。
但是这里也就出现一个问题,就是Cron表达式要调整,比如要凌晨执行,就要改成凌晨8点执行了,当然刚才说了,所有日志的时间,都是按北京来的。
END
Java小强
未曾清贫难成人,不经打击老天真。
自古英雄出炼狱,从来富贵入凡尘。
发表评论: