Grails 中的多线程 - 将域对象传递到每个线程会导致某些字段随机为空

Multithreading in Grails - Passing domain objects into each thread causes some fields to randomly be null

我试图通过引入并行编程来加速 Grails 应用程序中的进程。这个特定的过程需要筛选成千上万的文档,从中收集必要的数据并将其导出到 excel 文件。

在尝试追查为什么这个过程进行得如此缓慢的几个小时之后,我确定这个过程必须做大量的工作来从每个域对象收集特定部分的数据。 (示例:域对象内部有数据列表,此过程获取这些列表中的每个索引并将其附加到带有逗号的字符串,以在 excel sheet。还有更多例子,但这些应该不重要。)

所以任何不是简单数据访问(document.id、document.name 等...)的事情都会导致此过程花费很长时间。

我的想法是为每个文档使用线程来异步获取所有这些数据,当每个线程完成数据收集后,它可以回到主线程并放入 excel sheet,现在所有的数据访问都是简单的,因为线程已经收集了所有的数据。

这似乎有效,但是我在域对象和线程方面存在错误。每个线程都传入其对应的文档域对象,但由于某种原因,文档域对象会随机将其部分数据更改为空。

例如:在文档被传递到线程之前,域对象的一部分将有一个如下所示的列表:[US, England, Wales],随机在任何一点,列表看起来像这在线程中:[美国,空,威尔士]。这发生在域对象的任何随机部分,在任何随机时间。

生成线程:

def docThreadPool = Executors.newFixedThreadPool(1)
def docThreadsResults = new Future<Map>[filteredDocs.size()]
filteredDocs.each {
    def final document = it
    def future = docThreadPool.submit(new DocumentExportCallable(document))
    docThreadsResults[docCount] = future
    docCount++
}

从线程取回数据:

filteredDocs.each {
        def data = docThreadsResults[count].get()

        build excel spreadsheet...
}

DocumentExportCallable class:

class DocumentExportCallable implements Callable {
    def final document

    DocumentExportCallable(document) {
        this.document = document
    }

    Map call() {
            def data = [:]

            code to get all the data...

            return data
    }
}

编辑: 如下所示,如果我可以向您展示领域对象,那将会很有用。但是我无法做到这一点。但是,你们问我有关域对象的事实让我认为这可能就是问题所在。事实证明,在线程中随机混乱的域对象的每个部分都是 "mapping" 内部域对象中的一个变量,它使用 SQL 连接来获取这些变量的数据。我刚刚了解到 Grails 中的惰性抓取与急切抓取。我想知道这是否可能是问题所在……默认情况下它被设置为延迟获取,因此每个线程对数据库的持续访问可能是出现问题的地方。我相信找到一种方法将其更改为急切获取可能会解决问题。

我找到了为什么这些空值随机出现的答案。现在一切似乎都在工作,我的实施现在比以前的实施快得多!

事实证明,即使在获取对象本身之后,当您访问这些字段时,具有 1-m 关系的 Grails 域对象也会进行单独的 sql 调用。这一定会导致这些线程进行非线程安全的 sql 调用,从而创建这些随机空值。在此特定情况下将这些 1 米的属性设置为急切获取可解决此问题。

对于稍后阅读的任何人,您需要阅读惰性与急切获取以获得更好的理解。

至于代码:

这些是在我的域对象中出现问题的 1-m 变量:

static hasMany = [propertyOne : OtherDomainObject, propertyTwo : OtherDomainObject, propertyThree : OtherDomainObject]

我在我的数据库调用中添加了一个标志,该标志将为这种特定情况启用此代码,因为我不希望在整个应用程序中始终急切地获取这些属性:

if (isEager) {
    fetchMode 'propertyOne', FetchMode.JOIN
    fetchMode 'propertyTwo', FetchMode.JOIN
    fetchMode 'propertyThree', FetchMode.JOIN
    setResultTransformer Criteria.DISTINCT_ROOT_ENTITY
}

抱歉,但目前我不记得为什么我必须在上面的代码中放置 "setResultTransformer",但没有它就会出现问题。也许稍后有人可以解释这一点,否则我相信 google 搜索会解释。

发生的事情是您的 grails 域对象正在从休眠会话中分离,因此当您的线程尝试加载惰性属性时会触发 LazyInitiationException。

切换到即时抓取对您来说很好,但它可能不是每个人的选择。您还可以做的是使用 grails 异步任务框架,因为它内置了会话处理。参见 https://async.grails.org/latest/guide/index.html

然而,即使使用 grails 异步任务在线程之间传递对象似乎也会将其分离,因为新线程将有一个新绑定的会话。我找到的解决方案是在新线程上 .attach().merge() 将其与调用线程上的会话绑定。

我认为最佳解决方案是让 Hibernate 在新线程上加载对象,这意味着在您的代码片段中您将传递一个文档 ID 并 Document.get(id) 在您的会话支持线程上。