首页 > 解决方案 > Spring Data JPA 附加 EntityManagerFactory 仅针对缓存和批量操作进行了优化

问题描述

我有一个遗留的 Spring Data JPA 应用程序,它有大量的实体和 CrudRepositories。JPA 是使用下面的 XML 配置的。我们有一个新要求,要求我们通过 FileUpload 一次将 10,000 - 50,000 个实体插入到数据库中。使用现有配置,数据库 CPU 会出现峰值。启用休眠统计信息后,很明显这 10,000 个插入操作生成了超过 200,000 个 DB 查询,因为InvoiceService.

原始配置

<bean id="dataSource" destroy-method="close" class="org.apache.commons.dbcp2.BasicDataSource">
    <property name="driverClassName" value="${db.driver}"/>
    <property name="url" value="${db.jdbcurl}"/>
    <property name="username" value="${db.username}"/>
    <property name="password" value="${db.password}"/>

    <property name="maxTotal" value="${db.maxTotal}"/>
    <property name="maxIdle" value="${db.maxIdle}"/>
    <property name="minIdle" value="${db.minIdle}"/>
    <property name="initialSize" value="${db.initialSize}"/>
    <property name="maxWaitMillis" value="${db.maxWaitMillis}"/>
    <property name="minEvictableIdleTimeMillis" value="${db.minEvictableIdleTimeMillis}"/>
    <property name="timeBetweenEvictionRunsMillis" value="${db.timeBetweenEvictionRunsMillis}"/>
    <property name="testOnBorrow" value="${db.testOnBorrow}"/>
    <property name="testOnReturn" value="${db.testOnReturn}"/>
    <property name="testWhileIdle" value="${db.testWhileIdle}"/>
    <property name="removeAbandonedOnBorrow" value="${db.removeAbandonedOnBorrow}"/>
    <property name="removeAbandonedOnMaintenance" value="${db.removeAbandonedOnMaintenance}"/>
    <property name="removeAbandonedTimeout" value="${db.removeAbandonedTimeout}"/>
    <property name="logAbandoned" value="${db.logAbandoned}"/>
</bean>

<bean id="entityManagerFactory"
    class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean" depends-on="flyway">
    <property name="dataSource" ref="dataSource" />
    <property name="packagesToScan" value="my.package.domain" />
    <property name="loadTimeWeaver">
        <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
    </property>
    <property name="jpaVendorAdapter">
        <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" />
    </property>
    <property name="jpaProperties">
        <props>
            <prop key="hibernate.hbm2ddl.auto">${hibernate.hbm2ddl.auto:validate}</prop>
            <prop key="hibernate.dialect">org.hibernate.dialect.PostgreSQL9Dialect</prop>
            <prop key="hibernate.show_sql">${hibernate.show_sql:false}</prop>
        </props>
    </property>
    <property name="persistenceUnitName" value="entityManagerFactory" />
</bean>

<bean id="persistenceAnnotationBeanPostProcessor" class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor">
    <property name="defaultPersistenceUnitName" value="entityManagerFactory"/>
</bean>

<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
    <property name="entityManagerFactory" ref="entityManagerFactory" />
</bean>

<tx:annotation-driven proxy-target-class="true" />

<bean id="persistenceExceptionTranslationPostProcessor"
    class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />

<jpa:repositories base-package="my.package.repository" entity-manager-factory-ref="entityManagerFactory"/>

FileUploadService 片段看起来像这样......

    EntityManager batchEntityManager = entityManagerFactory.createEntityManager();
    EntityTransaction transaction = batchEntityManager.getTransaction();
    transaction.begin();
    try (BufferedReader buffer = new BufferedReader(new InputStreamReader(file.getInputStream()))) {

        buffer.lines()
            .filter(StringUtils::isNotBlank)
            .forEach(csvLine -> {
                invoiceService.createInvoice(csvLine);
                if (counter.incrementAndGet() % (updateFrequency.equals(0) ? 1 : updateFrequency) == 0) {
                    FileUpload fileUpload1 = fileUploadRepository.findOne(fileUpload.getId());
                    fileUpload1.setNumberOfSentRecords(sentCount.get());
                    fileUploadRepository.save(fileUpload1);
                    transaction.commit();
                    transaction.begin();
                    batchEntityManager.clear();
                }
            });
          transaction.commit();
    } catch (IOException ex) {
        systemException.incrementAndGet();
        log.error("Unexpected error while performing send task.", ex);
        transaction.rollback();
    }

    // Update FileUpload status.
    FileUpload fileUpload1 = fileUploadRepository.findOne(fileUpload.getId());
    fileUpload1.setNumberOfSentRecords(sentCount.get());
    if (systemException.get() != 0) {
        fileUpload1.setStatus(FileUploadStatus.SYSTEM_ERROR);
    } else {
        fileUpload1.setStatus(FileUploadStatus.SENT);
    }
    fileUploadRepository.save(fileUpload1);
    batchEntityManager.close();

