【问题标题】:Manage transactions with multiple datasource, entity managers for same application code管理具有多个数据源的事务,同一应用程序代码的实体管理器
【发布时间】:2018-07-11 18:12:55
【问题描述】:

我正在构建一个具有多个数据源、实体管理器、事务管理器和数据库的 Spring Boot 应用程序。每一个都是针对一个客户的,并且共享相同的 DAO、服务。

数据源之间的切换完美无缺。但我的交易有问题

这是我的配置:

package org.foo.config;

@Configuration
@EnableJpaRepositories(basePackages = "org.foo")
@EnableTransactionManagement
public class DataSourceConfiguration
{

@Value("#{'${load.datasources}'.split(',')}")
private List<String> toLoadDatasources;

@Value("${default.datasource}")
private String defaultDatasource;

@Bean
@ConfigurationProperties("spring.jpa")
public JpaProperties jpaProperties()
{
    return new JpaProperties();
}

@Bean
@Primary
public DataSource dataSource()
{
    if(toLoadDatasources.isEmpty())
    {
        throw new IllegalArgumentException("At least one datasource to load must be provided. Please check datasources configuration");
    }
    if(defaultDatasource == null || defaultDatasource.isEmpty())
    {
        throw new IllegalArgumentException("No default datasource provided. Please check datasources configuration");
    }
    if(!toLoadDatasources.contains(defaultDatasource))
    {
        throw new IllegalArgumentException("Default datasource must appear in the list of datasources to load. Please check datasources configuration");
    }

    final Map<Object, Object> map = new HashMap<Object, Object>();

    if(toLoadDatasources.contains(Customer.CUST1.name()))
    {
        map.put("datasourceCust1", dataSourceCust1());
    }
    if(toLoadDatasources.contains(Customer.CUST2.name()))
    {
        map.put("datasourceCust2", dataSourceCust2());
    }
    if(toLoadDatasources.contains(Customer.CUST3.name()))
    {
        map.put("datasourceCust3", dataSourceCust3());
    }
    if(toLoadDatasources.contains(Customer.CUST4.name()))
    {
        map.put("datasourceCust4", dataSourceCust4());
    }

    DataSourceRouter router = new DataSourceRouter();
    router.setTargetDataSources(map);

    if(Customer.CUST1.name().equalsIgnoreCase(defaultDatasource))
    {
        router.setDefaultTargetDataSource(dataSourceCust1());
    }
    else if(Customer.CUST2.name().equalsIgnoreCase(defaultDatasource))
    {
        router.setDefaultTargetDataSource(dataSourceCust2());
    }
    else if(Customer.CUST3.name().equalsIgnoreCase(defaultDatasource))
    {
        router.setDefaultTargetDataSource(dataSourceCust3());
    }
    else if(Customer.CUST4.name().equalsIgnoreCase(defaultDatasource))
    {
        router.setDefaultTargetDataSource(dataSourceCust4());
    }
    else
    {
        throw new IllegalArgumentException("At least one default datasource must be provided.");
    }

    return router;
}

@Bean
@Primary
public LocalContainerEntityManagerFactoryBean emfb(DataSource ds, EntityManagerFactoryBuilder builder, final JpaProperties jpaProperties)
{
    return builder.dataSource(ds)
            .packages("org.foo")
            .build();
}

@Bean
@Primary
public PlatformTransactionManager transactionManager(EntityManagerFactoryBuilder builder, final JpaProperties jpaProperties)
{
    JpaTransactionManager transactionManager = new JpaTransactionManager();
    transactionManager.setEntityManagerFactory(emfb(dataSource(), builder, jpaProperties).getObject());
    return transactionManager;
}

@Bean(name="dataSourceCust1")
@Conditional(LoadCust1DatasourceCondition.class)
@ConfigurationProperties(prefix = "spring.cust1.datasource")
public DataSource dataSourceCust1()
{
    return DataSourceBuilder.create().build();
}

@PersistenceContext(unitName = "entityManagerCust1")
@Bean(name="entityManagerCust1")
@Conditional(LoadCust1DatasourceCondition.class)
public LocalContainerEntityManagerFactoryBean emfbCust1(DataSource ds, EntityManagerFactoryBuilder builder, final JpaProperties jpaProperties)
{
    return builder.dataSource(ds)
            .packages("org.foo")
            .persistenceUnit("entityManagerCust1")
            .build();
}

@Bean(name="transactionManagerCust1")
@Conditional(LoadCust1DatasourceCondition.class)
public PlatformTransactionManager transactionManagerCust1(EntityManagerFactoryBuilder builder, final JpaProperties jpaProperties)
{
    JpaTransactionManager transactionManager = new JpaTransactionManager();
    transactionManager.setEntityManagerFactory(emfbCust1(dataSourceCust1(), builder, jpaProperties).getObject());
    return transactionManager;
}

@Bean(name="dataSourceCust2")
@Conditional(LoadCust2DatasourceCondition.class)
@ConfigurationProperties(prefix = "spring.cust2.datasource")
public DataSource dataSourceCust2()
{
    return DataSourceBuilder.create().build();
}

@PersistenceContext(unitName = "entityManagerCust2")
@Bean(name="entityManagerCust2")
@Conditional(LoadCust2DatasourceCondition.class)
public LocalContainerEntityManagerFactoryBean emfbCust2(@Qualifier("dataSourceCust2") DataSource ds, EntityManagerFactoryBuilder builder, final JpaProperties jpaProperties)
{
    return builder.dataSource(ds)
            .packages("org.foo")
            .persistenceUnit("entityManagerCust2")
            .build();
}

@Bean(name="transactionManagerCust2")
@Conditional(LoadCust2DatasourceCondition.class)
public PlatformTransactionManager transactionManagerCust2(EntityManagerFactoryBuilder builder, final JpaProperties jpaProperties)
{
    JpaTransactionManager transactionManager = new JpaTransactionManager();
    transactionManager.setEntityManagerFactory(emfbCust2(dataSourceCust2(), builder, jpaProperties).getObject());
    return transactionManager;
}

@Bean(name="dataSourceCust3")
@Conditional(LoadCust3DatasourceCondition.class)
@ConfigurationProperties(prefix = "spring.cust3.datasource")
public DataSource dataSourceCust3()
{
    return DataSourceBuilder.create().build();
}

@PersistenceContext(unitName = "entityManagerCust3")
@Bean(name="entityManagerCust3")
@Conditional(LoadCust3DatasourceCondition.class)
public LocalContainerEntityManagerFactoryBean emfbCust3(@Qualifier("dataSourceCust3") DataSource ds, EntityManagerFactoryBuilder builder, final JpaProperties jpaProperties)
{
    return builder.dataSource(ds)
            .packages("org.foo")
            .persistenceUnit("entityManagerCust3")
            .build();
}

@Bean(name="transactionManagerCust3")
@Conditional(LoadCust3DatasourceCondition.class)
public PlatformTransactionManager transactionManagerCust3(EntityManagerFactoryBuilder builder, final JpaProperties jpaProperties)
{
    JpaTransactionManager transactionManager = new JpaTransactionManager();
    transactionManager.setEntityManagerFactory(emfbCust3(dataSourceCust3(), builder, jpaProperties).getObject());
    return transactionManager;
}

@Bean(name="dataSourceCust4")
@Conditional(LoadCust4DatasourceCondition.class)
@ConfigurationProperties(prefix = "spring.cust4.datasource")
public DataSource dataSourceCust4()
{
    return DataSourceBuilder.create().build();
}

@PersistenceContext(unitName = "entityManagerCust4")
@Bean(name="entityManagerCust4")
@Conditional(LoadCust4DatasourceCondition.class)
public LocalContainerEntityManagerFactoryBean emfbCust4(@Qualifier("dataSourceCust4") DataSource ds, EntityManagerFactoryBuilder builder, final JpaProperties jpaProperties)
{
    return builder.dataSource(ds)
            .packages("org.foo")
            .persistenceUnit("entityManagerCust4")
            .build();
}

@Bean(name="transactionManagerCust4")
@Conditional(LoadCust4DatasourceCondition.class)
public PlatformTransactionManager transactionManagerCust4(EntityManagerFactoryBuilder builder, final JpaProperties jpaProperties)
{
    JpaTransactionManager transactionManager = new JpaTransactionManager();
    transactionManager.setEntityManagerFactory(emfbCust4(dataSourceCust4(), builder, jpaProperties).getObject());
    return transactionManager;
}

}

