Spring boot jpa querydsl 多数据源使用入坑

背景

  • 随着同类项目越来越多,从产品、架构角度考虑,需要把重复的、基础的东西抽离出来,用以复用
  • 用户并没有达到指数级增长,从部署的角度考虑,暂时不需要做微服务
  • 因此,目前只做数据库、工程的拆分,在部署层面仍然使用单一服务形式。
  • 在以上形式下,就需要配置多数据源,下面是入坑指南

入坑过程

项目单数据源初始状态

项目的基础包

  • spring boot
  • spring data jpa
  • querydsl
  • mysql8.0

    org.springframework.boot
    spring-boot-starter-web



    org.springframework.boot
    spring-boot-starter-data-jpa



    mysql
    mysql-connector-java



    com.querydsl
    querydsl-jpa
    ${querydsl.version}


    com.querydsl
    querydsl-apt
    ${querydsl.version}
    provided


    org.projectlombok
    lombok
    true

初步的想法和说明

  • 项目module怎么搭根据你们自己的要求搭建,但是请注意Spring习惯使用packageScan,为了后续搭建方便,最好对多模块进行分包
  • 对多数据源的配置和使用,一条基本原则就是多数据源配置尽可能的与业务开发无关,最好开发无感,需要事务的时候和原来一样使用@Transactional
  • 原先想要拆分的Entity之间通过@ManyToOne、@OneToMany等JPA的标准接口做的关联关系,我们希望:
    • 只做不同数据源的JPA的packageScan,让对应的JPA自动建立到对应的库中
    • 底层可以支持自动识别并进行跨库查询,当我使用由关联关系的Entity时
  • 如果底层支持第二条,我们再考虑事务和Session

配置多数据源

简单的数据源配置

如何配置多数据源

Configure Two DataSources

If you need to configure multiple data sources, you can apply the same tricks that are described in the previous section. You must, however, mark one of the DataSource instances as @Primary, because various auto-configurations down the road expect to be able to get one by type.
If you create your own DataSource, the auto-configuration backs off. In the following example, we provide the exact same feature set as the auto-configuration provides on the primary data source:

@Bean
@Primary
@ConfigurationProperties("app.datasource.first")
public DataSourceProperties firstDataSourceProperties() {
    return new DataSourceProperties();
}

@Bean
@Primary
@ConfigurationProperties("app.datasource.first.configuration")
public HikariDataSource firstDataSource() {
    return firstDataSourceProperties().initializeDataSourceBuilder().type(HikariDataSource.class).build();
}

// u can build the second datasource as same as shown above
@Bean
@ConfigurationProperties("app.datasource.second")
public BasicDataSource secondDataSource() {
    return DataSourceBuilder.create().type(BasicDataSource.class).build();
}
  • 当你使用多数据源配置datasources时,注意设置@Primary
  • 推荐使用DataSourceProperties方式,type推荐HikariDataSource,这个是Spring单数据源的默认类型

Use Two EntityManagers

Even if the default EntityManagerFactory works fine, you need to define a new one, otherwise the presence of the second bean of that type switches off the default. You can use the EntityManagerBuilder provided by Spring Boot to help you to create one. Alternatively, you can use the LocalContainerEntityManagerFactoryBean directly from Spring ORM, as shown in the following example:

@Bean
public LocalContainerEntityManagerFactoryBean customerEntityManagerFactory(EntityManagerFactoryBuilder builder) {
    return builder
            .dataSource(customerDataSource())
            .packages(Customer.class)
            .persistenceUnit("customers")
            .build();
}

@Bean
public LocalContainerEntityManagerFactoryBean orderEntityManagerFactory(EntityManagerFactoryBuilder builder) {
    return builder
            .dataSource(orderDataSource())
            .packages(Order.class)
            .persistenceUnit("orders")
            .build();
}
  • 一旦你开启多数据源配置,自定义EntityManagerFactory无法避免
  • 你有两种方式创建它,EntityManagerBuilder或者直接new LocalContainerEntityManagerFactoryBean
  • 注意!!!!!!!!!!!!!!下面的说明如果你没有看将会有很多坑,后面会一点一点补完。大致的意思就是说最好用Spring自己的EntityManagerFactoryBuilder去生成LocalContainerEntityManagerFactoryBean,否则后果自负

