【问题标题】:Is running a long-running-process always bad on a servlet thread having a single HTTP request? [duplicate]在具有单个 HTTP 请求的 servlet 线程上运行长时间运行的进程总是很糟糕吗? [复制]
【发布时间】:2016-11-30 15:28:10
【问题描述】:

我有一个 Java 应用程序(在 WAS 8.5 上运行),它充当客户端的一种服务器清单。该应用程序有一个 servlet,可触发长时间运行的进程。

流程:从第三方数据库获取数据,执行 Java 逻辑,将记录写回应用程序自己的数据库(这些数据库连接是池化的)。

servlet 不是在启动时加载的,并且每月仅由单个操作人员手动触发一次(在某个特定日期基于客户每月的选择)。 servlet 在历史上一直以这种方式使用 Timer 和 TimerTask:

public class SyncMissingServlet extends HttpServlet implements Servlet{

    public void doPost(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException
        {               
            try{
                SyncMissing.runSync();
            }catch(Exception ex){
                logger.error(new LogMessage("ERROR: "), ex);
                this.sendReply(printWriter, "ERROR: " + ex.toString());
            }       
        }
}

public class SyncMissing
{
    public static void runSync() throws Exception 
    {
        Timer t = new Timer(true);

        SyncMissingTask task = new SyncMissingTask(); //SyncMissingTask is an instance of TimerTask
        // Start the synchronization 5 secs from now, and run it every 30 days.
        t.schedule(task, 5000, 2592000000l); //this 30 day timings never really worked out for the client, 
                                             //since the app server is restarted frequently for deployments.

    }
}

当前代码中没有使用 Timer.close() 或 TimerTask.close()。 最近这个 Servlet 似乎已经自动触发了,在系统重新启动并重新启动系统上的 WAS 服务之后......这就是担心。

虽然我无法向我的客户解释自动触发器,但我提出了以下选项:
1. 放弃使用 Timer 和 TimerTask(长时间运行的进程然后在 servlet 的线程本身上运行)
2. 代替 TimerTask,将其设为常规 Runnable,并在 servlet 线程内的单独线程中运行。
3.利用Java的Executor Service
4. 迁移到 Servlet 3.0,将 servlet 变成 Async servlet。
5. 完全放弃 servlet,并用批处理作业替换它。

我知道选项 3 和 4 确实是推荐的选项(或者可能是选项 5)。但我有一种感觉,在我的业务场景中 - 选项 3 和 4 可能是矫枉过正。

如果确实需要每月只有一个用户手动调用 servlet,选项 1 和 2 有那么糟糕吗? (我的客户想要最快的解决方案,当然不会资助选项 5)

【问题讨论】:

  • “单数据库连接”是指单例吗?您是否仅限于 JVM 中的一个连接?如果是这样,这就产生了问题。编辑您的问题以澄清。
  • 对不起。我现在已经更新了那部分。所有数据库连接都是池化的(包括连接到第三方数据库的连接)。此外,第三方数据库连接仅发生在整个应用程序中的这个 servlet...这个 servlet 有点独立,这个长时间运行的过程的结果不会从应用程序的任何其他模块引用。
  • 您还没有真正说出问题的确切性质。您是否希望任务每个日历月运行一次,但即使服务器重新启动也严格只运行一次?如果是这样,您需要在某处记录执行该任务的上一年月。在执行任务之前检查存储的值。
  • @Basil Bourque:你是正确的。作为第二步,我已经计划好了(记录最后一次运行的时间戳,以确保它严格每月运行一次)。但是,我首先尝试了解在我的情况下是否存在任何性能问题 - 如果我在 servlet 线程本身(或其中的并行线程)中运行该进程。我的场景 - 每个月只有一个计划的对 servlet 的请求。

标签: java multithreading servlets


【解决方案1】:

好吧,如果 servlet 应该一个月只运行一次,并且只有一个客户端触发它,那么可以在 servlet 的线程本身中运行它,或者在 servlet 中创建一个新线程并让它执行任务。当您有很多客户端同时发出请求时,就会出现负载和响应时间的问题,此时您可能想要使用 Executor 服务或异步 servlet。

【讨论】:

  • 请原谅我对这件事缺乏了解。说,如果我确实在 servlet 线程中的一个单独线程上运行该进程,并且在应用程序服务器运行时出现计划外的系统重新启动,并且顺便说一下线程的 run() 在那一刻被执行......会发生什么线程本身? (我怀疑线程是否会正常关闭,例如应用服务器重启)
  • 如果是正确的重启,那么应用服务器将有机会在系统停机重启之前完成它正在做的事情并自行关闭,所以“理想情况下”应该没问题。但是,取决于 init 服务,它会在杀死应用服务器之前等待多长时间 :)
【解决方案2】:

无需通过调用 servlet 来激活后台任务。您的 Web 应用程序有自己的生命周期。 Servlet spec 为您的网络应用程序设置和拆除提供了挂钩。无需客户端用户调用 servlet 即可启动和退出后台任务的理想场所。

无需依赖人类用户记住启动后台任务。让您的网络应用技术为您完成工作。

此外,您可能经常听到/读到“从不从 JSP 或 Servlet 启动线程”。对于处理生成网页的每个传入请求,这是值得建议的。但是后台任务(与单个 servlet 请求没有直接关系)是另一回事;只要您正确处理它们,就可以将线程用于后台任务。 “正确”是指您明确地适当地结束这些线程,并处理thread-safety 问题。后台任务的一个示例可能是定期轮询 Web 服务或数据库以刷新数据缓存。

