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.MySQL5InnoDBDialect
Config
這里主要注入主庫(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ì)按類(lèi)型在容器中查找事務(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-07java運(yùn)行windows的cmd命令簡(jiǎn)單代碼
這篇文章主要介紹了java運(yùn)行windows的cmd命令簡(jiǎn)單代碼,有需要的朋友可以參考一下2013-12-12

java ConcurrentHashMap分段加鎖提高并發(fā)效率

解決springcloud中Feign導(dǎo)入依賴(lài)為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ì)算器流程詳解