When you create a bean for LocalContainerEntityManagerFactoryBean yourself, any customization that was applied during the creation of the auto-configured LocalContainerEntityManagerFactoryBean is lost. For example, in case of Hibernate, any properties under the spring.jpa.hibernate prefix will not be automatically applied to your LocalContainerEntityManagerFactoryBean. If you were relying on these properties for configuring things like the naming strategy or the DDL mode, you will need to explicitly configure that when creating the LocalContainerEntityManagerFactoryBean bean. On the other hand, properties that get applied to the auto-configured EntityManagerFactoryBuilder, which are specified via spring.jpa.properties, will automatically be applied, provided you use the auto-configured EntityManagerFactoryBuilder to build the LocalContainerEntityManagerFactoryBean bean.

  • 注:这里的代码用的bean name和上面db config bean没有关系,只是个例子,读者自行修改以适应自己的项目,下同,本质上就是配置两个数据源,最后会给出完整配置和源代码github地址,先了解大概即可

源码分析

EntityManagerFactoryBuilder
  • 位置:spring-boot-autoconfigure-2.1.4.RELEASE.jar
  • JpaBaseConfiguration
    • 注意@EnableConfigurationProperties(JpaProperties.class),实例化属性取决于JpaProperties.class配置的spring.jpa,稍后看ymal文件即可
    • @ConditionalOnMissingBean表明如果我们自己不实例化EntityManagerFactoryBuilder,则Spring会帮我们构造一个
package org.springframework.boot.autoconfigure.orm.jpa;
@Configuration
@EnableConfigurationProperties(JpaProperties.class)
@Import(DataSourceInitializedPublisher.Registrar.class)
public abstract class JpaBaseConfiguration implements BeanFactoryAware {

    private final DataSource dataSource;

    private final JpaProperties properties;

    private final JtaTransactionManager jtaTransactionManager;

    private final TransactionManagerCustomizers transactionManagerCustomizers;

    private ConfigurableListableBeanFactory beanFactory;

    protected JpaBaseConfiguration(DataSource dataSource, JpaProperties properties,
            ObjectProvider jtaTransactionManager,
            ObjectProvider transactionManagerCustomizers) {
        this.dataSource = dataSource;
        this.properties = properties;
        this.jtaTransactionManager = jtaTransactionManager.getIfAvailable();
        this.transactionManagerCustomizers = transactionManagerCustomizers
                .getIfAvailable();
    }

    @Bean
    @ConditionalOnMissingBean
    public PlatformTransactionManager transactionManager() {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        if (this.transactionManagerCustomizers != null) {
            this.transactionManagerCustomizers.customize(transactionManager);
        }
        return transactionManager;
    }

    @Bean
    @ConditionalOnMissingBean
    public JpaVendorAdapter jpaVendorAdapter() {
        AbstractJpaVendorAdapter adapter = createJpaVendorAdapter();
        adapter.setShowSql(this.properties.isShowSql());
        adapter.setDatabase(this.properties.determineDatabase(this.dataSource));
        adapter.setDatabasePlatform(this.properties.getDatabasePlatform());
        adapter.setGenerateDdl(this.properties.isGenerateDdl());
        return adapter;
    }

    @Bean
    @ConditionalOnMissingBean
    public EntityManagerFactoryBuilder entityManagerFactoryBuilder(
            JpaVendorAdapter jpaVendorAdapter,
            ObjectProvider persistenceUnitManager,
            ObjectProvider customizers) {
        EntityManagerFactoryBuilder builder = new EntityManagerFactoryBuilder(
                jpaVendorAdapter, this.properties.getProperties(),
                persistenceUnitManager.getIfAvailable());
        customizers.orderedStream()
                .forEach((customizer) -> customizer.customize(builder));
        return builder;
    }

