有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

java如何使用Spring在readonly和readwrite中进行数据库路由

我在春季研究事务路由,但我的应用程序存在运行时问题

我有两个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>

应用程序。属性

# 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

数据源类型。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;
    }

}

路由配置。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;
    }

}

亿万富翁。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;
    }
    
}

亿万富翁储蓄。java

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

}

亿万航空服务。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);
    }

}

在亿万富翁服务类中,我在findAll方法上应用了@Transactional(readOnly = true)来使用READ_ONLY数据源,但这并没有发生

findAll方法应用于READ_ONLY数据源,而save方法应用于READ_WRITE数据源

有人能帮我解决这个问题吗


共 (2) 个答案

  1. # 1 楼答案

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

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

    @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方法之前获取连接

  2. # 2 楼答案

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

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

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

    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,交易管理等,因为这将自动完成

    最后,这里有一个测试来测试这一点(您可以测试这两个场景)。只读模式将失败,因为那里没有模式,保存将成功,因为在事物的读写端有一个模式

    @Test
    void testDatabaseSwitch() {
        Assertions.assertThatThrownBy(() -> billionaireService.findAll())
                .isInstanceOf(DataAccessException.class);
    
        Billionaire newBIllionaire = new Billionaire(null, "Marten", "Deinum", "Spring Nerd.");
        billionaireService.save(newBIllionaire);
    
    }