【问题标题】:How to do database routing in read-only and read-write with Spring如何使用Spring以只读和读写方式进行数据库路由
【发布时间】:2021-06-17 03:24:58
【问题描述】:

我正在研究 Spring 中的事务路由,但我的应用程序存在运行时问题。

我有两个MySQL数据库,一个用于读,一个用于读/写,但是我的路由配置不起作用,当我应用只读配置时,我没有成功。

这是我的配置:

pom.xml

    <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.1</version>
    </parent>
    
    <groupId>br.com.multidatasources</groupId>
    <artifactId>multidatasources</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>multidatasources</name>
    
    <properties>
        <java.version>11</java.version>
    </properties>
    
    <dependencies>      
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>       
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

application.properties

# Database master
master.datasource.url=jdbc:mysql://localhost:3306/billionaires?createDatabaseIfNotExist=true&useTimezone=true&serverTimezone=UTC
master.datasource.username=root
master.datasource.password=root

# Database slave
slave.datasource.url=jdbc:mysql://localhost:3307/billionaires?createDatabaseIfNotExist=true&useTimezone=true&serverTimezone=UTC
slave.datasource.username=root
slave.datasource.password=root

# Database driver
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

# JPA property settings
spring.jpa.database=mysql
spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect

DataSourceType.java

public enum DataSourceType {
    READ_ONLY,
    READ_WRITE
}

TransactionRoutingDataSource.java

public class TransactionRoutingDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return TransactionSynchronizationManager.isCurrentTransactionReadOnly() ? DataSourceType.READ_ONLY : DataSourceType.READ_WRITE;
    }

}

RoutingConfiguration.java

@Configuration
@EnableTransactionManagement
public class RoutingConfiguration {
    
    private final Environment environment;
    
    public RoutingConfiguration(Environment environment) {
        this.environment = environment;
    }
    
    @Bean
    public JpaTransactionManager transactionManager(@Qualifier("entityManagerFactory") LocalContainerEntityManagerFactoryBean entityManagerFactory) {
        return new JpaTransactionManager(entityManagerFactory.getObject());
    }
    
    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(@Qualifier("routingDataSource") DataSource routingDataSource) {
        LocalContainerEntityManagerFactoryBean bean = new LocalContainerEntityManagerFactoryBean();
        bean.setDataSource(routingDataSource);
        bean.setPackagesToScan(Billionaires.class.getPackageName());
        bean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
        bean.setJpaProperties(additionalProperties());
        return bean;
    }
    
    @Bean
    public DataSource dataSource(@Qualifier("routingDataSource") DataSource routingDataSource) {
        return new LazyConnectionDataSourceProxy(routingDataSource);
    }
    
    @Bean
    public TransactionRoutingDataSource routingDataSource(
            @Qualifier("masterDataSource") DataSource masterDataSource,
            @Qualifier("slaveDataSource") DataSource slaveDataSource
    ) {
        TransactionRoutingDataSource routingDataSource = new TransactionRoutingDataSource();
 
        Map<Object, Object> dataSourceMap = new HashMap<>();
        dataSourceMap.put(DataSourceType.READ_WRITE, masterDataSource);
        dataSourceMap.put(DataSourceType.READ_ONLY, slaveDataSource);
 
        routingDataSource.setTargetDataSources(dataSourceMap);
        routingDataSource.setDefaultTargetDataSource(masterDataSource());

        return routingDataSource;
    }
    
    @Bean
    public DataSource masterDataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setUrl(environment.getProperty("master.datasource.url"));
        dataSource.setUsername(environment.getProperty("master.datasource.username"));
        dataSource.setPassword(environment.getProperty("master.datasource.password"));
        return dataSource;
    }

    @Bean
    public DataSource slaveDataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setUrl(environment.getProperty("slave.datasource.url"));
        dataSource.setUsername(environment.getProperty("slave.datasource.username"));
        dataSource.setPassword(environment.getProperty("slave.datasource.password"));
        return dataSource;
    }
    
    private Properties additionalProperties() {
        Properties properties = new Properties();
        properties.setProperty("hibernate.dialect", "org.hibernate.dialect.MySQL8Dialect");
           
        return properties;
    }

}

Billionaires.java

@Entity
@Table(name = "billionaires")
public class Billionaires {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = "first_name")
    private String firstName;
    
    @Column(name = "last_name")
    private String lastName;
    
    private String career;
    
    public Billionaires() { }

    public Billionaires(Long id, String firstName, String lastName, String career) {        
        this.id = id;
        this.firstName = firstName;
        this.lastName = lastName;
        this.career = career;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getCareer() {
        return career;
    }

    public void setCareer(String career) {
        this.career = career;
    }
    
}