根据配置文件加载数据源。 LoadCust4DatasourceCondition 等类用于检查是否加载。

我的数据源配置文件是:

 # Datasources
 spring.cust1.datasource.driver-class-name: com.mysql.jdbc.Driver
 spring.cust1.datasource.url: 
 spring.cust1.datasource.username: root
 spring.cust1.datasource.password: pass

 spring.cust2.datasource.driver-class-name: com.mysql.jdbc.Driver
 spring.cust2.datasource.url: 
 spring.cust2.datasource.username: root
 spring.cust2.datasource.password: pass

 spring.cust3.datasource.driver-class-name: com.mysql.jdbc.Driver
 spring.cust3.datasource.url: 
 spring.cust3.datasource.username: root
 spring.cust3.datasource.password: pass

 spring.cust4.datasource.driver-class-name: com.mysql.jdbc.Driver
 spring.cust4.datasource.url: jdbc:
 spring.cust4.datasource.username: root
 spring.cust4.datasource.password: pass


 # JPA/Hibernate
 spring.jpa.hibernate.dialect: org.hibernate.dialect.MySQL5Dialect
 spring.jpa.hibernate.show_sql: true
 spring.jpa.hibernate.hbm2ddl.auto: none
 spring.jpa.entitymanager.packagesToScan: org.foo.domain

 load.datasources: CUST1, CUST2, CUST3, CUST4
 default.datasource: CUST1