    @Bean
    @Primary
    @ConditionalOnMissingBean({ LocalContainerEntityManagerFactoryBean.class,
            EntityManagerFactory.class })
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(
            EntityManagerFactoryBuilder factoryBuilder) {
        Map vendorProperties = getVendorProperties();
        customizeVendorProperties(vendorProperties);
        return factoryBuilder.dataSource(this.dataSource).packages(getPackagesToScan())
                .properties(vendorProperties).mappingResources(getMappingResources())
                .jta(isJta()).build();
    }
}
HibernateJpaConfiguration
  • JpaBaseConfiguration的默认实现
  • 注意@ConditionalOnSingleCandidate(DataSource.class),因为我们有多个DataSource实例,所以它会失效,也就是说hibernate的默认配置无法生效
  • @EnableConfigurationProperties(HibernateProperties.class),表明spring.jpa.hibernate也无效
@Configuration
@EnableConfigurationProperties(HibernateProperties.class)
@ConditionalOnSingleCandidate(DataSource.class)
class HibernateJpaConfiguration extends JpaBaseConfiguration {...}
自动装配
  • org.springframework.boot.autoconfigure.orm.jpa.JpaRepositoriesAutoConfiguration
  • org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration
  • org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfigureRegistrar

PlatformTransactionManager

The configuration above almost works on its own. To complete the picture, you need to configure TransactionManagers for the two EntityManagers as well. If you mark one of them as @Primary, it could be picked up by the default JpaTransactionManager in Spring Boot. The other would have to be explicitly injected into a new instance. Alternatively, you might be able to use a JTA transaction manager that spans both.

@Bean(name = Constant.TRANSACTION_MANAGER_PROJECT)
@Primary
public JpaTransactionManager transactionManager() {
    JpaTransactionManager jpaTransactionManager = new JpaTransactionManager();
    jpaTransactionManager.setEntityManagerFactory(entityManagerFactoryBean().getObject());
    return jpaTransactionManager;
}

@Bean(name = Constant.TRANSACTION_MANAGER_EVENT)
public JpaTransactionManager transactionManager() {
    JpaTransactionManager jpaTransactionManager = new JpaTransactionManager();
    jpaTransactionManager.setEntityManagerFactory(entityManagerFactoryBean().getObject());
    return jpaTransactionManager;
}
  • spring没有给demo,这是我自己加的,差不多就是这个样子

EntityManager & JPAQueryFactory

@Bean(name = Constant.ENTITY_MANAGER_PROJECT)
@Primary
public EntityManager entityManager() {
    return entityManagerFactoryBean().getObject().createEntityManager();
}

@Bean(name = Constant.JPA_QUERY_FACTORY_PROJECT)
@Primary
public JPAQueryFactory jpaQueryFactory() {
    return new JPAQueryFactory(entityManager());
}

@Bean(name = Constant.ENTITY_MANAGER_EVENT)
public EntityManager entityManager() {
    return entityManagerFactoryBean().getObject().createEntityManager();
}

@Bean(name = Constant.JPA_QUERY_FACTORY_EVENT)
public JPAQueryFactory jpaQueryFactory() {
    return new JPAQueryFactory(entityManager());
}
  • 因为我们项目用到了querydsl,所以需要JPAQueryFactory

@EnableJpaRepositories

If you use Spring Data, you need to configure @EnableJpaRepositories accordingly, as shown in the following example

@Configuration
@EnableJpaRepositories(
        basePackageClasses = {ProjectRepository.class},
        entityManagerFactoryRef = Constant.ENTITY_MANAGER_FACTORY_PROJECT,
        transactionManagerRef = Constant.TRANSACTION_MANAGER_PROJECT)
public class JpaConfigurationProject {
    ...
}

