如何使用 Spring Data JPA + Hibernate + PostgresSQL 禁用 L1 会话缓存

Jee*_*87c 5 java postgresql hibernate jpa spring-data-jpa

我有以下 Spring Data JPA 存储库

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();
}
Run Code Online (Sandbox Code Playgroud)

在下面的方法中调用如下

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

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

当运行具有数百万个实体的代码时Foo,这个确切的代码会抛出一个OutOfMemoryError. 查看崩溃后的堆转储,我发现有大量的MutableEntityEntry,FooEntityEntryContext$ManagedEntityImpl。所有三个都有完全相同的计数。最重要的是, 的数量正好是该数量的两倍EntityKeyEntityKey例如,我在堆转储中前 3 个和 80k 各有 40k 。

为了完成这项工作,我尝试手动刷新、清除和垃圾收集,但没有成功,如下所示

@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);
    });
 }
Run Code Online (Sandbox Code Playgroud)

由于我的代码中没有保留任何foo对流式传输的实体的引用,并且在抛出错误后不会查找堆转储中的对象,因此我怀疑问题出在 Hibernate 的 L1 会话缓存中,即使存在停用缓存QueryHint(从我的理解)。感觉就像只HINT_FETCH_SIZE在我的方法给定的情况下工作QueryHints,我不知道为什么。

仅供参考,我在我的项目中根本没有使用 Spring Boot。所以我有以下 bean 来SpringConfiguration配置 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;
}
Run Code Online (Sandbox Code Playgroud)

这是每个的版本

  • 春季 5.2.13.RELEASE
  • Spring Data JPA 2.3.7.RELEASE
  • 休眠 5.4.28.Final
  • PostgreSQL 13.1(在 alpine 上使用 Docker)

Jee*_*87c 1

终于发现了问题,那就是entityManager在我的班级中注入的方式。您必须在类中的字段声明上使用,而不是在 my 中为其添加 beanSpringConfiguration并通过构造函数注入它。@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);
    });
 }
Run Code Online (Sandbox Code Playgroud)

这样做entityManager.clear()将正确清除 L1 会话缓存,如此处所述