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