@Configuration
@EnableJpaRepositories(
        basePackageClasses = {EventRepository.class},
        entityManagerFactoryRef = Constant.ENTITY_MANAGER_FACTORY_EVENT,
        transactionManagerRef = Constant.TRANSACTION_MANAGER_EVENT)
public class JpaConfigurationEvent {
    ...
}
  • 你也可以使用basePackages配置扫描

Initialize a Database Using JPA

JPA has features for DDL generation, and these can be set up to run on startup against the database. This is controlled through two external properties:

  1. spring.jpa.generate-ddl (boolean) switches the feature on and off and is vendor independent.
  2. spring.jpa.hibernate.ddl-auto (enum) is a Hibernate feature that controls the behavior in a more fine-grained way. This feature is described in more detail later in this guide.

两种方式都可以,第一种更通用,第二种具体到hibernate,粒度更细

跨服务、数据库应该通过服务接口实现,而不是DB

  • 配置完成后,启动服务报错:@OneToOne or @ManyToOne on xxx.xxx.currency references an unknown entity
  • 这个是stackoverflow的讨论
    • https://stackoverflow.com/questions/21133131/error-using-onetoone-manytoone-with-jpa-and-multiple-data-sources
    • 核心的意思就是:No. You cannot have entity x in database x and entity y in database y and then expect entity x and y to work together. They work separately but you cannot have relations between entity x and entity y
  • 很显然,跨数据源的操作,需要通过服务接口实现是正统

移除@OneToOne、@ManyToOne

  • 实体关系依赖改为ID依赖

默认hibernate配置失效

See HibernateJpaAutoConfiguration and JpaBaseConfiguration for more details.

方式1(Spring推荐)

  • Hibernate uses two different naming strategies to map names from the object model to the corresponding database names. The fully qualified class name of the physical and the implicit strategy implementations can be configured by setting the spring.jpa.hibernate.naming.physical-strategy and spring.jpa.hibernate.naming.implicit-strategy properties, respectively. Alternatively, if ImplicitNamingStrategy or PhysicalNamingStrategy beans are available in the application context, Hibernate will be automatically configured to use them.
  • By default, Spring Boot configures the physical naming strategy with SpringPhysicalNamingStrategy. This implementation provides the same table structure as Hibernate 4: all dots are replaced by underscores and camel casing is replaced by underscores as well. Additionally, by default, all table names are generated in lower case. For example, a TelephoneNumber entity is mapped to the telephone_number table. If your schema requires mixed-case identifiers, define a custom SpringPhysicalNamingStrategy bean, as shown in the following example:
@Bean
SpringPhysicalNamingStrategy caseSensitivePhysicalNamingStrategy() {
    return new SpringPhysicalNamingStrategy() {

        @Override
        protected boolean isCaseInsensitive(JdbcEnvironment jdbcEnvironment) {
            return false;
        }

    };
}

@Bean
public PhysicalNamingStrategy physicalNamingStrategy() {
    return new PhysicalNamingStrategyStandardImpl();
}

方式2(Spring推荐)

spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl

方式3(自定义HibernateConfiguration)

  • 上面源码时也说过,HibernateJpaConfiguration只对单数据源生效,我们直接模仿它写一个实现类给LocalContainerEntityManagerFactoryBean配置即可
package cc.gegee.pangolin.config.atomikos;

import lombok.val;
import org.hibernate.boot.model.naming.ImplicitNamingStrategy;
import org.hibernate.boot.model.naming.PhysicalNamingStrategy;
import org.hibernate.cfg.AvailableSettings;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateProperties;
import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateSettings;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
import org.springframework.boot.jdbc.EmbeddedDatabaseConnection;
import org.springframework.boot.jdbc.SchemaManagement;
import org.springframework.boot.jdbc.SchemaManagementProvider;
import org.springframework.context.annotation.Configuration;
import org.springframework.orm.hibernate5.SpringBeanContainer;
import org.springframework.util.ClassUtils;