我的服务是这样的:

 @Service
 public class InvoiceServiceImpl implements IInvoiceService {
  @Autowired
  private IInvoiceDao invoiceDao;

  @Override
  @Transactional(propagation = Propagation.REQUIRED, rollbackFor = {TechnicalException.class, BusinessException.class}, transactionManager = "transactionManagerCust1")
  public Invoice create(Invoice invoice, Customer customer) throws AbstractException {

    return invoiceDao.persist(invoice, customer);
}

}

我的道是这样的:

@Repository
public class InvoiceDaoImpl implements IInvoiceDao
{

@Autowired(required = false)
@Qualifier("entityManagerCust1")
private EntityManager entityManagerCust1;

@Autowired(required = false)
@Qualifier("entityManagerCust2")
private EntityManager entityManagerCust2;

@Autowired(required = false)
@Qualifier("entityManagerCust3")
private EntityManager entityManagerCust3;

@Autowired(required = false)
@Qualifier("entityManagerCust4")
private EntityManager entityManagerCust4;

@Override
public Invoice persist(Invoice invoice, Customer customer) throws AbstractException {

    try {
        getEntityManager(customer).persist(invoice);
    } catch(EntityExistsException eee) {
        logger.error(ExceptionConstantes.MSG_INV_ALRDY_EXIST);
        throw new BusinessException(ExceptionConstantes.MSG_INV_ALRDY_EXIST, ExceptionConstantes.CODE_INV_ALRDY_EXIST);
    }catch (Exception e){
        logger.error(String.format(ExceptionConstantes.MSG_CREATE_ERR, invoice.getClass().getSimpleName()), e);
        throw new TechnicalException(String.format(ExceptionConstantes.MSG_CREATE_ERR, invoice.getClass().getSimpleName()));
    }

    return invoice;
}




private EntityManager getEntityManager(Customer customer) throws IllegalArgumentException
{
    switch(customer)
    {
        case CUST1 : if(entityManagerCust1 == null){ throw new IllegalArgumentException("Requested " + customer.name() +"'s datasource is not loaded. Please check datasources configuration"); }
                  return entityManagerCust1;
        case CUST2 : if(entityManagerCust2 == null){ throw new IllegalArgumentException("Requested " + customer.name() + "'s datasource is not loaded. Please check datasources configuration"); }
                  return entityManagerCust2;
        case CUST3 : if(entityManagerCust3 == null){ throw new IllegalArgumentException("Requested " + customer.name() + "'s datasource is not loaded. Please check datasources configuration"); }
                  return entityManagerCust3;
        case CUST4 : if(entityManagerCust4 == null){ throw new IllegalArgumentException("Requested " + customer.name() + "'s datasource is not loaded. Please check datasources configuration"); }
                  return entityManagerCust4;
        default: throw new IllegalArgumentException("Invalid customer " + customer.name());
    }
}


@Conditional(LoadCust1DatasourceCondition.class)
public void setEntityManagerCust1(EntityManager entityManagerCust1) 
{
    this.entityManagerCust1 = entityManagerCust1;
}

@Conditional(LoadCust2DatasourceCondition.class)
public void setEntityManagerCust2(EntityManager entityManagerCust2) 
{
    this.entityManagerCust2 = entityManagerCust2;
}
@Conditional(LoadCust3DatasourceCondition.class)
public void setEntityManagerCust3(EntityManager entityManagerCust3) 
{
    this.entityManagerCust3 = entityManagerCust3;
}
@Conditional(LoadCust4DatasourceCondition.class)
public void setEntityManagerCust4(EntityManager entityManagerCust4) 
{
    this.entityManagerCust4 = entityManagerCust4;
}

在服务级别中,如果@TransactionaltransactionManager 属性未使用底层客户transactionManager bean 设置,则EntityManager 的persist 方法不会持久化到数据库。我希望根据使用的 Datasource/EntityManager 动态更改此值。

或全局事务管理器,但如果所有客户同时使用相同的服务和 DAO,则不会出现事务问题。

客户在使用 Invoice Service 的 Webservice 层确定。

感谢您的回复。

【问题讨论】:

  • 您什么时候开始了解您想要合作的客户(CUST1、CUST2 ...)?整个请求都一样吗?
  • 我已经更新了描述。客户是从 Web 服务层传递过来的。如果是 CUST1,他使用 entityManagerCust1,但他不能持久化到数据库,因为我们必须将 @Transactional 注解的 transactionManager 属性设置为 transactionManagerCust1。我不能这样做,因为它适用于 CUST1,但不适用于其他人

标签: hibernate jpa spring-boot spring-transactions


【解决方案1】:

我会尝试创建一个自定义PlatformTransactionManager,它将其调用委托给当前客户的正确事务管理器。为此,它必须能够从某个地方获取当前客户——例如从ThreadLocal 变量。像这样的:

public class CustomerAwareTransactionManager implements PlatformTransactionManager {

    // Tx managers beans and their names
    @Autowired 
    private Map<String, PlatformTransactionManager> txManagerMap;

    private PlatformTransactionManager getCurrentManager() {
        // CustomerHolder gets the customer from a ThreadLocal variable
        // something like SecurityContextHolder
        // It should be set just once for a request and removed at the end
        // of each request (to prevent memory leaks)
        String currentIdentifier = CustomerHolder.getCustomer().get().name;
        for (String managerName : txManagerMap.keySet()) {
            if (managerName.equals("transactionManager" + currentIdentifier)) {
                return txManagerMap.get(managerName);
            }
        }
        throw new IllegalStateException("No tx manager for id " + currentIdentifier);
    }
    @Override
    public commit(TransactionStatus status) {
        this.getCurrentManager().commit(status);
    }
    @Override
    public getTransaction(TransactionDefinition definition) {
        this.getCurrentManager().getTransaction(definition);
    }
    @Override
    public rollback(TransactionStatus status) {
        this.getCurrentManager().commit(status);
    }
}

DataSourceConfiguration 中,我已将主事务管理器bean 替换为以下sn-p:

 @Bean
@Primary
public PlatformTransactionManager transactionManager()
{
    return new CustomerAwareTransactionManager();
}

并且我在 CustomerHolder 中创建了一个 ThreadLocal 变量来存储当前的客户:

public class CustomerHolder
{
   private static ThreadLocal<Customer> customer= new ThreadLocal<Customer>();

public static ThreadLocal<Customer> getCustomer() {
    return customer;
}

public static void setCustomer(ThreadLocal<Customer> customer) {
    CustomerHolder.customer= customer;
}
}

在调用我们服务的 create 方法的 webservice 方法开始时,我将当前客户存储在 CustomerHolder 中,并在同一方法结束时删除当前客户以避免内存泄漏。

然后不要使用@TransactionaltransactionManager 属性并将此自定义事务管理器命名为transactionManager 以使其成为默认值。

【讨论】:

  • 谢谢@Ján Halaša:它解决了我的问题。我已经向 CustomerHolder 添加了一个 ThreadLocal 变量来存储当前客户,并在调用 web 服务后将其从 threadlocal 中删除以避免内存泄漏。再次感谢您!
【解决方案2】:

这是多租户设置、一个应用程序、多个数据库、每个客户端一个数据库的示例。在Spring boot - Multiple Database Access (MYSQL)查看我的回答

我在Multi-tenant applications using Spring Boot, JPA, Hibernate and Postgres的博文中也对此进行了介绍

基本配置持久层以支持多租户包括:

  • Hibernate、JPA 和数据源属性。比如:

application.yml

...
multitenancy:
  dvdrental:
    dataSources:
      -
        tenantId: TENANT_01
        url: jdbc:postgresql://172.16.69.133:5432/db_dvdrental
        username: user_dvdrental
        password: changeit
        driverClassName: org.postgresql.Driver
      -
        tenantId: TENANT_02
        url: jdbc:postgresql://172.16.69.133:5532/db_dvdrental
        username: user_dvdrental
        password: changeit
        driverClassName: org.postgresql.Driver
...

我使用了一个属性文件来存储租户数据,但这可以适应将租户信息存储在一种主数据库中。

MultiTenantJpaConfiguration.java

 ...
 @Configuration
 @EnableConfigurationProperties({ MultiTenantDvdRentalProperties.class, JpaProperties.class })
 @ImportResource(locations = { "classpath:applicationContent.xml" })
 @EnableTransactionManagement
 public class MultiTenantJpaConfiguration {

   @Autowired
   private JpaProperties jpaProperties;

   @Autowired
   private MultiTenantDvdRentalProperties multiTenantDvdRentalProperties;
 ...
 }

MultiTenantDvdRentalProperties.java

...
@Configuration
@ConfigurationProperties(prefix = "multitenancy.dvdrental")
public class MultiTenantDvdRentalProperties {

  private List<DataSourceProperties> dataSourcesProps;
  // Getters and Setters

  public static class DataSourceProperties extends org.springframework.boot.autoconfigure.jdbc.DataSourceProperties {

    private String tenantId;
    // Getters and Setters
  }
}
  • 数据源 bean

MultiTenantJpaConfiguration.java

 ...
 public class MultiTenantJpaConfiguration {
 ...
   @Bean(name = "dataSourcesDvdRental" )
   public Map<String, DataSource> dataSourcesDvdRental() {
       ...
   }
 ...
 }
  • 实体管理器工厂 bean

MultiTenantJpaConfiguration.java

 ...
 public class MultiTenantJpaConfiguration {
 ...
   @Bean
   public MultiTenantConnectionProvider multiTenantConnectionProvider() {
       ...
   }

   @Bean
   public CurrentTenantIdentifierResolver currentTenantIdentifierResolver() {
       ...
   }

   @Bean
   public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(MultiTenantConnectionProvider multiTenantConnectionProvider,
     CurrentTenantIdentifierResolver currentTenantIdentifierResolver) {
       ...  
   }
 ...
 }
  • 事务管理器 bean

MultiTenantJpaConfiguration.java

 ...
 public class MultiTenantJpaConfiguration {
 ...
   @Bean
   public EntityManagerFactory entityManagerFactory(LocalContainerEntityManagerFactoryBean entityManagerFactoryBean) {
       ...
   }

   @Bean
   public PlatformTransactionManager txManager(EntityManagerFactory entityManagerFactory) {
       ...
   }
 ...
 }
  • Spring Data JPA 和事务支持配置

applicationContent.xml

...
<jpa:repositories base-package="com.asimio.dvdrental.dao" transaction-manager-ref="txManager" />
<tx:annotation-driven transaction-manager="txManager" proxy-target-class="true" />
...

ActorDao.java

public interface ActorDao extends JpaRepository<Actor, Integer> {
}

根据您的需要,可以这样做:

...
@Autowired
private ActorDao actorDao;
...

DvdRentalTenantContext.setTenantId("TENANT_01");
this.actorDao.findOne(...);
...

// Or
DvdRentalTenantContext.setTenantId("TENANT_02");
this.actorDao.save(...);
...

可以在 servlet 过滤器/Spring MVC 拦截器/要执行 JPA 操作的线程等中设置tenantId。

【讨论】:

    猜你喜欢
    • 2014-05-27
    • 2022-09-27
    • 2011-11-11
    • 2011-01-01
    • 2015-04-22
    • 2019-12-30
    • 2019-07-02
    • 2012-01-19
    • 2015-01-18
    相关资源
    最近更新 更多