我正在研究 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>
应用程序属性
# 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;
}
}
路由配置.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;
}
}
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 类中,我应用
@Transactional(readOnly = true)
on findAll
方法来使用 READ_ONLY
数据源,但这并没有发生。
findAll
方法应使用READ_ONLY
数据源,save
方法应使用READ_WRITE
数据源。
有人可以帮我解决这个问题吗?
我强烈建议尽可能使用自动配置,它会让事情变得更简单。主要关键是设置延迟获取连接并为当前事务做好准备。
这可以通过两种不同的方式来实现。
将
prepareConnection
的 JpaDialect
属性设置为 false
。如果您不这样做,那么 JpaTransactionManager
将急切地获取 Connection
并为交易做好准备。这甚至是在它有时间将事务的当前状态设置到 TransactionSynchronizationManager
之前。这将使对 TransactionSynchronizationManager.isCurrentTransactionReadOnly
的调用始终返回 false
(因为它是在 doBegin
中 JpaTransactionManager
方法的末尾设置的。
将
hibernate.connection.handling_mode
设置为 DELAYED_ACQUISITION_AND_RELEASE_AFTER_TRANSACTION
。这将延迟连接的获取并在事务完成后关闭连接。如果没有 Spring,这也是 Hibernate 5.2+ 的默认设置(请参阅 Hibernate 用户指南),但由于遗留原因,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 参考指南 中进行了描述,它将尽可能多地重用 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
您设置 database
或 database-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);
}
我通过更改
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
方法之前获取连接。
我尝试了上述解决方案,发现 HikariDataSource 始终为 null。即使添加了 @Transactional(readOnly=true),路由也发生在 READ_WRITE 数据库上。
我正在使用 Spring boot 3.2.4 和 java 17。
DataSourceConfiguration.java
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class DataSourceConfiguration {
@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;
}
};
}
@Bean
@ConfigurationProperties("app.datasource.readwrite.configuration")
public HikariDataSource masterDataSource(DataSourceProperties masterDataSourceProperties) {
return masterDataSourceProperties.initializeDataSourceBuilder()
.type(HikariDataSource.class)
.build();
}
@Bean
@ConfigurationProperties("app.datasource.readwrite")
public DataSourceProperties masterDataSourceProperties() {
return new DataSourceProperties();
}
@Bean
@ConfigurationProperties("app.datasource.readonly.configuration")
public HikariDataSource slaveDataSource(DataSourceProperties slaveDataSourceProperties) {
return slaveDataSourceProperties.initializeDataSourceBuilder()
.type(HikariDataSource.class)
.build();
}
@Bean
@ConfigurationProperties("app.datasource.readonly")
public DataSourceProperties slaveDataSourceProperties() {
return new DataSourceProperties();
}
@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;
}
}
TransactionRoutingDataSource.java
@Slf4j
public class TransactionRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
boolean isReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
DataSourceType dataSourceType = isReadOnly ? DataSourceType.READ_ONLY : DataSourceType.READ_WRITE;
log.info("Current DataSource type: {}", dataSourceType);
return dataSourceType;
}
}
DataSourceType.java
public enum DataSourceType {
READ_ONLY,
READ_WRITE
}
应用属性
app.datasource.readwrite.url=jdbc:mysql://localhost:3306/demotest
app.datasource.readwrite.username=root
app.datasource.readwrite.password=toor
app.datasource.readonly.url=jdbc:mysql://localhost:3307/demotest
app.datasource.readonly.username=root
app.datasource.readonly.password=toor
spring.jpa.database=default
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
spring.jpa.hibernate.naming.implicit-strategy=org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl
spring.jpa.show-sql=true
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>3.2.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo-dbrouting</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo-dbrouting</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>17</java.version>
</properties>
<profiles>
<profile>
<id>dev</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<log4j.level>DEBUG</log4j.level>
<spring.profiles.active>dev</spring.profiles.active>
</properties>
</profile>
</profiles>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</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>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
请任何人帮助我。