import javax.sql.DataSource;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

/**
 * copy from org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaConfiguration
 * used for default hibernate setting
 */
@Configuration
public class HibernateConfiguration {

    private final ObjectProvider providers;

    private final HibernateProperties hibernateProperties;

    private final JpaProperties properties;

    private final ObjectProvider physicalNamingStrategy;

    private final ObjectProvider implicitNamingStrategy;

    private final ConfigurableListableBeanFactory beanFactory;

    private final ObjectProvider hibernatePropertiesCustomizers;

    public HibernateConfiguration(ObjectProvider providers, HibernateProperties hibernateProperties, JpaProperties properties, ObjectProvider physicalNamingStrategy, ObjectProvider implicitNamingStrategy, ConfigurableListableBeanFactory beanFactory, ObjectProvider hibernatePropertiesCustomizers) {
        this.providers = providers;
        this.hibernateProperties = hibernateProperties;
        this.properties = properties;
        this.physicalNamingStrategy = physicalNamingStrategy;
        this.implicitNamingStrategy = implicitNamingStrategy;
        this.beanFactory = beanFactory;
        this.hibernatePropertiesCustomizers = hibernatePropertiesCustomizers;
    }

    /**
     * 获取配置文件信息
     */
    public Map getVendorProperties(DataSource dataSource) {
        List hibernatePropertiesCustomizers = determineHibernatePropertiesCustomizers(
                physicalNamingStrategy.getIfAvailable(),
                implicitNamingStrategy.getIfAvailable(), beanFactory,
                this.hibernatePropertiesCustomizers.orderedStream()
                        .collect(Collectors.toList()));
        Supplier defaultDdlMode = () -> new HibernateDefaultDdlAutoProvider(providers)
                .getDefaultDdlAuto(dataSource);
        val vendorProperties = new LinkedHashMap<>(this.hibernateProperties.determineHibernateProperties(
                properties.getProperties(),
                new HibernateSettings().ddlAuto(defaultDdlMode)
                        .hibernatePropertiesCustomizers(
                                hibernatePropertiesCustomizers)));
        // DdlTransactionIsolatorJtaImpl could not locate TransactionManager to suspend any current transaction; base JtaPlatform impl
        vendorProperties.put("hibernate.transaction.jta.platform", AtomikosJtaPlatform.class.getName());
        // i can not find any reason to set this value, if any question happen, try to set it
        vendorProperties.put("javax.persistence.transactionType", "JTA");
//        vendorProperties.put("javax.persistence.cache.storeMode", CacheStoreMode.BYPASS);
        return vendorProperties;
    }

    /**
     * 命名策略自动判断
     */
    private List determineHibernatePropertiesCustomizers(
            PhysicalNamingStrategy physicalNamingStrategy,
            ImplicitNamingStrategy implicitNamingStrategy,
            ConfigurableListableBeanFactory beanFactory,
            List hibernatePropertiesCustomizers) {
        List customizers = new ArrayList<>();
        if (ClassUtils.isPresent(
                "org.hibernate.resource.beans.container.spi.BeanContainer",
                getClass().getClassLoader())) {
            customizers
                    .add((properties) -> properties.put(AvailableSettings.BEAN_CONTAINER,
                            new SpringBeanContainer(beanFactory)));
        }
        if (physicalNamingStrategy != null || implicitNamingStrategy != null) {
            customizers.add(new NamingStrategiesHibernatePropertiesCustomizer(
                    physicalNamingStrategy, implicitNamingStrategy));
        }
        customizers.addAll(hibernatePropertiesCustomizers);
        return customizers;
    }

    /**
     * 自动进行建表操作
     */
    static class HibernateDefaultDdlAutoProvider implements SchemaManagementProvider {

        private final Iterable providers;

        HibernateDefaultDdlAutoProvider(Iterable providers) {
            this.providers = providers;
        }