BillionairesRepository.java

@Repository
public interface BillionairesRepository extends JpaRepository<Billionaires, Long> {

}

BillionairesService.java

@Service
public class BillionairesService {
    
    private final BillionairesRepository billionairesRepository;

    public BillionairesService(BillionairesRepository billionairesRepository) {
        this.billionairesRepository = billionairesRepository;
    }
    
    @Transactional(readOnly = true)  // Should be used the READ_ONLY  (This point not working)
    public List<Billionaires> findAll() {
        return billionairesRepository.findAll();
    }
    
    @Transactional // Should be used the READ_WRITE
    public Billionaires save(Billionaires billionaires) {
        return billionairesRepository.save(billionaires);
    }

}

在 BillionairesService 类中,我在 findAll 上应用了 @Transactional(readOnly = true) 方法以使用 READ_ONLY 数据源,但这并没有发生。

findAll 方法应使用READ_ONLY 数据源,save 方法应使用READ_WRITE 数据源。

谁能帮我解决这个问题?

【问题讨论】:

  • 当从数据源获得连接时,不必有事务。正如我怀疑您正在使用视图中的打开会话(默认情况下),这甚至在数据源可以确定 tx 状态之前发生。
  • 附加提示,您使用的是 Spring Boot,因此您只需要配置数据源,JPA 的东西仍然可以由 Spring Boot 以及@EnableTransactionManagement 自动配置。还要检查你是否使用了正确的TransactionSynchronizationManager(有 2 一个响应式和一个经典,你应该使用后一个!)。
  • @M.Deinum 我使用@EnableTransactionalManagement 来创建一个bean entityManagerFactory,因为没有这个设置会导致初始化异常:` ************** ************* 应用程序启动失败 *************************** 描述:构造函数的参数 0在 br.com.multidatasources.multidatasources.service.BillionairesService 中需要一个名为“entityManagerFactory”的 bean,但无法找到。行动:考虑在您的配置中定义一个名为“entityManagerFactory”的bean。 `
  • TransactionSynchronizationManager 我使用的是经典,没有任何反应。
  • @EnableTransactionManagement 不创建该 bean,它只注册方面以驱动事务。您应该将LazyDataSourceConnection bean 标记为@Primary,以便自动配置的实体管理器使用它。

标签: java spring spring-boot spring-data-jpa spring-data


【解决方案1】:

我强烈建议尽可能多地使用自动配置,这会使事情变得更简单。主要的关键是设置延迟获取连接并为当前事务做准备。

这可以通过两种不同的方式实现。

  1. JpaDialectprepareConnection 属性设置为false。如果您不这样做,那么JpaTransactionManager 将急切地获得Connection 并为交易做好准备。这甚至在它有时间将事务的当前状态设置到TransactionSynchronizationManager 之前。这将使对TransactionSynchronizationManager.isCurrentTransactionReadOnly 的调用始终返回false(因为它设置在JpaTransactionManagerdoBegin 方法的末尾。

  2. hibernate.connection.handling_mode 设置为DELAYED_ACQUISITION_AND_RELEASE_AFTER_TRANSACTION。这将延迟连接的获取并在事务后关闭连接。如果没有 Spring,这也是 Hibernate 5.2+ 的默认设置(请参阅 Hibernate User Guide),但由于遗留原因,Spring 将其切换为 DELAYED_ACQUISITION_AND_HOLD

这些解决方案中的任何一个都将起作用,因为连接的准备被延迟,因此JpaTransactionManager 有时间同步TransactionSynchronizationManager 中的状态。

@Bean
public BeanPostProcessor dialectProcessor() {

    return new BeanPostProcessor() {
        @Override
        public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
            if (bean instanceof HibernateJpaVendorAdapter) {
                ((HibernateJpaVendorAdapter) bean).getJpaDialect().setPrepareConnection(false);
            }
            return bean;
        }
    };
}

但是,将此属性添加到您的 application.properties 也可以:

spring.jpa.properties.hibernate.connection.handling_mode=DELAYED_ACQUISITION_AND_RELEASE_AFTER_TRANSACTION

使用其中任何一种解决方案,您现在都可以放弃事务配置、jpa 等。还有一种更简单的方法来配置多个数据源。它在Spring Boot Reference Guide 中有描述,它将尽可能多地重用 Spring 自动配置。

首先确保您的application.properties中包含以下内容

