SpringBoot同一個(gè)方法操作多個(gè)數(shù)據(jù)源保證事務(wù)一致性
前言
工作中開(kāi)發(fā)過(guò)多數(shù)據(jù)源的系統(tǒng),比如資產(chǎn)清查系統(tǒng),數(shù)據(jù)的存儲(chǔ)分成了兩個(gè)庫(kù),一個(gè)當(dāng)前庫(kù)和歸檔庫(kù),系統(tǒng)就需要配置兩個(gè)數(shù)據(jù)源來(lái)滿足業(yè)務(wù)需求。在常規(guī)的業(yè)務(wù)場(chǎng)景下,對(duì)兩個(gè)庫(kù)的業(yè)務(wù)操作是分開(kāi)的,井水不犯河水。但是有一個(gè)功能實(shí)現(xiàn)是個(gè)例外,就是歸檔。將當(dāng)前庫(kù)的數(shù)據(jù)進(jìn)行歸檔,需要修改當(dāng)前庫(kù)數(shù)據(jù)的狀態(tài),并將當(dāng)前庫(kù)數(shù)據(jù)插入到歸檔庫(kù)中,這就需要在同一個(gè)方法實(shí)現(xiàn)中同時(shí)操作兩個(gè)數(shù)據(jù)源,直接使用聲明式事務(wù)@Transcational注解是無(wú)法保證兩個(gè)事務(wù)的一致性的。
聲明式事務(wù)則只能做到方法級(jí)別的顆粒度,而且每個(gè)方法只能配置一個(gè)事務(wù)管理器,雖然可以將邏輯拆分到多個(gè)方法中,再為每個(gè)方法加上@Transactional注解,但還是會(huì)存在問(wèn)題,無(wú)法很好地處理多事務(wù)的業(yè)務(wù)場(chǎng)景。而這種問(wèn)題可以使用編程式事務(wù)來(lái)解決,編程式事務(wù)可以將做到代碼級(jí)別的顆粒度,更加的靈活。
前置環(huán)境
JDK8 + SringBoot2 + MySQL8
數(shù)據(jù)庫(kù)
分別創(chuàng)建數(shù)據(jù)庫(kù) test1 test2
分別在兩個(gè)數(shù)據(jù)庫(kù)中創(chuàng)建 user 表
create table user (
id int auto_increment primary key,
username varchar(255),
password varchar(255)
);pom
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</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: com.mysql.cj.jdbc.Driver
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: com.mysql.cj.jdbc.Driver
jpa:
primary:
show-sql: true
properties:
hibernate:
hbm2ddl:
auto: update
dialect: org.hibernate.dialect.MySQL5InnoDBDialect
secondary:
show-sql: true
properties:
hibernate:
hbm2ddl:
auto: update
dialect: org.hibernate.dialect.MySQL5InnoDBDialectConfig
這里主要注入主庫(kù)和從庫(kù)各自的JDBCTemplate和TransactionManager,以便后續(xù)使用
主庫(kù)數(shù)據(jù)源配置
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories (
basePackages = PrimaryDatasourceAndJpaConfig.REPOSITORY_PACKAGE,
entityManagerFactoryRef = "primaryEntityManagerFactory",
transactionManagerRef = "primaryTransactionManager"
)
public class PrimaryDatasourceAndJpaConfig {
private static final String REPOSITORY_PACKAGE = "com.jpa.dao.primary";
private static final String ENTITY_PACKAGE = "com.jpa.entity.primary";
//--------------數(shù)據(jù)源配置-------------------
/**
* 掃描spring.datasource.primary開(kāi)頭的配置信息
*
* @return 數(shù)據(jù)源配置信息
*/
@Primary
@Bean(name = "primaryDataSourceProperties")
@ConfigurationProperties(prefix = "spring.datasource.primary")
public DataSourceProperties dataSourceProperties() {
return new DataSourceProperties();
}
/**
* 取主庫(kù)數(shù)據(jù)源對(duì)象
*
* @param dataSourceProperties 注入名為primaryDataSourceProperties的bean
* @return 數(shù)據(jù)源對(duì)象
*/
@Primary
@Bean(name = "primaryDataSource")
public DataSource dataSource(@Qualifier("primaryDataSourceProperties") DataSourceProperties dataSourceProperties) {
return dataSourceProperties.initializeDataSourceBuilder().build();
}
/**
* 該方法僅在需要使用JdbcTemplate對(duì)象時(shí)選用
*
* @param dataSource 注入名為primaryDataSource的bean
* @return 數(shù)據(jù)源JdbcTemplate對(duì)象
*/
@Primary
@Bean(name = "primaryJdbcTemplate")
public JdbcTemplate jdbcTemplate(@Qualifier("primaryDataSource") DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
/**
* 掃描spring.jpa.primary開(kāi)頭的配置信息
*
* @return jpa配置信息
*/
@Primary
@Bean (name = "primaryJpaProperties")
@ConfigurationProperties (prefix = "spring.jpa.primary")
public JpaProperties jpaProperties() {
return new JpaProperties();
}
/**
* 獲取主庫(kù)實(shí)體管理工廠對(duì)象
*
* @param primaryDataSource 注入名為primaryDataSource的數(shù)據(jù)源
* @param jpaProperties 注入名為primaryJpaProperties的jpa配置信息
* @param builder 注入EntityManagerFactoryBuilder
* @return 實(shí)體管理工廠對(duì)象
*/
@Primary
@Bean(name = "primaryEntityManagerFactory")
public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(
@Qualifier ("primaryDataSource") DataSource primaryDataSource,
@Qualifier("primaryJpaProperties") JpaProperties jpaProperties,
EntityManagerFactoryBuilder builder
) {
return builder
// 設(shè)置數(shù)據(jù)源
.dataSource(primaryDataSource)
// 設(shè)置jpa配置
.properties(jpaProperties.getProperties())
// 設(shè)置實(shí)體包名
.packages(ENTITY_PACKAGE)
// 設(shè)置持久化單元名,用于@PersistenceContext注解獲取EntityManager時(shí)指定數(shù)據(jù)源
.persistenceUnit("primaryPersistenceUnit").build();
}
/**
* 獲取實(shí)體管理對(duì)象
*
* @param factory 注入名為primaryEntityManagerFactory的bean
* @return 實(shí)體管理對(duì)象
*/
@Primary
@Bean(name = "primaryEntityManager")
public EntityManager entityManager(@Qualifier("primaryEntityManagerFactory") EntityManagerFactory factory) {
return factory.createEntityManager();
}
/**
* 獲取主庫(kù)事務(wù)管理對(duì)象
*
* @param factory 注入名為primaryEntityManagerFactory的bean
* @return 事務(wù)管理對(duì)象
*/
@Primary
@Bean(name = "primaryTransactionManager")
public JpaTransactionManager transactionManager(@Qualifier("primaryEntityManagerFactory") EntityManagerFactory factory) {
return new JpaTransactionManager(factory);
}
}從庫(kù)數(shù)據(jù)源配置
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
basePackages = SecondaryDatasourceAndJpaConfig.REPOSITORY_PACKAGE,
entityManagerFactoryRef = "secondaryEntityManagerFactory",
transactionManagerRef = "secondaryTransactionManager"
)
public class SecondaryDatasourceAndJpaConfig {
static final String REPOSITORY_PACKAGE = "com.jpa.dao.secondary";
static final String ENTITY_PACKAGE = "com.jpa.entity.secondary";
//--------------數(shù)據(jù)源配置-------------------
/**
* 掃描spring.datasource.secondary開(kāi)頭的配置信息
*
* @return 數(shù)據(jù)源配置信息
*/
@Bean(name = "secondaryDataSourceProperties")
@ConfigurationProperties(prefix = "spring.datasource.secondary")
public DataSourceProperties dataSourceProperties() {
return new DataSourceProperties();
}
/**
* 獲取次數(shù)據(jù)源對(duì)象
*
* @param dataSourceProperties 注入名為secondaryDataSourceProperties的bean
* @return 數(shù)據(jù)源對(duì)象
*/
@Bean("secondaryDataSource")
public DataSource dataSource(@Qualifier("secondaryDataSourceProperties") DataSourceProperties dataSourceProperties) {
return dataSourceProperties.initializeDataSourceBuilder().build();
}
/**
* 該方法僅在需要使用JdbcTemplate對(duì)象時(shí)選用
*
* @param dataSource 注入名為secondaryDataSource的bean
* @return 數(shù)據(jù)源JdbcTemplate對(duì)象
*/
@Bean(name = "secondaryJdbcTemplate")
public JdbcTemplate jdbcTemplate(@Qualifier("secondaryDataSource") DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
/**
* 掃描spring.jpa.secondary
*
* @return jpa配置信息
*/
@Bean(name = "secondaryJpaProperties")
@ConfigurationProperties(prefix = "spring.jpa.secondary")
public JpaProperties jpaProperties() {
return new JpaProperties();
}
/**
* 獲取次庫(kù)實(shí)體管理工廠對(duì)象
*
* @param secondaryDataSource 注入名為secondaryDataSource的數(shù)據(jù)源
* @param jpaProperties 注入名為secondaryJpaProperties的jpa配置信息
* @param builder 注入EntityManagerFactoryBuilder
* @return 實(shí)體管理工廠對(duì)象
*/
@Bean(name = "secondaryEntityManagerFactory")
public LocalContainerEntityManagerFactoryBean entityManagerFactory(
@Qualifier("secondaryDataSource") DataSource secondaryDataSource,
@Qualifier("secondaryJpaProperties") JpaProperties jpaProperties,
EntityManagerFactoryBuilder builder
) {
return builder
// 設(shè)置數(shù)據(jù)源
.dataSource(secondaryDataSource)
// 設(shè)置jpa配置
.properties(jpaProperties.getProperties())
// 設(shè)置實(shí)體包名
.packages(ENTITY_PACKAGE)
// 設(shè)置持久化單元名,用于@PersistenceContext注解獲取EntityManager時(shí)指定數(shù)據(jù)源
.persistenceUnit("secondaryPersistenceUnit").build();
}
/**
* 獲取實(shí)體管理對(duì)象
*
* @param factory 注入名為secondaryEntityManagerFactory的bean
* @return 實(shí)體管理對(duì)象
*/
@Bean(name = "secondaryEntityManager")
public EntityManager entityManager(@Qualifier("secondaryEntityManagerFactory") EntityManagerFactory factory) {
return factory.createEntityManager();
}
/**
* 獲取事務(wù)管理對(duì)象
*
* @param factory 注入名為secondaryEntityManagerFactory的bean
* @return 事務(wù)管理對(duì)象
*/
@Bean(name = "secondaryTransactionManager")
public JpaTransactionManager transactionManager(@Qualifier("secondaryEntityManagerFactory") EntityManagerFactory factory) {
return new JpaTransactionManager(factory);
}
}聲明式事務(wù)
錯(cuò)誤寫(xiě)法
@Service
public class TestService {
@Resource
JdbcTemplate primaryJdbcTemplate;
@Resource
JdbcTemplate secondaryJdbcTemplate;
@Transactional
public void method() {
//do something 1
primaryJdbcTemplate.execute("insert into user(username, password) values('張三', '123456')");
//do something 2
secondaryJdbcTemplate.execute("insert into user(username, password) values('李四', '123456');");
//do something 3
}
}@Transactional中沒(méi)有指定事務(wù)管理器,這在單數(shù)據(jù)源系統(tǒng)中就不會(huì)有任何問(wèn)題,在單數(shù)據(jù)源系統(tǒng)中,整個(gè)Spring容器中只定義了一個(gè)事務(wù)管理器,Spring啟動(dòng)事務(wù)的時(shí)候,默認(rèn)會(huì)按類型在容器中查找事務(wù)管理器,而容器中就只有一個(gè)事務(wù)管理器,正好拿來(lái)用,不會(huì)有問(wèn)題。
但是在多數(shù)據(jù)源系統(tǒng)中,Spring容器中是會(huì)存在多個(gè)事務(wù)管理器的,如果不指定事務(wù)管理器,如果使用的事務(wù)管理器和實(shí)際操作的數(shù)據(jù)源不一致的話,是管理不了事務(wù)的(由于配置主庫(kù)數(shù)據(jù)源使用@primary注解,所有默認(rèn)會(huì)使用主庫(kù)的事務(wù)管理器),所以在數(shù)據(jù)源系統(tǒng)中使用聲明式事務(wù),必須指定事務(wù)管理器
上面代碼將兩個(gè)數(shù)據(jù)庫(kù)操作都放在同一個(gè)方法中,無(wú)論拿到了哪個(gè)事務(wù)管理器,只要 do something 3 處發(fā)生了異常,那么其中的一個(gè)事務(wù)是不會(huì)回滾的
改進(jìn)寫(xiě)法
@Service
public class TestService {
@Resource
JdbcTemplate primaryJdbcTemplate;
@Resource
JdbcTemplate secondaryJdbcTemplate;
@Transactional(value = "primaryTransactionManager")
public void method1() {
//do something 1
primaryJdbcTemplate.execute("insert into user(username, password) values('張三', '123456')");
//do something 2
method2();
//do something 5
}
@Transactional(value = "secondaryTransactionManager")
public void method2() {
//do something 3
secondaryJdbcTemplate.execute("insert into user(username, password) values('李四', '123456');");
//do something 4
}
}改進(jìn)的寫(xiě)法,將不同數(shù)據(jù)源的操作拆到不同的方法中,分別加上了@Transactional注解,并指定了對(duì)應(yīng)的事務(wù)管理器。這種寫(xiě)法相對(duì)之前的就規(guī)范了不少,但是還是存在問(wèn)題,如果在 do something 5 處發(fā)生了異常,因?yàn)?method2 方法已經(jīng)執(zhí)行結(jié)束了,事務(wù)已經(jīng)提交了,所以還是無(wú)法做到一起回滾。
編程式事務(wù)
@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 = primaryTransactionManager.getTransaction(primaryDef);
TransactionDefinition secondaryDef = new DefaultTransactionDefinition();
TransactionStatus secondaryStatus = secondaryTransactionManager.getTransaction(secondaryDef);
try {
//do something 1
primaryJdbcTemplate.execute("insert into user(username, password) values('張三', '123456')");
//do something 2
secondaryJdbcTemplate.execute("insert into user(username, password) values('李四', '123456');");
//do something 3
primaryTransactionManager.commit(primaryStatus);
secondaryTransactionManager.commit(secondaryStatus);
} catch (Exception e) {
primaryTransactionManager.rollback(primaryStatus);
secondaryTransactionManager.rollback(secondaryStatus);
throw new RuntimeException(e.getMessage());
}
}
}編程式事務(wù)的顆粒度時(shí)代碼級(jí)別的,可以嵌入到方法里面,這樣可以控制不同數(shù)據(jù)源的事務(wù)同時(shí)開(kāi)啟,一旦出現(xiàn)異常,則兩個(gè)事務(wù)一起回滾,這樣就保證了多數(shù)據(jù)事務(wù)的一致性。
這種實(shí)現(xiàn)實(shí)際上和分布式事務(wù)的XA模式思想一樣,只不過(guò)分布式事務(wù)管理的是分布式系統(tǒng)中不同服務(wù)不同的數(shù)據(jù)源,而這里是一個(gè)服務(wù)同一個(gè)方法中操作多個(gè)數(shù)據(jù)源。本質(zhì)上都是處理管理多數(shù)據(jù)源的事務(wù)。
到此這篇關(guān)于SpringBoot同一個(gè)方法操作多個(gè)數(shù)據(jù)源保證事務(wù)一致性的文章就介紹到這了,更多相關(guān)SpringBoot 事務(wù)一致性內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot中Controller參數(shù)與返回值的用法總結(jié)
這篇文章主要介紹了SpringBoot中Controller參數(shù)與返回值的用法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-07-07
java運(yùn)行windows的cmd命令簡(jiǎn)單代碼
這篇文章主要介紹了java運(yùn)行windows的cmd命令簡(jiǎn)單代碼,有需要的朋友可以參考一下2013-12-12
java ConcurrentHashMap分段加鎖提高并發(fā)效率
解決springcloud中Feign導(dǎo)入依賴為unknow的情況
SpringBoot版本升級(jí)容易遇到的一些問(wèn)題
Spring Cloud Alibaba Nacos Config進(jìn)階使用
基于JavaMail的Java實(shí)現(xiàn)復(fù)雜郵件發(fā)送功能
Java GUI圖形界面開(kāi)發(fā)實(shí)現(xiàn)小型計(jì)算器流程詳解