        public String getDefaultDdlAuto(DataSource dataSource) {
            if (!EmbeddedDatabaseConnection.isEmbedded(dataSource)) {
                return "none";
            }
            SchemaManagement schemaManagement = getSchemaManagement(dataSource);
            if (SchemaManagement.MANAGED.equals(schemaManagement)) {
                return "none";
            }
            return "create-drop";

        }

        @Override
        public SchemaManagement getSchemaManagement(DataSource dataSource) {
            return StreamSupport.stream(this.providers.spliterator(), false)
                    .map((provider) -> provider.getSchemaManagement(dataSource))
                    .filter(SchemaManagement.MANAGED::equals).findFirst()
                    .orElse(SchemaManagement.UNMANAGED);
        }

    }

    private static class NamingStrategiesHibernatePropertiesCustomizer
            implements HibernatePropertiesCustomizer {

        private final PhysicalNamingStrategy physicalNamingStrategy;

        private final ImplicitNamingStrategy implicitNamingStrategy;

        NamingStrategiesHibernatePropertiesCustomizer(PhysicalNamingStrategy physicalNamingStrategy, ImplicitNamingStrategy implicitNamingStrategy) {
            this.physicalNamingStrategy = physicalNamingStrategy;
            this.implicitNamingStrategy = implicitNamingStrategy;
        }

        /**
         * 数据库命名映射策略
         *
         * @param hibernateProperties the JPA vendor properties to customize
         */
        @Override
        public void customize(Map hibernateProperties) {
            if (this.physicalNamingStrategy != null) {
                hibernateProperties.put("hibernate.physical_naming_strategy", this.physicalNamingStrategy);
            }
            if (this.implicitNamingStrategy != null) {
                hibernateProperties.put("hibernate.implicit_naming_strategy", this.implicitNamingStrategy);
            }
        }
    }
}

  • 最后在配置LocalContainerEntityManagerFactoryBean的地方setProperties即可

多数据源事务

多数据源事务场景及其问题

  • 只涉及单库单服务事务,没问题
  • 跨库跨服务事务,有问题,当内部服务调用成功之后,而当前服务随后抛出异常,则内部服务(不同PlatformTransactionManager)不会回滚,因为是两个事务(借助数据库实现的)

解决方案

  • 弱一致性
    • 消息队列
  • 强一致性
    • XA协议,两阶段提交协议2PC、三阶段提交协议3PC是对2的优化
  • 最终一致性
    • 事务补偿(TCC)

选择哪个方案

  • 选择的几个维度:一致性强度、吞吐量、实现复杂度、架构搭建后开发的复杂度
  • 我们项目的现状:用户不多、基本都是数据库的操作、不希望开发为此额外做很多开发
  • 这个帖子我觉得说的蛮有道理的:https://www.atomikos.com/Blog/ToXAOrNotToXA
  • 所以选择XA协议

分布式事务XA协议

  • spring官方推荐XA实现框架:https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-jta
  • 选择XA实现Atomikos框架
  • 关于XA协议,网上有很多资料了,关于它的二阶段提交简单来说:
    • 从模型上讲:一个事务管理器,多个资源管理器
    • 所谓二阶段:第一阶段,多个资源管理器只执行不提交。第二阶段,如果多个资源管理器都执行成功,分别commit,否则rollback

使用atomikos对原项目改造


    org.springframework.boot
    spring-boot-starter-jta-atomikos

use atomikos to impl AbstractJtaPlatform

import com.atomikos.icatch.jta.UserTransactionManager;
import org.hibernate.engine.transaction.jta.platform.internal.AbstractJtaPlatform;

import javax.transaction.TransactionManager;
import javax.transaction.UserTransaction;

public class AtomikosJtaPlatform extends AbstractJtaPlatform {

    private static final long serialVersionUID = 1L;
    private UserTransactionManager utm;

    public AtomikosJtaPlatform() {
        utm = new UserTransactionManager();
    }