# DATABASE MASTER PROPERTIES
master.datasource.url=jdbc:h2:mem:masterdb;DB_CLOSE_DELAY=-1
master.datasource.username=sa
master.datasource.password=sa
master.datasource.configuration.pool-name=Master-DB

# DATABASE SLAVE PROPERTIES
slave.datasource.url=jdbc:h2:mem:slavedb;DB_CLOSE_DELAY=-1
slave.datasource.username=sa
slave.datasource.password=sa
slave.datasource.configuration.pool-name=Slave-DB

# JPA PROPERTIES SETTINGS
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.properties.hibernate.connection.provider_disables_autocommit=true
spring.jpa.open-in-view=false

# ENABLE ERRORS IN DESERIALIZATION OF MISSING OR IGNORED PROPERTIES
spring.jackson.deserialization.fail-on-unknown-properties=true
spring.jackson.deserialization.fail-on-ignored-properties=true

# ENABLE ERRORS ON REQUESTS FOR NON-EXISTENT RESOURCES
spring.mvc.throw-exception-if-no-handler-found=true

# DISABLE MAPPINGS OF STATIC RESOURCES (IS NOT USABLE IN DEVELOPMENT OF APIs)
spring.web.resources.add-mappings=false

注意: 删除了 JDBC 驱动程序(不需要),仅设置 spring.jpa.database-platform 您设置了 databasedatabase-platform,而不是两者。

现在有了这个和下面的 @Configuration 类,您将拥有 2 个数据源,路由一个和上面提到的 BeanPostProcessor(如果您选择使用该属性,您可以删除上述 BeanPostProcessor

@Configuration
public class DatasourceConfiguration {

    @Bean
    @ConfigurationProperties("master.datasource")
    public DataSourceProperties masterDataSourceProperties() {
        return new DataSourceProperties();
    }

    @Bean
    @ConfigurationProperties("master.datasource.configuration")
    public HikariDataSource masterDataSource(DataSourceProperties masterDataSourceProperties) {
        return masterDataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
    }

    @Bean
    @ConfigurationProperties("slave.datasource")
    public DataSourceProperties slaveDataSourceProperties() {
        return new DataSourceProperties();
    }

    @Bean
    @ConfigurationProperties("slave.datasource.configuration")
    public HikariDataSource slaveDataSource(DataSourceProperties slaveDataSourceProperties) {
        return slaveDataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
    }

    @Bean
    @Primary
    public TransactionRoutingDataSource routingDataSource(DataSource masterDataSource,  DataSource slaveDataSource) {
        TransactionRoutingDataSource routingDataSource = new TransactionRoutingDataSource();

        Map<Object, Object> dataSourceMap = new HashMap<>();
        dataSourceMap.put(DataSourceType.READ_WRITE, masterDataSource);
        dataSourceMap.put(DataSourceType.READ_ONLY, slaveDataSource);

        routingDataSource.setTargetDataSources(dataSourceMap);
        routingDataSource.setDefaultTargetDataSource(masterDataSource);

        return routingDataSource;
    }

    @Bean
    public BeanPostProcessor dialectProcessor() {

        return new BeanPostProcessor() {
            @Override
            public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
                if (bean instanceof HibernateJpaVendorAdapter) {
                    ((HibernateJpaVendorAdapter) bean).getJpaDialect().setPrepareConnection(false);
                }
                return bean;
            }
        };
    }
}

这将设置您工作所需的一切,并且仍然能够尽可能多地使用自动配置和检测。有了这个,您需要做的唯一配置就是这个DataSource 设置。没有 JPA、事务管理等,因为这将自动完成。

最后,这里有一个测试来测试这个(你可以测试两种场景)。只读的会失败,因为那里没有架构,保存会成功,因为 READ_WRITE 方面有架构。

@Test
void testDatabaseSwitch() {
    Assertions.assertThatThrownBy(() -> billionaireService.findAll())
            .isInstanceOf(DataAccessException.class);

    Billionaire newBIllionaire = new Billionaire(null, "Marten", "Deinum", "Spring Nerd.");
    billionaireService.save(newBIllionaire);

}

【讨论】:

  • 这是一个非常好的建议!我的错是没有将@Bean 定义为@Primary,因此出现entityManager 初始化错误。我非常感谢您对我的帮助。
【解决方案2】:

我通过更改 RoutingConfiguration.java 类的实现解决了这个问题。

我将数据源配置为使用setAutoCommit(false) 配置,并添加属性hibernate.connection.provider_disables_autocommit 和值为true

