记一次定时任务重复执行问题的解决过程
问题现象
项目中的定时任务每次都会在同一时间执行两次,但是手动停止任务之后,重新启动又恢复“正常”,对于这个问题,此前的项目负责人给出的解释的这是个有点历史的项目了,定时任务是用 Timer 实现的,这应该是 Timer 的 bug,无法解决。因此一开始接手这个项目的时候,解决这个问题并不在我的任务列表里,但是后面发现因为这个问题是会接连引发其他业务问题的,所以决定还是自己动手查找一下这个问题。
查找问题
查代码
首先定位定时任务的相关代码,大致的实现方式是这样:每一个定时任务是一个 TimerTask,用一个 Timer 负责调度所有定时任务,用一个 Map 保存每一个任务与 TimerTask 的映射关系,项目启动时在 ServletContextListener.contextInitialized()
方法中初始化所有定时任务,这个实现看起来是没有问题的。
打印日志
既然初次判断代码没有问题,那么我们可以通过简单的打印日志的方式查看定时任务的执行轨迹,也许是在中间的某个方法重复执行了呢。于是,从ServletContextListener.contextInitialized()
到 Timer.schedule()
到 TimerTask.run()
到 TimerTask.cancel()
几个关键节点到加上日志输出。观察定时任务执行情况。通过查看日志,问题马上就浮出水面了,ServletContextListener.contextInitialized()
方法调用了两次,也就意味着,在方法中执行的定时任务初始化工作也做了两次,这就与定时任务重复执行的现象吻合了。
检查 Tomcat 配置
通过上一步,初步确定了这个现象不是代码的问题,而是重复初始化导致的,那么 ServletContextListener.contextInitialized()
是在什么时候调用的呢?答案是 servlet 容器启动时 ServletContextListener 就会调用 contextInitialized 方法,所以这是 servlet 容器的问题,也就是 Tomcat。于是直接打开 conf/server.xml
文件,检查配置是否正确(替换了域名项目名):
1
2
3
4
<Host name="localhost" appBase="webapps"
unpackWARs="true" autoDeploy="true" reloadable="true">
<Context path="" docBase="my_app" reloadable="true" />
</Host>
果然,问题就出在这里的配置,首先解释一下这里两个关键的配置参数:
- appBase: 这个目录下面的子目录将自动被部署为应用,且 war 文件将被自动解压缩并部署为应用,默认为 tomcat 下 webapps 目录
- docBase: 指向了某个应用的目录,该目录也会被部署为一个应用,这个可以和 appBase 没有任何关系
这两个参数都是指定了应用所在的目录,两个目录下的应用都会被部署,my_app 项目在 webapps 目录下,所以首先它会被自动部署,而 docBase 也是指定了同一个项目,所以 my_app 会被第二部署。这就解释了 ServletContextListener.contextInitialized()
执行两次的情况了。至此,算是找到了问题的源头。使用这样的配置 Tomcat 是可以正常运行的,项目也都可以正常部署,但是 conf/server.xml
中默认是没有 context 配置的,因此,访问 webapps 下的项目是需要带项目名的,例如:http://localhost:8080/my_app/,而如果配置了 context ,则可以通过域名之间访问到 docBase 指定目录的项目,也就是说访问 http://localhost:8080/ 就是访问 my_app 项目,这也就是为什么上面要配置 context。
解决问题
找到问题源头之后,要解决这个问题其实就很简单了,有两个方法:
- 保留 appBase 参数配置,删除 context 配置,这样修改之后通过项目名访问
- 修改 appBase=””, docBase=”my_app的绝对路径”,这样修改之后通过域名直接访问,但是 webapps 下的其他项目就不会被部署了。
实际上,这里只有 my_app 一个项目,所以我采用了第二种方法,因为可以继续域名直接访问,而且可以独立管理这个项目,如果有另外的项目需要放到同一个服务器的话,可以在 conf/server.xml
增加一个 host 配置独立管理。
后续问题
解决了项目重复部署的问题之后,定时任务也恢复了正常。但是第二天用户就反馈富文本编辑器中的图片都显示不了了,检查图片链接发现确实都 404 了,因为图片链接都是带项目名的,原来富文本编辑器中保存图片地址是有前缀项目名的:/my_app/upload/pic/xxxx.webp
。这个问题其实也解释项目确实部署了两次,之前可以正常访问就是因为带项目名或者不带都能访问到 my_app 项目,而现在修改了配置之后,带项目名已经不能访问了。用 nginx 就可以快速解决这个问题了,只需要将所有 /my_app/ 的请求都转发到原始域名即可。
1
2
3
4
location /my_app {
proxy_pass http://原始域名;
proxy_set_header X-Real-IP $remote_addr;
}