    @Override
    protected TransactionManager locateTransactionManager() {
        return utm;
    }

    @Override
    protected UserTransaction locateUserTransaction() {
        return utm;
    }
}

datasource

private AtomikosDataSourceBean buildAtomikosDataSourceBean(DataSourceProperties dataSourceProperties, String resourceName) throws SQLException {
    val xaDataSource = dataSourceProperties.initializeDataSourceBuilder()
            .type(MysqlXADataSource.class).build();
    xaDataSource.setPinGlobalTxToPhysicalConnection(true);
    val atomikosDataSourceBean = new AtomikosDataSourceBean();
    atomikosDataSourceBean.setXaDataSource(xaDataSource);
    atomikosDataSourceBean.setUniqueResourceName(resourceName);
    atomikosDataSourceBean.setPoolSize(10);
    return atomikosDataSourceBean;
}
  • MysqlXADataSource:主流数据库都支持XA协议,我用的是mysql,只要实现javax.sql.XADataSource即可
  • PinGlobalTxToPhysicalConnection:这个要打开否则无法使用jta事务

一个jta事务管理器

import cc.gegee.common.jpa.Constant;
import com.atomikos.icatch.jta.UserTransactionImp;
import com.atomikos.icatch.jta.UserTransactionManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.transaction.jta.JtaTransactionManager;

import javax.transaction.UserTransaction;

@Configuration
public class JtaTransactionManagerConfig {

    @Primary
    @Bean(name = Constant.TRANSACTION_MANAGER_JTA)
    public JtaTransactionManager regTransactionManager () {
        UserTransactionManager userTransactionManager = new UserTransactionManager();
        UserTransaction userTransaction = new UserTransactionImp();
        return new JtaTransactionManager(userTransaction, userTransactionManager);
    }
}

修改原先的配置(另一个类同)

  • @EnableJpaRepositoriestransactionManagerRef指向JtaTransactionManager
  • LocalContainerEntityManagerFactoryBean打开JTA
import cc.gegee.common.jpa.Constant;
import com.querydsl.jpa.impl.JPAQueryFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.SharedEntityManagerCreator;

import javax.persistence.EntityManager;
import javax.sql.DataSource;
import java.util.Objects;

@Configuration
@EnableJpaRepositories(
        basePackages = {
                Constant.PACKAGES_1
                , Constant.PACKAGES_2
                , Constant.PACKAGES_3
                , Constant.PACKAGES_4
        },
        entityManagerFactoryRef = Constant.ENTITY_MANAGER_FACTORY_1,
        transactionManagerRef = Constant.TRANSACTION_MANAGER_JTA
)
public class JpaConfigurationPangolin {

    private final DataSource dataSource;

    private final EntityManagerFactoryBuilder builder;

    private final HibernateConfiguration hibernateConfiguration;

    public JpaConfigurationPangolin(@Qualifier(Constant.DATA_SOURCE_1) DataSource dataSource, EntityManagerFactoryBuilder builder, HibernateConfiguration hibernateConfiguration) {
        this.dataSource = dataSource;
        this.builder = builder;
        this.hibernateConfiguration = hibernateConfiguration;
    }

    @Bean(name = Constant.ENTITY_MANAGER_FACTORY_1)
    @Primary
    public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean() {
        return builder
                .dataSource(dataSource)
                .properties(hibernateConfiguration.getVendorProperties(dataSource))
                .packages(
                        Constant.PACKAGES_1
                        , Constant.PACKAGES_2
                        , Constant.PACKAGES_3
                        , Constant.PACKAGES_4
                )
                .persistenceUnit(Constant.PERSISTENCE_UNIT_1)
                .jta(true)
                .build();
    }

    @Bean(name = Constant.ENTITY_MANAGER_1)
    @Primary
    public EntityManager entityManager() {
        // return SharedEntityManagerCreator.createSharedEntityManager(Objects.requireNonNull(entityManagerFactoryBean().getObject()));
        return Objects.requireNonNull(entityManagerFactoryBean().getObject()).createEntityManager();
    }