@Configuration
@EnableTransactionManagement
public class RoutingConfiguration {

    private final Environment environment;

    public RoutingConfiguration(Environment environment) {
        this.environment = environment;
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(@Qualifier("routingDataSource") DataSource routingDataSource) {
        LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();

        entityManagerFactoryBean.setPersistenceUnitName(getClass().getSimpleName());
        entityManagerFactoryBean.setPersistenceProvider(new HibernatePersistenceProvider());
        entityManagerFactoryBean.setDataSource(routingDataSource);
        entityManagerFactoryBean.setPackagesToScan(Billionaires.class.getPackageName());

        HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        HibernateJpaDialect jpaDialect = vendorAdapter.getJpaDialect();

        jpaDialect.setPrepareConnection(false);

        entityManagerFactoryBean.setJpaVendorAdapter(vendorAdapter);
        entityManagerFactoryBean.setJpaProperties(additionalProperties());

        return entityManagerFactoryBean;
    }

    @Bean
    public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory){
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(entityManagerFactory);
        return transactionManager;
    }

    @Bean
    public TransactionTemplate transactionTemplate(EntityManagerFactory entityManagerFactory) {
        return new TransactionTemplate(transactionManager(entityManagerFactory));
    }

    @Bean
    public TransactionRoutingDataSource routingDataSource(
            @Qualifier("masterDataSource") DataSource masterDataSource,
            @Qualifier("slaveDataSource") DataSource slaveDataSource
    ) {
        TransactionRoutingDataSource routingDataSource = new TransactionRoutingDataSource();

        Map<Object, Object> dataSourceMap = new HashMap<>();
        dataSourceMap.put(DataSourceType.READ_WRITE, masterDataSource);
        dataSourceMap.put(DataSourceType.READ_ONLY, slaveDataSource);

        routingDataSource.setTargetDataSources(dataSourceMap);
        routingDataSource.setDefaultTargetDataSource(masterDataSource());

        return routingDataSource;
    }

    @Bean
    public DataSource masterDataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setUrl(environment.getProperty("master.datasource.url"));
        dataSource.setUsername(environment.getProperty("master.datasource.username"));
        dataSource.setPassword(environment.getProperty("master.datasource.password"));
        return connectionPoolDataSource(dataSource, determinePoolName(DataSourceType.READ_WRITE));
    }

    @Bean
    public DataSource slaveDataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setUrl(environment.getProperty("slave.datasource.url"));
        dataSource.setUsername(environment.getProperty("slave.datasource.username"));
        dataSource.setPassword(environment.getProperty("slave.datasource.password"));
        return connectionPoolDataSource(dataSource, determinePoolName(DataSourceType.READ_ONLY));
    }

    private HikariDataSource connectionPoolDataSource(DataSource dataSource, String poolName) {
        return new HikariDataSource(hikariConfig(dataSource, poolName));
    }

    private HikariConfig hikariConfig(DataSource dataSource, String poolName) {
        HikariConfig hikariConfig = new HikariConfig();

        hikariConfig.setPoolName(poolName);
        hikariConfig.setMaximumPoolSize(Runtime.getRuntime().availableProcessors() * 4);
        hikariConfig.setDataSource(dataSource);
        hikariConfig.setAutoCommit(false);

        return hikariConfig;
    }

    private Properties additionalProperties() {
        Properties properties = new Properties();

        properties.setProperty("hibernate.dialect", environment.getProperty("spring.jpa.database-platform"));
        properties.setProperty("hibernate.connection.provider_disables_autocommit", "true");

        return properties;
    }

    private String determinePoolName(DataSourceType dataSourceType) {
        return dataSourceType.getPoolName().concat("-").concat(dataSourceType.name());
    }

}

hibernate.connection.provider_disables_autocommit 允许在调用determineCurrentLookupKey 方法之前获取连接。

【讨论】:

  • 您可以通过application.properties设置该属性,您不需要为此替换JPA的自动配置。
  • 我尝试使用这种方法,但它没有达到预期的效果,需要我自己创建与 JPA 设置相关的 bean。
  • 这是最终解决方案的研究案例的存储库:github.com/jonathanmdr/RoutingDataSource
  • 你没有通过 JPA 设置属性(正如我提到的那样)如果你不这样做当然仍然无法工作。
  • 我按照你的指示做了,但是没有用。出于这个原因,我按照答案保留了实现。
猜你喜欢
  • 1970-01-01
  • 2014-04-11
  • 2011-06-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-03-01
  • 1970-01-01
相关资源
最近更新 更多