OutOfMemoryError 作为多次搜索的结果

OutOfMemoryError as a result of multiple searches

我有一个经典的 Java EE 系统,带有 JSF 的 Web 层,用于 BL 的 EJB 3,以及对 DB2 数据库进行数据访问的 Hibernate 3。我在以下场景中苦苦挣扎:用户将启动一个过程,该过程涉及从数据库中检索大型数据集。检索过程需要一些时间,因此用户没有立即收到响应,不耐烦并打开新浏览器并再次启动检索,有时是多次。 EJB 容器显然没有意识到第一次检索不再相关的事实,并且当数据库 return 是结果集时,Hibernate 开始填充一组占用大量内存的 POJO,最终导致 OutOfMemoryError.

我想到的一个潜在解决方案是使用 Hibernate Session 的 cancelQuery 方法。但是,cancelQuery 方法仅在 数据库 return 成为结果集之前 有效。一旦数据库 return 成为结果集并且 Hibernate 开始填充 POJO,cancelQuery 方法就不再有效。在这种情况下,数据库查询自身 return 相当快,并且大部分性能开销似乎存在于填充 POJO 中,此时我们不能再调用 cancelQuery 方法。

我在完全不同的环境中遇到了类似的问题。我做了以下操作:在将新作业添加到我的队列之前,我首先检查 'same job' 是否已经从该用户入队。如果是这样我不接受第二份工作并告知用户。

这没有回答您关于在数据太大而无法放入可用 ram 时如何保护用户免受内存不足的问题。但这是保护您的服务器不做无用事情的好方法。

实施的解决方案最终如下所示:

一般的想法是维护当前 运行 查询到发起它们的用户的 HttpSession 的所有 Hibernate 会话的映射,这样当用户关闭浏览器时我们就可以终止 运行 查询。

这里有两个主要挑战需要克服。一个是将 HTTP session-id 从 Web 层传播到 EJB 层,而不干扰沿途的所有方法调用——即不篡改系统中的现有代码。第二个挑战是弄清楚一旦数据库已经开始返回结果并且 Hibernate 正在用结果填充对象时如何取消查询。

第一个问题已经解决,因为我们意识到沿堆栈调用的所有方法都由同一个线程处理。这是有道理的,因为我们的应用程序都存在于一个容器中 does not have any remote calls。在这种情况下,我们创建了一个 Servlet 过滤器,它拦截对应用程序的每个调用,并添加一个 ThreadLocal 变量和当前的 HTTP 会话 ID。这样,HTTP 会话 ID 将可用于沿线下方的每个方法调用。

第二个挑战有点棘手。我们发现负责 运行 查询并随后填充 POJO 的 Hibernate 方法被称为 doQuery 并且位于 org.hibernate.loader.Loader.java class 中。 (我们碰巧使用的是 Hibernate 3.5.3,但对于较新版本的 Hibernate 也是如此。):

private List doQuery(
        final SessionImplementor session,
        final QueryParameters queryParameters,
        final boolean returnProxies) throws SQLException, HibernateException {

    final RowSelection selection = queryParameters.getRowSelection();
    final int maxRows = hasMaxRows( selection ) ?
            selection.getMaxRows().intValue() :
            Integer.MAX_VALUE;

    final int entitySpan = getEntityPersisters().length;

    final ArrayList hydratedObjects = entitySpan == 0 ? null : new ArrayList( entitySpan * 10 );
    final PreparedStatement st = prepareQueryStatement( queryParameters, false, session );
    final ResultSet rs = getResultSet( st, queryParameters.hasAutoDiscoverScalarTypes(), queryParameters.isCallable(), selection, session );

    final EntityKey optionalObjectKey = getOptionalObjectKey( queryParameters, session );
    final LockMode[] lockModesArray = getLockModes( queryParameters.getLockOptions() );
    final boolean createSubselects = isSubselectLoadingEnabled();
    final List subselectResultKeys = createSubselects ? new ArrayList() : null;
    final List results = new ArrayList();

    try {

        handleEmptyCollections( queryParameters.getCollectionKeys(), rs, session );

        EntityKey[] keys = new EntityKey[entitySpan]; //we can reuse it for each row

        if ( log.isTraceEnabled() ) log.trace( "processing result set" );

        int count;
        for ( count = 0; count < maxRows && rs.next(); count++ ) {

            if ( log.isTraceEnabled() ) log.debug("result set row: " + count);

            Object result = getRowFromResultSet( 
                    rs,
                    session,
                    queryParameters,
                    lockModesArray,
                    optionalObjectKey,
                    hydratedObjects,
                    keys,
                    returnProxies 
            );
            results.add( result );

            if ( createSubselects ) {
                subselectResultKeys.add(keys);
                keys = new EntityKey[entitySpan]; //can't reuse in this case
            }

        }

        if ( log.isTraceEnabled() ) {
            log.trace( "done processing result set (" + count + " rows)" );
        }

    }
    finally {
        session.getBatcher().closeQueryStatement( st, rs );
    }

    initializeEntitiesAndCollections( hydratedObjects, rs, session, queryParameters.isReadOnly( session ) );

    if ( createSubselects ) createSubselects( subselectResultKeys, queryParameters, session );

    return results; //getResultList(results);

}

在这个方法中,你可以看到首先结果是从数据库中以一种很好的老式形式 java.sql.ResultSet 获取的,然后它在每个集合上循环运行并从中创建一个对象.在循环后调用的 initializeEntitiesAndCollections() 方法中执行一些额外的初始化。稍加调试后,我们发现大部分性能开销都在该方法的这些部分,而不是在从数据库中获取 java.sql.ResultSet 的部分,而 cancelQuery 方法仅有效在第一部分。因此解决方案是在for循环中添加一个附加条件,以检查线程是否被中断,如下所示:

for ( count = 0; count < maxRows && rs.next() && !currentThread.isInterrupted(); count++ ) {
// ...
}

以及在调用 initializeEntitiesAndCollections() 方法之前执行相同的检查:

if (!Thread.interrupted()) {

    initializeEntitiesAndCollections(hydratedObjects, rs, session,
                queryParameters.isReadOnly(session));
    if (createSubselects) {

        createSubselects(subselectResultKeys, queryParameters, session);
    }
}

此外,通过在第二次检查时调用Thread.interrupted(),标志被清除并且不影响程序的进一步运行。现在,当要取消查询时,取消方法访问存储在以 HTTP session-id 为键的映射中的 Hibernate 会话和线程,调用会话上的 cancelQuery 方法并调用 interrupt线程的方法。

对我来说太复杂了 :-) 我想为 "heavy" 查询创建单独的服务。并在其中存储有关查询参数的信息,可能是结果,这将是有效的有限时间。如果查询执行时间太长,用户会收到消息,执行他的任务将花费相当长的时间,他可能会等待或取消它。这种情况适用于分析查询。此变体使您可以简单地访问服务器上的任务 运行 以终止它。

但是,如果您遇到休眠问题,那么我认为问题不在分析查询中,而是在普通业务查询中。如果执行时间过长,可否尝试使用L2缓存(冷启动可能会很长,但热数据会立即收到)?或者优化hibernate\jbdc个参数?