大多数数据库查询都是选择语句,它们为插入的每条记录返回相同的结果。很明显,启用 EhCache 作为二级缓存会显着提高性能。但是,在没有启用 ehcache 的情况下,这个应用程序已经在生产环境中完美运行了好几年。我犹豫是否要在全球范围内打开它,因为我不知道这将如何影响大量其他存储库/查询。

问题 1

有没有办法配置一个“备用”EntityManagerFactory,它只为这个批处理使用二级缓存?我怎样才能选择使用这个工厂而不是只用于这个批处理的主要工厂?

我尝试在我的 spring 配置中添加类似下面的内容。我可以轻松地将这个额外的 EntityManager 注入我的班级并使用它。但是,现有的存储库(例如 FileUploadRepository)似乎没有使用它——它们只是返回 null。我不确定这种方法是否可行。文档JpaTransactionManager说_ _

此事务管理器适用于使用单个 JPA EntityManagerFactory 进行事务数据访问的应用程序

这正是我没有做的。那么还有哪些其他选择呢?

<bean id="batchEntityManagerFactory"
    class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean" depends-on="flyway">
    <property name="dataSource" ref="dataSource" />
    <property name="packagesToScan" value="my.package.domain" />
    <property name="loadTimeWeaver">
        <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
    </property>
    <property name="jpaVendorAdapter">
        <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" />
    </property>
    <property name="jpaProperties">
        <props>
            <prop key="hibernate.hbm2ddl.auto">${hibernate.hbm2ddl.auto:validate}</prop>
            <prop key="hibernate.dialect">org.hibernate.dialect.PostgreSQL9Dialect</prop>
            <prop key="hibernate.show_sql">${hibernate.show_sql:false}</prop>
            <prop key="hibernate.generate_statistics">${hibernate.generate_statistics:false}</prop>
            <prop key="hibernate.ejb.interceptor">my.package.HibernateStatistics</prop>
            <prop key="hibernate.cache.use_query_cache">true</prop>
            <prop key="hibernate.cache.use_second_level_cache">true</prop>
            <prop key="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</prop>
            <prop key="hibernate.jdbc.batch_size">100</prop>
            <prop key="hibernate.order_inserts">true</prop>
            <prop key="hibernate.order_updates">true</prop>
        </props>
    </property>
    <property name="persistenceUnitName" value="entityManagerFactory" />
</bean>

问题2

假设没有“选择性”使用 EhCache 的其他选项,我尝试仅在主 EntityManagerFactory 上启用它。我们当然可以进行回归测试,以确保我们不会引入新问题。我认为这样做相当安全?然而,另一个问题出现了。我正在尝试按照本文中的描述分批提交插入,并在上面的代码中显示。由于Connection org.postgresql.jdbc.PgConnection@1e7eb804 is closed.. 我认为这是由于数据源上的maxWaitMillis属性。

我不想为应用程序中所有其他现有的 spring Service/Repository/query 更改此属性。如果我可以使用“自定义”EntityManagerFactory,我可以轻松地为不同的 DataSource Bean 提供我想要的属性。再次,这可能吗?

也许我看这个问题都错了。还有其他建议吗?

标签: javaspringhibernatespring-data-jpaehcache

解决方案


您可以拥有另一个EntityManagerFactory具有不同限定符的 bean,因此这是一种选择。我仍然建议您查看这些选择查询。我敢打赌,问题只是缺少索引,导致数据库进行全表扫描。如果您添加了正确的索引,您可能不需要更改应用程序中的任何内容。


推荐阅读