如何在Spring Data JPA + Hibernate + PostgresSQL中禁用L1会话缓存?

5

我有以下的 Spring Data JPA Repository

public interface FooRepository extends JpaRepository<Foo, String> {

  @QueryHints(
      value = {
        @QueryHint(name = HINT_FETCH_SIZE, value = "1000"),
        @QueryHint(name = HINT_CACHEABLE, value = "false"),
        @QueryHint(name = HINT_FLUSH_MODE, value = "ALWAYS"),
        @QueryHint(name = HINT_CACHE_MODE, value = "IGNORE"),
        @QueryHint(name = HINT_READONLY, value = "true")
      })
  Stream<Foo> findAll();
}

以下方法中被称为如下所示

@Transactional
public void doSomething() {
  AtomicInteger counter = new AtomicInteger();

  try(Stream<Foo> stream = fooRepository.findAll()) {
    stream.forEach(foo -> {
      int i = counter.incrementAndGet();
      logger.info(() -> "" + i);
    });
  }
}

当运行这段代码时,有数百万个Foo实体,此代码会抛出OutOfMemoryError。在崩溃后查看堆转储时,我发现有大量的MutableEntityEntry、Foo和EntityEntryContext$ManagedEntityImpl。所有三者都具有完全相同的计数。除此之外,还有正好是该计数两倍的EntityKey。例如,在堆转储中,我有40k个前3个实体和80k个EntityKey。
为了使其工作,我尝试手动刷新、清除和垃圾回收,但没有成功。
@Transactional // org.springframework.transaction.annotation.Transactional
public void doSomething() {
  entityManager.joinTransaction(); // properly injected through Spring DI
  AtomicInteger counter = new AtomicInteger();

  try(Stream<Foo> stream = fooRepository.findAll()) {
    stream.forEach(foo -> {
      int i = counter.incrementAndGet();
      if (i % 100 == 0) {
        fooRepository.flush();
        entityManager.clear();
        System.gc();
        logger.info(() -> "flush, clear, gc");
      }
      logger.info(() -> "" + i);
    });
 }

由于在我的代码中没有保留任何流式传输到堆转储的foo实体的引用,一旦出现错误,我怀疑问题出在Hibernate的L1 Session缓存中,即使有一个QueryHint停用了缓存(据我理解)。感觉只有给定方法中的HINT_FETCH_SIZEQueryHints中起作用,但我不知道为什么。
顺便说一下,我的项目中根本没有使用Spring Boot。因此,在我的SpringConfiguration中有以下bean来配置Spring Data JPA:
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory()
    throws MalformedURLException {
  HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
  vendorAdapter.setDatabase(Database.POSTGRESQL);
  vendorAdapter.setGenerateDdl(false);

  LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
  factory.setJpaVendorAdapter(vendorAdapter);
  factory.setPackagesToScan(getClass().getPackage().getName());
  factory.setDataSource(dataSource());
  Properties jpaProperties = new Properties();
  jpaProperties.setProperty(
      "hibernate.physical_naming_strategy",
      "my.domain.hibernate.SnakeCasePhysicalNamingStrategy");
  jpaProperties.setProperty("hibernate.dialect", "org.hibernate.dialect.PostgreSQL10Dialect");
  factory.setJpaProperties(jpaProperties);

  return factory;
}

@Bean
public EntityManager entityManager() throws MalformedURLException {
  return entityManagerFactory().getObject().createEntityManager();
}

@Bean
public PlatformTransactionManager transactionManager() throws MalformedURLException {
  JpaTransactionManager txManager = new JpaTransactionManager();
  txManager.setEntityManagerFactory(entityManagerFactory().getObject());

  return txManager;
}

以下是每个版本的信息:
  • Spring 5.2.13.RELEASE
  • Spring Data JPA 2.3.7.RELEASE
  • Hibernate 5.4.28.Final
  • PostgreSQL 13.1(在Docker上使用alpine)

您无法禁用L1会话缓存,应考虑减少加载的数据量。 - SternK
你修改这些实体吗?如果不修改,Hibernate 有减少内存使用的方法。 - Guillaume
@Guillaume 我不会修改这些实体。真正的流程会创建新的实体(不同类型),以便稍后持久化(即在关闭此流之后)。 - Jeep87c
2个回答

2
我认为你应该尝试使用DTO projections而不是entities:因为你不会修改这些对象,所以Hibernate没有必要跟踪实体状态。 这篇文章解释了一些可能有帮助的策略。
Hibernate还有无状态会话的概念,但我没有尝试过,而且我认为它没有通过JPA API公开。

多亏了您的建议,我们的代码在执行时间方面提高了67.5%,内存消耗也减少了75%。 - Jeep87c

1

终于找到了问题所在,是我的类中注入entityManager的方式。不应该在SpringConfiguration中添加一个bean并通过构造函数注入它,而是必须在类中的字段声明上使用@PersistenceContext

这是可行的代码:

@PersistenceContext
private EntityManager entityManager;

[...]

@Transactional // org.springframework.transaction.annotation.Transactional
public void doSomething() {
  entityManager.joinTransaction();
  AtomicInteger counter = new AtomicInteger();

  try(Stream<Foo> stream = fooRepository.findAll()) {
    stream.forEach(foo -> {
      int i = counter.incrementAndGet();
      if (i % 100 == 0) {
        entityManager.flush();
        entityManager.clear();
        logger.info(() -> "flush then clear);
      }
      logger.info(() -> "" + i);
    });
 }

因此,执行entityManager.clear()将适当地清除L1会话缓存,如此处所述。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接