    @Bean(name = Constant.JPA_QUERY_FACTORY_1)
    @Primary
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(entityManager());
    }
}

使用

  • 和原来没什么区别:在service上配置@Transactional(rollbackFor = Exception.class)即可

Hibernate Session的管理问题

  • OK,到此多数据源的配置完结了,以为可以放松一段一时间了,谁知测试发现一个奇怪的现象,就是恰巧某个功能用JPA做了修改之后,使用了querydsl做了查询,将结果返回。但是查询来的值是修改之前的。此外,其他使用querydsl查询的地方永远不再更新修改后的查询结果。

一条弯路

  • 经过初步的测试排查就是querydsl和JPA有'冲突'
  • 再经过debug,每次请求JPAQueryFactory使用的session都是同一个,并且其他请求也是同一个
  • 我们知道hibernate默认开启的是一级缓存(session),由于和JPA使用的是不同的两个session,JPAQueryFactory的session一直是同一个,而JPA执行完后又没有通知JPAQueryFactory的session,导致一直查询的是历史值
  • 开始以为是JPAQueryFactory的session机制是有缓存了不会查询导致,于是打开mysql的日志系统
## 局域网服务器
ssh [email protected]
## 用的docker
docker ps
## 进入docker 容器
docker exec -it mysql8.0 /bin/bash
## 搜索my.cnf文件位置
mysql --help --verbose | grep my.cnf
vim my.cnf
## 开启log
在 my.cnf 设置 general_log = 1
## 查看
mysql -uroot -p"pwd"
show variables where variable_name like "%general_log%";
## 监视日志
tail -f xxxx.log
  • 发现有查询,但是不知道为什么,结果不更新,且仍然从缓存取值

另一角度

  • 以前单数据源是怎样的?
  • 测试了下单数据源时,同时使用JPA和querydsl,没有问题,并且使用的是同一个session
  • 单数据源和多数据源querydsl的差别仅在于EntityManager是自己创建的,这里是关键

寻找Spring是如何实例化EntityManager

  • debug JPAQueryFactory实例化时的EntityManager,发现描述为:Shared EntityManager proxy for target factory [org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean@3937fd51]
  • 我们找到org.springframework.orm.jpa.SharedEntityManagerCreator,定位到它有一个关键属性synchronizedWithTransaction,最终通过SharedEntityManagerInvocationHandler动态代理实例化EntityManager,增强EntityManager
  • 我们模拟Spring单数据源创建EntityManager,如下:
@Bean(name = Constant.ENTITY_MANAGER_1)
@Primary
public EntityManager entityManager() {
    return SharedEntityManagerCreator.createSharedEntityManager(Objects.requireNonNull(entityManagerFactoryBean().getObject()));
}

后记

  • 至此我们完成了多数据源的所有配置
  • 实际上,Spring官网提供了多数据源的使用的例子,但是是没有考虑事务、session管理的问题的,或者说是分开考虑
  • EntityManager线程安全性:JPA/Hibernate Persistence Context

resources

  • GitHub - todo

reference

  • SpringBoot2.x Data JPA 多数据源爬坑
  • Spring Boot 配置 : HibernateJpaConfiguration
  • Spring事务传播机制与分布式事务解决方案
  • 嵌套事务、挂起事务,Spring 事务机制有什么奥秘?
  • 分布式事务管理之JTA与链式事务
  • 还不理解“分布式事务”?这篇给你讲清楚
  • Docker下MySQL数据库日志的打开方法
  • Distributed transactions in Spring, with and without XA
  • Spring的分布式事务实现-使用和不使用XA(翻译)
  • REST微服务的分布式事务实现-分布式系统、事务以及JTA介绍
  • 微服务分布式事务4种解决方案实战

你可能感兴趣的:(Spring boot jpa querydsl 多数据源使用入坑)