ServletContextListener

如果您希望在您的网络应用中定期执行自动化任务,请使用ServletContextListener

该接口定义了一对方法。一,contextInitialized,在 Web 应用程序启动时自动调用,保证在处理任何 HTTP 请求之前运行。另一种方法contextDestroyed 在 Web 应用程序被拆除时运行。

提示:使用 @WebListener 注释标记您的侦听器将导致您的 Servlet 容器自动注意到它并在 Web 应用程序启动时进行实例化。

当心a nasty bug when doing development with NetBeans & Tomcat(仅是开发问题,不是部署问题),其中 Web 应用会进行两次启动。

ScheduledExecutorService

在您实现该接口的自定义类中,在contextInitialized 中,建立一个ScheduledExecutorService 对象以重复运行您的任务。在contextDestroyed 中,关闭该执行程序。这非常重要,因为该执行程序的线程将在您的 Web 应用程序甚至 servlet 容器关闭后继续存在。

ScheduledExecutorService 技术取代了TimerTimerTask 类。尤其不建议在 Java Servlet 环境中使用这些类。

您可以在侦听器对象中存储对执行器的引用。

@WebListener
class MonthlyTaskRunner implements ServletContextListener {
    private ScheduledExecutorService scheduledExecutorService;

    void contextInitialized(ServletContextEvent see) {
        // initialize your ScheduledExecutorService. 
        // The ScheduledExecutorService will use one or more threads for its work outside of this thread running now.
        this.scheduledExecutorService = … ;
    }

    void contextInitialized(ServletContextEvent see) {
        // Shutdown the executor along with its thread(s).
        this.scheduledExecutorService.shutDown();
    }


}

我和其他人已经在 Stack Overflow 上广泛发布了此内容,例如 this。所以search Stack Overflow。我已经在 Vaadin Web 应用程序的上下文中发布了大量示例,但这些原则适用于任何 servlet Web 应用程序。并查看Oracle Tutorial on Executors

实例化后在哪里存储对 ScheduledExecutorService 的引用?您可以存储在上下文侦听器的成员变量中。但是更容易访问的地方将是作为 servlet 上下文的“属性”。我在对另一个问题的回答中详细描述了这一点以及示例代码和漂亮的图表:Start & Stop a ScheduledExecutorService in Java EE environment using servlet

YearMonth

在该执行程序任务中,获取您的业务上下文时区的当前日期的年月。将该年份月份与上次执行任务时记录的月份进行比较。在某个地方、一个文件、一个数据库、某个地方记录那个年份的月份。

安排您的ScheduledExecutorService 以不必要的频率运行。与其担心安排一个月,不如让它每天运行。将当前YearMonth 与存储的年月进行比较的检查几乎不需要执行时间。 KISS.

Java 包含一个YearMonth 类。

YearMonth ymThen = YearMonth.parse( "2016-11" );  // Retrieve that string from storage.

ZoneId z = ZoneId.of( "America/Montreal" );
YearMonth ymNow = YearMonth.now( z );
if( ymNow.isAfter( ymThen ) ) {
    // … run the task
    String ymOutput = ymNow.toString();  
    // … write that `ymOutput` string someplace in storage.
 } // Else do nothing. Let the ScheduledExecutorService run again after its designated rest period.

类似问题

【讨论】:

  • 我很欣赏 ServletContextListener 的使用,并且我可以很好地使用 ScheduledExecutorService 而不是 TimerTask。此外,侦听器的 contextDestroyed() 方法可以方便地正确停止执行程序。但是,在服务器启动时自动执行进程——这正是我的客户不想做的事情。这是该过程的手动触发,每个日历月一次(但并不总是严格地在特定的一天)。所以,我想我不应该使用监听器的 contextInitialized()...而且我仍然需要一个 servlet...不是吗?
  • 然后我可以做这样的事情(在 servlet 的 doPost() 中),让进程在 5 秒延迟后启动,并且没有任何未来调度:private ScheduledExecutorService scheduler; scheduler = Executors.newSingleThreadScheduledExecutor(); scheduler.schedule(new SomeDailyJob(), 5, TimeUnit.SECONDS); 然后问题出现了 - 在哪里做我声明了 ScheduledExecutorService 的实例(第一条语句),以便它对两者都可用 - 用于调度的 servlet 的 doPost + 用于关闭的侦听器的 contextDestroyed() ?
  • @Tatha 所以这个过程是每个月手动触发的,但您使用计时器来安排下个月?如何手动触发自动调度?用词似乎是矛盾的。你的故事很混乱。
  • 你说得对……听起来很奇怪。这是故事。一年前客户自己编写代码时,他们在 doPost() 中使用 TimerTask 进行了调度,但很快意识到他们运行该流程的业务需求不会是每个月的固定日期。然而,他们并不关心自己敲掉调度代码,并且一直在他们需要的任何日期手动触发 servlet。这并没有造成问题,因为每个月他们在前 2 周左右都有代码部署,并且服务器重启取消了从上个月触发开始的任何进程调度。
  • 最近,在他们与我们签订了 IT 服务合同后,他们现在想要完全取消调度工作,并将其变成一个常规的 servlet。这能说明情况吗?我希望我能用更短的 cmets 更好地解释它..
猜你喜欢
  • 1970-01-01
  • 2020-03-19
  • 2010-12-11
  • 1970-01-01
  • 1970-01-01
  • 2011-01-12
  • 1970-01-01
  • 2012-08-20
  • 2011-08-17
相关资源
最近更新 更多