问题描述

Spring Quartz是很常用的定时任务框架。把一个Quartz的工程部署到Tomcat中启动,意外地发现,每个Task都在同一时间跑了两次,而本地在开发的过程中却没有问题。

问题排查

为了防止多线程问题,有部分Task上是加了锁的,类似如下方式:

[@Component](https://my.oschina.net/u/3907912)
public class ExampleTask{
	private ReentrantLock lock = new ReentrantLock();
    protected void executeInternal(){
    	if (lock.tryLock()) {
            try {
                // task main logic
            } finally {
                lock.unlock();
            }
        }
    }
}

按理说,SpringBean默认是单例的,加了锁之后,同一时间,只会有一个线程能拿到锁,然后执行Task的逻辑才对。难道锁不生效?于是我们又新增了类似如下日志,把ReentrantLock对象和this都打印出来:

logger.info("lock: " + lock + ", this: " + this);

得到:

2020-05-12 06:26:40 INFO  ExampleTask:30 - 7db46a61-e1e6-4d26-a038-d2f6721f70ac|lock: java.util.concurrent.locks.ReentrantLock@1cd8d32a[Unlocked], this: cn.com.nightfield.ExampleTask@121f2ec1
2020-05-12 06:26:40 INFO  ExampleTask:30 - 51afa06a-7d61-493c-943d-6e1f8c2ecc79|lock: java.util.concurrent.locks.ReentrantLock@7e7aab34[Unlocked], this: cn.com.nightfield.ExampleTask@70bd5a8b

表示震惊:ReentrantLockthis竟然都不是同一个实例! 于是我们大致可以有一个结论:应该是工程跑了两遍导致的。果然,在log中看到,QuartzScheduler被初始化了两次:

......
2020-05-12 06:26:23 INFO  QuartzScheduler:240 - Quartz Scheduler v.2.2.1 created.
2020-05-12 06:26:23 INFO  RAMJobStore:155 - RAMJobStore initialized.
......
2020-05-12 06:26:28 INFO  QuartzScheduler:240 - Quartz Scheduler v.2.2.1 created.
2020-05-12 06:26:28 INFO  RAMJobStore:155 - RAMJobStore initialized.
......

自然的,把目标放到了Tomcat身上。

检查了一下server.xml文件:

<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
	<Context path="nightfield" docBase="/usr/local/tomcat/webapps/nightfield" debug="0" reloadable="false"/>
    <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
            prefix="localhost_access_log" suffix=".txt"
            pattern="%h %l %u %t &quot;%r&quot; %s %b" />
</Host>

问题就出在这里:我们把工程放到了Tomcatwebapps下面,而且把autoDeploy设成了true。 根据Tomcat官网对Automatic Application Deployment的介绍,当autoDeploytrue的时候,Tomcat会起线程监控appBase下的文件变化,当检测到有文件变化的时候,工程会被重新加载(reload)或被重新部署(redeploy)。所以在autoDeploy模式下,工程目录(docBase)需要指定在appBase目录之外:

3. 问题解决

有了官网的指导,问题解决也就很简单了,有三种方法:

  1. 把工程放到webapps外面:
<Context path="nightfield" docBase="/usr/local/nightfield" debug="0" reloadable="false"/>
  1. appBase设置成空:
<Host name="localhost" appBase="" unpackWARs="true" autoDeploy="true">
  1. autoDeploy设成false,顺便把deployOnStartup也设置成false
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="false" deployOnStartup="false">

总结

一般情况下,TomcatautoDeploy功能在开发过程中很有用,能节省调试过程中重启服务的时间;但是在服务器环境上,推荐关闭此功能。不当的使用,可能会使服务多次部署,导致无法预料的bug。

04-03 05:44