gpt4 book ai didi

java - 如何使用Spring以只读和读写方式进行数据库路由

转载 作者:行者123 更新时间:2023-12-05 00:53:24 27 4
gpt4 key购买 nike

我正在研究 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数据来源。

谁能帮我解决这个问题?

最佳答案

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

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

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

  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);

}

关于java - 如何使用Spring以只读和读写方式进行数据库路由,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/68012475/

27 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com