Preface
Systems with too many data sources are developed at work, such as asset inventory system. The data storage is divided into two libraries, one is the current library and the archive library. The system needs to configure two data sources to meet business needs. In conventional business scenarios, the business operations of the two warehouses are separated, and the well water will not interfere with the river. But there is an exception to the implementation of functions, which is archive. To archive the data of the current library, you need to modify the status of the current library data and insert the current library data into the archive library. This requires operating two data sources at the same time in the same method implementation. Directly using the declarative transaction @Transcational annotation cannot guarantee the consistency of the two transactions.
Declarative transactions can only achieve method-level granularity, and each method can only configure one transaction manager. Although logic can be split into multiple methods and then @Transactional annotation is added to each method, there are still problems and cannot handle multi-transactional business scenarios well. This problem can be solved using programming transactions, which can achieve code-level granularity and be more flexible.
Pre-environment
JDK8 + SringBoot2 + MySQL8
database
Create databases separately test1 test2
Create user tables in two databases separately
create table user ( id int auto_increment primary key, username varchar(255), password varchar(255) );
pom
<dependencyManagement> <dependencies> <dependency> <groupId></groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId></groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId></groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependencies>
yml
server: port: 8888 spring: datasource: primary: url: jdbc:mysql://localhost:3306/test1?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true username: root password: mysql driver-class-name: secondary: url: jdbc:mysql://localhost:3306/test2?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true username: root password: mysql driver-class-name: jpa: primary: show-sql: true properties: hibernate: hbm2ddl: auto: update dialect: .MySQL5InnoDBDialect secondary: show-sql: true properties: hibernate: hbm2ddl: auto: update dialect: .MySQL5InnoDBDialect
Config
Here, the main injection of the main library and the slave library areJDBCTemplateandTransactionManagerfor subsequent use
Main library data source configuration
@Configuration @EnableTransactionManagement @EnableJpaRepositories ( basePackages = PrimaryDatasourceAndJpaConfig.REPOSITORY_PACKAGE, entityManagerFactoryRef = "primaryEntityManagerFactory", transactionManagerRef = "primaryTransactionManager" ) public class PrimaryDatasourceAndJpaConfig { private static final String REPOSITORY_PACKAGE = ""; private static final String ENTITY_PACKAGE = ""; //----------------------------------------------------------------------------------------------------------------------------- /** * Scan the configuration information at the beginning * * @return Data source configuration information */ @Primary @Bean(name = "primaryDataSourceProperties") @ConfigurationProperties(prefix = "") public DataSourceProperties dataSourceProperties() { return new DataSourceProperties(); } /** * Take the main library data source object * * @param dataSourceProperties Inject bean named primaryDataSourceProperties * @return Data source object */ @Primary @Bean(name = "primaryDataSource") public DataSource dataSource(@Qualifier("primaryDataSourceProperties") DataSourceProperties dataSourceProperties) { return ().build(); } /** * This method is only selected when using JdbcTemplate object * * @param dataSource Inject a bean named primaryDataSource * @return Data source JdbcTemplate object */ @Primary @Bean(name = "primaryJdbcTemplate") public JdbcTemplate jdbcTemplate(@Qualifier("primaryDataSource") DataSource dataSource) { return new JdbcTemplate(dataSource); } /** * Scan the configuration information at the beginning * * @return jpa configuration information */ @Primary @Bean (name = "primaryJpaProperties") @ConfigurationProperties (prefix = "") public JpaProperties jpaProperties() { return new JpaProperties(); } /** * Get the main library entity management factory object * * @param primaryDataSource Inject a data source named primaryDataSource * @param jpaProperties Inject jpa configuration information named primaryJpaProperties * @param builder Inject EntityManagerFactoryBuilder * @return Entity management factory objects */ @Primary @Bean(name = "primaryEntityManagerFactory") public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean( @Qualifier ("primaryDataSource") DataSource primaryDataSource, @Qualifier("primaryJpaProperties") JpaProperties jpaProperties, EntityManagerFactoryBuilder builder ) { return builder // Set the data source .dataSource(primaryDataSource) // Set jpa configuration .properties(()) // Set the entity package name .packages(ENTITY_PACKAGE) // Set the persistent unit name to specify the data source when obtaining the EntityManager at @PersistenceContext annotation .persistenceUnit("primaryPersistenceUnit").build(); } /** * Get entity management object * * @param factory Inject bean named primaryEntityManagerFactory * @return Entity Management Objects */ @Primary @Bean(name = "primaryEntityManager") public EntityManager entityManager(@Qualifier("primaryEntityManagerFactory") EntityManagerFactory factory) { return (); } /** * Get the main library transaction management object * * @param factory Inject bean named primaryEntityManagerFactory * @return Transaction management object */ @Primary @Bean(name = "primaryTransactionManager") public JpaTransactionManager transactionManager(@Qualifier("primaryEntityManagerFactory") EntityManagerFactory factory) { return new JpaTransactionManager(factory); } }
Configuration from library data source
@Configuration @EnableTransactionManagement @EnableJpaRepositories( basePackages = SecondaryDatasourceAndJpaConfig.REPOSITORY_PACKAGE, entityManagerFactoryRef = "secondaryEntityManagerFactory", transactionManagerRef = "secondaryTransactionManager" ) public class SecondaryDatasourceAndJpaConfig { static final String REPOSITORY_PACKAGE = ""; static final String ENTITY_PACKAGE = ""; //----------------------------------------------------------------------------------------------------------------------------- /** * Scan the configuration information at the beginning * * @return Data source configuration information */ @Bean(name = "secondaryDataSourceProperties") @ConfigurationProperties(prefix = "") public DataSourceProperties dataSourceProperties() { return new DataSourceProperties(); } /** * Get the secondary data source object * * @param dataSourceProperties Inject bean named secondaryDataSourceProperties * @return Data source object */ @Bean("secondaryDataSource") public DataSource dataSource(@Qualifier("secondaryDataSourceProperties") DataSourceProperties dataSourceProperties) { return ().build(); } /** * This method is only selected when using JdbcTemplate object * * @param dataSource Inject a bean named secondaryDataSource * @return Data source JdbcTemplate object */ @Bean(name = "secondaryJdbcTemplate") public JdbcTemplate jdbcTemplate(@Qualifier("secondaryDataSource") DataSource dataSource) { return new JdbcTemplate(dataSource); } /** * Scan * * @return jpa configuration information */ @Bean(name = "secondaryJpaProperties") @ConfigurationProperties(prefix = "") public JpaProperties jpaProperties() { return new JpaProperties(); } /** * Get the secondary library entity management factory object * * @param secondaryDataSource Inject a data source named secondaryDataSource * @param jpaProperties Inject jpa configuration information named secondaryJpaProperties * @param builder Inject EntityManagerFactoryBuilder * @return Entity management factory objects */ @Bean(name = "secondaryEntityManagerFactory") public LocalContainerEntityManagerFactoryBean entityManagerFactory( @Qualifier("secondaryDataSource") DataSource secondaryDataSource, @Qualifier("secondaryJpaProperties") JpaProperties jpaProperties, EntityManagerFactoryBuilder builder ) { return builder // Set the data source .dataSource(secondaryDataSource) // Set jpa configuration .properties(()) // Set the entity package name .packages(ENTITY_PACKAGE) // Set the persistent unit name to specify the data source when obtaining the EntityManager at @PersistenceContext annotation .persistenceUnit("secondaryPersistenceUnit").build(); } /** * Get entity management object * * @param factory Inject bean named secondaryEntityManagerFactory * @return Entity Management Objects */ @Bean(name = "secondaryEntityManager") public EntityManager entityManager(@Qualifier("secondaryEntityManagerFactory") EntityManagerFactory factory) { return (); } /** * Get transaction management object * * @param factory Inject bean named secondaryEntityManagerFactory * @return Transaction management object */ @Bean(name = "secondaryTransactionManager") public JpaTransactionManager transactionManager(@Qualifier("secondaryEntityManagerFactory") EntityManagerFactory factory) { return new JpaTransactionManager(factory); } }
Declarative transactions
Wrong writing
@Service public class TestService { @Resource JdbcTemplate primaryJdbcTemplate; @Resource JdbcTemplate secondaryJdbcTemplate; @Transactional public void method() { //do something 1 ("insert into user(username, password) values('Zhang San', '123456')"); //do something 2 ("insert into user(username, password) values('Li Si', '123456');"); //do something 3 } }
@Transactional does not specify a transaction manager, which will not have any problems in a single data source system. In a single data source system, only one transaction manager is defined in the entire Spring container. When Spring starts a transaction, the transaction manager will be searched in the container by type by default. There is only one transaction manager in the container, which is just used, so there will be no problems.
However, in a multi-data source system, there will be multiple transaction managers in the Spring container. If the transaction manager is not specified, if the transaction manager is used and the actual data source are not consistent, transactions cannot be managed (because the @primary annotation is used to configure the main library data source, all transaction managers will be used by default).To use declarative transactions in a data source system, a transaction manager must be specified
The above code places both database operations in the same method. No matter which transaction manager is obtained, as long as an exception occurs at do something 3, one of the transactions will not be rolled back.
Improved writing
@Service public class TestService { @Resource JdbcTemplate primaryJdbcTemplate; @Resource JdbcTemplate secondaryJdbcTemplate; @Transactional(value = "primaryTransactionManager") public void method1() { //do something 1 ("insert into user(username, password) values('Zhang San', '123456')"); //do something 2 method2(); //do something 5 } @Transactional(value = "secondaryTransactionManager") public void method2() { //do something 3 ("insert into user(username, password) values('Li Si', '123456');"); //do something 4 } }
Improved writing method, split operations of different data sources into different methods, add @Transactional annotation, and specify the corresponding transaction manager. This writing method is much more standardized than before, but there are still problems. If an exception occurs at do something 5, because the method2 method has been executed and the transaction has been submitted, it is still impossible to roll back together.
Programming transactions
@Service public class TestService { @Resource JdbcTemplate primaryJdbcTemplate; @Resource JdbcTemplate secondaryJdbcTemplate; @Resource PlatformTransactionManager primaryTransactionManager; @Resource PlatformTransactionManager secondaryTransactionManager; public void method() { TransactionDefinition primaryDef = new DefaultTransactionDefinition(); TransactionStatus primaryStatus = (primaryDef); TransactionDefinition secondaryDef = new DefaultTransactionDefinition(); TransactionStatus secondaryStatus = (secondaryDef); try { //do something 1 ("insert into user(username, password) values('Zhang San', '123456')"); //do something 2 ("insert into user(username, password) values('Li Si', '123456');"); //do something 3 (primaryStatus); (secondaryStatus); } catch (Exception e) { (primaryStatus); (secondaryStatus); throw new RuntimeException(()); } } }
The granularity of programming transactions can be embedded in the method, so that transactions from different data sources can be controlled to be started at the same time. Once an exception occurs, the two transactions will roll back together, which ensures the consistency of multiple data transactions.
This implementation is actually the same as the XA mode idea of distributed transactions, except that distributed transactions manage different data sources in the distributed system, and here is a service that operates multiple data sources in the same method. In essence, they are all dealing with transactions that manage multiple data sources.
This is the article about SpringBoot operating multiple data sources with the same method to ensure transaction consistency. For more relevant SpringBoot transaction consistency content, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!