多线程jpa读取时休眠空指针异常
Posted
技术标签:
【中文标题】多线程jpa读取时休眠空指针异常【英文标题】:Hibernate Null-pointer exception when multi-threaded jpa read 【发布时间】:2021-01-09 20:39:22 【问题描述】:我有一个 spring-batch 作业,它使用 RepositoryItemReader
从数据库中读取,然后将结果转换为 Map
,然后将结果写入 elasticsearch。它工作正常,虽然有点慢。所以现在我想添加一个池大小为 4 的 taskExecutor
以加快速度:
return stepBuilders.get("search-export").<AbstractEntityDefinition, Map<String, Object>>chunk(nemesisSearchProperties.getExport().getChunkSize())
.reader(reader)
.processor(processor)
.writer(writer)
.stream(reader)
.transactionAttribute(transactionAttribute)
.listener(chunkSessionReplicatorExecutionListener)
.listener(new NemesisChunkLoggingListener(indexName + " search export", nemesisSearchProperties.getExport().getChunkSize()))
.taskExecutor(searchExportTaskExecutor).throttleLimit(4) // <-- I add this
.build();
但是,当我添加任务执行器时,我得到了这个:
java.lang.NullPointerException
at org.hibernate.internal.util.collections.IdentityMap.entryArray(IdentityMap.java:162)
at org.hibernate.internal.util.collections.IdentityMap.concurrentEntries(IdentityMap.java:58)
at org.hibernate.engine.internal.StatefulPersistenceContext.forEachCollectionEntry(StatefulPersistenceContext.java:1135)
at org.hibernate.event.internal.AbstractFlushingEventListener.prepareCollectionFlushes(AbstractFlushingEventListener.java:193)
at org.hibernate.event.internal.AbstractFlushingEventListener.flushEverythingToExecutions(AbstractFlushingEventListener.java:85)
看着IdentityMap
我可以看到这个:
(1) if ( entryArray == null )
(2) entryArray = new Map.Entry[ map.size() ];
final Iterator<Entry<IdentityKey<K>, V>> itr = map.entrySet().iterator();
int i = 0;
while ( itr.hasNext() )
final Entry<IdentityKey<K>, V> me = itr.next();
(3) entryArray[i++] = new IdentityMapEntry( me.getKey().key, me.getValue() ); // Here entryArray is NULL!!!
异常发生在第 (3) 行,其中entryArray
为空。而且我想知道它是如何做到的,因为entryArray
在第 (1) 行被检查为空并在第 (2) 行初始化。
任何想法将不胜感激。
【问题讨论】:
从您分享的内容来看,Spring Batch 方面没有任何问题。由于这个 NPE 发生在 Hibernate 的代码中,我会让 hibernate 专家帮助你。 @MahmoudBenHassine 你能指导我如何根据 Christian 的建议编写一个每个线程都有一个单独的休眠会话的阅读器 我认为我们不需要这样做。项目阅读器不应该关心线程是如何分配的。它是驱动进程并决定读卡器应该由单个线程还是多个线程调用的步骤。如果项目阅读器不是线程安全的并且应该在多线程步骤中使用,那么它可以用SynchronizedIteamStreamReader
装饰。
也就是说,从RepositoryItemReader
的Javadoc 中,我看到:This implementation is thread-safe between calls to open(ExecutionContext), but remember to use saveState=false if used in a multi-threaded client (no restart available)
。您是否尝试过设置 saveState=false?我之所以问,是因为我无法从您的部分堆栈跟踪中看到在处理执行上下文时是否发生异常。
嗨@MahmoudBenHassine 确实我没有设置saveState=false。现在我添加了它,我在另一个地方得到了另一个空指针异常:gist.github.com/ptahchiev/b63d909fc8bb048d95df1c7376be43fe
【参考方案1】:
我不知道您的阅读器、处理器和编写器是什么样的,但这看起来像是一个并发问题,因为在线程之间共享 Session
。确保每个线程都有单独的会话。
查看文档:https://docs.spring.io/spring-batch/docs/current/reference/html/scalability.html
对于一些常见的批处理用例,使用多线程 Step 实现存在一些实际限制。 Step 中的许多参与者(例如 reader 和 writer)都是有状态的。如果状态不是按线程隔离的,那么这些组件在多线程步骤中不可用。特别是 Spring Batch 中的大多数现成的 reader 和 writer 都不是为多线程使用而设计的。
最好为所有对象创建一个包装器,将它们各自的读取器/写入器等存储在本地线程中。
Java/Jakarta Batch 做到了这一点。读取器、写入器、处理器实例始终位于每个分区,因此您永远不会遇到问题。
【讨论】:
嗨@christian-beikov 谢谢你的回复。我正在使用春季批次的标准RepositoryItemReader
。您知道如何确保每个线程都有单独的会话吗?
@Christian Beikov Java/Jakarta Batch gets this right. Reader, Writer, Processor instances are always per chunk, so you never run into issues.
是什么意思?这是否意味着如果有 10000 个块,就会创建 10000 个读取器/写入器/处理器实例?从框架 PoV 中,我们不知道会有多少块(除非您要求用户以某种方式提供项目的总数),这些实例也是在运行时动态创建并手动分配到单独的线程?我很想知道这是如何实现的以及它将如何解决问题。
Java/Jakarta Batch 中的实例只是“注入”的,使用哪个实例取决于 bean 的范围。默认范围是“依赖”,我认为默认情况下会导致每个分区都有一个实例。因此,对于每个线程,都会有一个单独的实例,除非您使用例如例如@ApplicationScoped
.
感谢您的反馈。我希望你同意我的观点,So for every thread, there will be a separate instance
与 instances are always per chunk
非常不同。每个线程的实例比每个块的实例更有意义,所以我认为值得更新答案,因为它目前具有误导性。每个线程都有一个读取器/处理器/写入器实例类似于 Spring Batch 中的 StepScope
d bean。
不确定 StepScoped 是否相同。就像我写的那样,在 Jakarta Batch 中,默认是分区范围的,即对于每个线程/分区,您都会获得一个单独的 bean 实例。我将答案更新为“每个分区”。以上是关于多线程jpa读取时休眠空指针异常的主要内容,如果未能解决你的问题,请参考以下文章
Hibernate/JPA @OneToOne 返回空指针异常
记录一些遇见的bug——记录一个使用多线程异步调用openfeign时子线程丢失request请求头导致的空指针异常错误