一. 需求採集
有一個業務操作,需要同時對兩個數據庫執行DML操作,要麼都執行成功,要麼都不成功。
二. 需求分析
這是一個典型的分佈式事務應用場景,但以前只是聽說過分佈式事務,卻沒有親自動手實踐過。項目本來是使用的SpringBoot框架,因此拿“springboot 分佈式事務”作爲關鍵詞交給度娘,發現可參考的資料很多,心中竊喜,但仔細一看,絕大多數文章都使用了mybatis,mybatis雖盛行,但我們公司卻沒用過,咱用的是JPA,Hibernate實現的。所以覺得有必要爲基於JPA的分佈式事務實現方式留下點什麼,故有此一篇文章。
三.程序設計
- 首先,看看pom.xml的依賴
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- 數據庫 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.18</version>
</dependency>
<dependency>
<groupId>com.oracle.driver</groupId>
<artifactId>jdbc-driver</artifactId>
<version>11g</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpmime</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.16</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.0</version>
</dependency>
<!--jta+atomokos進行分佈式事務管理需要用到的包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
- 然後是application.properties關於兩個數據源的配置:
#源庫數據源
spring.datasource.url=jdbc:oracle:thin:@127.0.0.1:1521/orcl
spring.datasource.username=aditest
spring.datasource.password=testpassone
spring.datasource.driverClassName = oracle.jdbc.OracleDriver
#中間庫數據源
spring.datasource.business.url=jdbc:oracle:thin:@127.0.0.1:1521/orcl
spring.datasource.business.username=python
spring.datasource.business.password=testpasstwo
spring.datasource.business.driverClassName = oracle.jdbc.OracleDriver
#####以下爲數據源連接的拓展配置屬性包含連接池等開始#####
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.initialSize=5
spring.datasource.minIdle=5
spring.datasource.maxActive=50
spring.datasource.maxWait=60000
#####以上爲數據源連接的拓展配置屬性結束#####
- 在config包(也可以是其他包名)下創建第一個數據源的配置類ClientDBConfig.class
package com.bitservice.adi.client.config;
import java.sql.SQLException;
import java.util.Properties;
import javax.persistence.EntityManager;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import com.alibaba.druid.pool.xa.DruidXADataSource;
import com.atomikos.jdbc.AtomikosDataSourceBean;
@Configuration
//basePackages:設置Repository所在的包
//entityManagerFactoryRef:設置實體管理工廠
//transactionManagerRef:設置事務管理器
@EnableJpaRepositories(basePackages = "com.bitservice.adi.client.dao", entityManagerFactoryRef = "entityManagerFactory", transactionManagerRef = "transactionManager")
public class ClientDBConfig {
// 數據庫連接信息
@Value("${spring.datasource.url}")
private String dbUrl;
@Value("${spring.datasource.username}")
private String username;
@Value("${spring.datasource.password}")
private String password;
@Value("${spring.datasource.driverClassName}")
private String driverClassName;
// 連接池連接信息
@Value("${spring.datasource.initialSize}")
private int initialSize;
@Value("${spring.datasource.minIdle}")
private int minIdle;
@Value("${spring.datasource.maxActive}")
private int maxActive;
@Value("${spring.datasource.maxWait}")
private int maxWait;
@Bean // 聲明其爲Bean實例
@Primary // 在同樣的DataSource中,首先使用被標註的DataSource
@Qualifier("dataSource")
public DataSource dataSource() throws SQLException {
DruidXADataSource druidXADataSource = new DruidXADataSource();
// 基礎連接信息
druidXADataSource.setUrl(this.dbUrl);
druidXADataSource.setUsername(username);
druidXADataSource.setPassword(password);
druidXADataSource.setDriverClassName(driverClassName);
// 連接池連接信息
druidXADataSource.setInitialSize(initialSize);
druidXADataSource.setMinIdle(minIdle);
druidXADataSource.setMaxActive(maxActive);
druidXADataSource.setMaxWait(maxWait);
druidXADataSource.setPoolPreparedStatements(true); // 是否緩存preparedStatement,也就是PSCache。PSCache對支持遊標的數據庫性能提升巨大,比如說oracle。在mysql下建議關閉。
druidXADataSource.setMaxPoolPreparedStatementPerConnectionSize(50);
druidXADataSource.setConnectionProperties("oracle.net.CONNECT_TIMEOUT=6000;oracle.jdbc.ReadTimeout=60000");// 對於耗時長的查詢sql,會受限於ReadTimeout的控制,單位毫秒
druidXADataSource.setTestOnBorrow(true); // 申請連接時執行validationQuery檢測連接是否有效,這裏建議配置爲TRUE,防止取到的連接不可用
druidXADataSource.setTestWhileIdle(true);// 建議配置爲true,不影響性能,並且保證安全性。申請連接的時候檢測,如果空閒時間大於timeBetweenEvictionRunsMillis,執行validationQuery檢測連接是否有效。
String validationQuery = "select 1 from dual";
druidXADataSource.setValidationQuery(validationQuery); // 用來檢測連接是否有效的sql,要求是一個查詢語句。如果validationQuery爲null,testOnBorrow、testOnReturn、testWhileIdle都不會起作用。
druidXADataSource.setFilters("stat,wall");// 屬性類型是字符串,通過別名的方式配置擴展插件,常用的插件有:監控統計用的filter:stat日誌用的filter:log4j防禦sql注入的filter:wall
druidXADataSource.setTimeBetweenEvictionRunsMillis(60000); // 配置間隔多久才進行一次檢測,檢測需要關閉的空閒連接,單位是毫秒
druidXADataSource.setMinEvictableIdleTimeMillis(180000); // 配置一個連接在池中最小生存的時間,單位是毫秒,這裏配置爲3分鐘180000
druidXADataSource.setKeepAlive(true); // 打開druid.keepAlive之後,當連接池空閒時,池中的minIdle數量以內的連接,空閒時間超過minEvictableIdleTimeMillis,則會執行keepAlive操作,即執行druid.validationQuery指定的查詢SQL,一般爲select
// * from
// dual,只要minEvictableIdleTimeMillis設置的小於防火牆切斷連接時間,就可以保證當連接空閒時自動做保活檢測,不會被防火牆切斷
druidXADataSource.setRemoveAbandoned(true); // 是否移除泄露的連接/超過時間限制是否回收。
druidXADataSource.setRemoveAbandonedTimeout(3600); // 泄露連接的定義時間(要超過最大事務的處理時間);單位爲秒。這裏配置爲1小時
druidXADataSource.setLogAbandoned(true); //// 移除泄露連接發生是是否記錄日誌
AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
xaDataSource.setXaDataSource(druidXADataSource);
xaDataSource.setUniqueResourceName("dataSource");
return xaDataSource;
}
@Bean(name = "entityManagerFactory")
@Qualifier("entityManagerFactory")
@Primary
public LocalContainerEntityManagerFactoryBean entityManageFactory(EntityManagerFactoryBuilder builder)
throws SQLException {
LocalContainerEntityManagerFactoryBean entityManagerFactory = builder.dataSource(dataSource())
.packages("com.bitservice.adi.client.entity").persistenceUnit("clientPersistenceUnit").build();
Properties jpaProperties = new Properties();
jpaProperties.put("hibernate.dialect", "org.hibernate.dialect.Oracle10gDialect");
jpaProperties.put("hibernate.physical_naming_strategy",
"org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy");
jpaProperties.put("hibernate.connection.charSet", "utf-8");
jpaProperties.put("hibernate.ddl-auto", "update");
jpaProperties.put("hibernate.show_sql", "true");
entityManagerFactory.setJpaProperties(jpaProperties);
return entityManagerFactory;
}
@Bean(name = "entityManager")
@Qualifier("entityManager")
@Primary
public EntityManager entityManager(EntityManagerFactoryBuilder builder) throws SQLException {
return entityManageFactory(builder).getObject().createEntityManager();
}
@Bean(name = "namedParameterJdbcTemplate")
@Qualifier("namedParameterJdbcTemplate")
@Primary
public NamedParameterJdbcTemplate jdbcTemplate(@Qualifier("dataSource") DataSource dataSource) {
return new NamedParameterJdbcTemplate(dataSource);
}
/**
* 返回數據源的事務管理器
*
* @param builder
* @return
* @throws SQLException
*/
@Bean(name = "transactionManager")
@Qualifier("transactionManager")
@Primary
public PlatformTransactionManager transactionManager(EntityManagerFactoryBuilder builder) throws SQLException {
return new JpaTransactionManager(entityManageFactory(builder).getObject());
}
}
4.在config包(也可以是其他包名)下創建第二個數據源的配置類BusinessDBConfig.class
package com.bitservice.adi.client.config;
import java.sql.SQLException;
import java.util.Properties;
import javax.persistence.EntityManager;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import com.alibaba.druid.pool.xa.DruidXADataSource;
import com.atomikos.jdbc.AtomikosDataSourceBean;
@Configuration
//basePackages:設置Repository所在的包
//entityManagerFactoryRef:設置實體管理工廠
//transactionManagerRef:設置事務管理器
@EnableJpaRepositories(basePackages = "com.bitservice.adi.business.dao", entityManagerFactoryRef = "businessEntityManagerFactory", transactionManagerRef = "businessTtransactionManager")
public class BusinessDBConfig {
// 中間庫連接信息
@Value("${spring.datasource.business.url}")
private String businessDbUrl;
@Value("${spring.datasource.business.username}")
private String businessUsername;
@Value("${spring.datasource.business.password}")
private String businessPassword;
@Value("${spring.datasource.business.driverClassName}")
private String businessDriverClassName;
// 連接池連接信息
@Value("${spring.datasource.initialSize}")
private int initialSize;
@Value("${spring.datasource.minIdle}")
private int minIdle;
@Value("${spring.datasource.maxActive}")
private int maxActive;
@Value("${spring.datasource.maxWait}")
private int maxWait;
@Bean(name = "businessDataSource")
@Qualifier("businessDataSource")
public DataSource businessDataSource() throws SQLException {
DruidXADataSource druidXADataSource = new DruidXADataSource();
// 基礎連接信息
druidXADataSource.setUrl(businessDbUrl);
druidXADataSource.setUsername(businessUsername);
druidXADataSource.setPassword(businessPassword);
druidXADataSource.setDriverClassName(businessDriverClassName);
// 連接池連接信息
druidXADataSource.setInitialSize(initialSize);
druidXADataSource.setMinIdle(minIdle);
druidXADataSource.setMaxActive(maxActive);
druidXADataSource.setMaxWait(maxWait);
druidXADataSource.setPoolPreparedStatements(true); // 是否緩存preparedStatement,也就是PSCache。PSCache對支持遊標的數據庫性能提升巨大,比如說oracle。在mysql下建議關閉。
druidXADataSource.setMaxPoolPreparedStatementPerConnectionSize(50);
druidXADataSource.setConnectionProperties("oracle.net.CONNECT_TIMEOUT=6000;oracle.jdbc.ReadTimeout=60000");// 對於耗時長的查詢sql,會受限於ReadTimeout的控制,單位毫秒
druidXADataSource.setTestOnBorrow(true); // 申請連接時執行validationQuery檢測連接是否有效,這裏建議配置爲TRUE,防止取到的連接不可用
druidXADataSource.setTestWhileIdle(true);// 建議配置爲true,不影響性能,並且保證安全性。申請連接的時候檢測,如果空閒時間大於timeBetweenEvictionRunsMillis,執行validationQuery檢測連接是否有效。
String validationQuery = "select 1 from dual";
druidXADataSource.setValidationQuery(validationQuery); // 用來檢測連接是否有效的sql,要求是一個查詢語句。如果validationQuery爲null,testOnBorrow、testOnReturn、testWhileIdle都不會起作用。
druidXADataSource.setFilters("stat,wall");// 屬性類型是字符串,通過別名的方式配置擴展插件,常用的插件有:監控統計用的filter:stat日誌用的filter:log4j防禦sql注入的filter:wall
druidXADataSource.setTimeBetweenEvictionRunsMillis(60000); // 配置間隔多久才進行一次檢測,檢測需要關閉的空閒連接,單位是毫秒
druidXADataSource.setMinEvictableIdleTimeMillis(180000); // 配置一個連接在池中最小生存的時間,單位是毫秒,這裏配置爲3分鐘180000
druidXADataSource.setKeepAlive(true); // 打開druid.keepAlive之後,當連接池空閒時,池中的minIdle數量以內的連接,空閒時間超過minEvictableIdleTimeMillis,則會執行keepAlive操作,即執行druid.validationQuery指定的查詢SQL,一般爲select
// * from
// dual,只要minEvictableIdleTimeMillis設置的小於防火牆切斷連接時間,就可以保證當連接空閒時自動做保活檢測,不會被防火牆切斷
druidXADataSource.setRemoveAbandoned(true); // 是否移除泄露的連接/超過時間限制是否回收。
druidXADataSource.setRemoveAbandonedTimeout(3600); // 泄露連接的定義時間(要超過最大事務的處理時間);單位爲秒。這裏配置爲1小時
druidXADataSource.setLogAbandoned(true); //// 移除泄露連接發生是是否記錄日誌
AtomikosDataSourceBean xaDataSource=new AtomikosDataSourceBean();
xaDataSource.setXaDataSource(druidXADataSource);
xaDataSource.setUniqueResourceName("businessDataSource");
return xaDataSource;
}
@Bean(name = "businessEntityManagerFactory")
@Qualifier("businessEntityManagerFactory")
public LocalContainerEntityManagerFactoryBean entityManageFactory(EntityManagerFactoryBuilder builder)
throws SQLException {
LocalContainerEntityManagerFactoryBean entityManagerFactory = builder.dataSource(businessDataSource())
.packages("com.bitservice.adi.business.entity").persistenceUnit("businessPersistenceUnit").build();
Properties jpaProperties = new Properties();
jpaProperties.put("hibernate.dialect", "org.hibernate.dialect.Oracle10gDialect");
jpaProperties.put("hibernate.physical_naming_strategy",
"org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy");
jpaProperties.put("hibernate.connection.charSet", "utf-8");
jpaProperties.put("hibernate.ddl-auto", "update");
jpaProperties.put("hibernate.show_sql", "true");
entityManagerFactory.setJpaProperties(jpaProperties);
return entityManagerFactory;
}
@Bean(name = "businessEntityManager")
@Qualifier("businessEntityManager")
public EntityManager entityManager(EntityManagerFactoryBuilder builder) throws SQLException {
return entityManageFactory(builder).getObject().createEntityManager();
}
@Bean(name = "businessJdbcTemplate")
@Qualifier("businessJdbcTemplate")
public NamedParameterJdbcTemplate businessJdbcTemplate(@Qualifier("businessDataSource") DataSource dataSource) {
return new NamedParameterJdbcTemplate(dataSource);
}
/**
* 返回數據源的事務管理器
* @param builder
* @return
* @throws SQLException
*/
@Bean(name = "businessTransactionManager")
@Qualifier("businessTransactionManager")
public PlatformTransactionManager transactionManager(EntityManagerFactoryBuilder builder) throws SQLException {
return new JpaTransactionManager(entityManageFactory(builder).getObject());
}
}
請注意,兩個數據源對應的dao和entity不能放在同一個包路徑下,例如我這裏,數據源一的dao在com.bitservice.adi.client.dao下,數據源二的dao在com.bitservice.adi.business.dao下,entity同理。
- 在config包(也可以是其他包名)下創建Jta事務管理配置類JtaTransactionManagerConfig.class,
要注意設置分佈式事務的超時時間,默認是10秒鐘。
package com.bitservice.adi.client.config;
import javax.transaction.SystemException;
import javax.transaction.UserTransaction;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.transaction.jta.JtaTransactionManager;
import com.atomikos.icatch.jta.UserTransactionImp;
import com.atomikos.icatch.jta.UserTransactionManager;
/**
* Jta事務管理配置類
* @author lx
*
* 2019年9月6日
*
*/
@Configuration
public class JtaTransactionManagerConfig {
@Bean(name = "jtaTransactionManager")
@Primary
public JtaTransactionManager regTransactionManager() throws SystemException {
UserTransactionManager userTransactionManager = new UserTransactionManager();
userTransactionManager.setTransactionTimeout(600); //設置整個事務的超時時間,單位秒
UserTransaction userTransaction = new UserTransactionImp();
return new JtaTransactionManager(userTransaction, userTransactionManager);
}
}
- 創建數據源一的dao類ClientRepository
package com.bitservice.adi.client.dao;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class ClientRepository {
@Autowired
@Qualifier("namedParameterJdbcTemplate")
private NamedParameterJdbcTemplate jdbcTemplate;
public int update(Map<String, Object> params) {
String sql = "UPDATE ADI_IDESCARTES_SYNLOGS xe SET xe.synflag_shuiwu = 1 WHERE xe.ywbh in (:ywbhs)";
int result = jdbcTemplate.update(sql, params);
return result;
}
}
- 創建數據源二的dao類BusinessRepository
package com.bitservice.adi.business.dao;
import java.util.List;
import java.util.Map;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class BusinessRepository {
private final Logger logger = Logger.getLogger(getClass());
@Autowired
@Qualifier("businessJdbcTemplate")
private NamedParameterJdbcTemplate jdbcTemplate;
/**
*
* @param hths 準備刪除的合同編號
* @param jyfcount 預計要刪除交易方數量
* @param params
*/
public void delDbData(List<String> hths, Map<String, Object> params) {
int htcount = hths.size();
String del_fcjycjxx_sql = "delete from SB_ZJB_FCJYCJXX where htbh in :htbhs";
String del_jyf_sql = "delete from SB_ZJB_FCJYCJXX_JYF where fcjycjxxuuid in (select fcjycjxxuuid from SB_ZJB_FCJYCJXX cjxx where cjxx.htbh in :htbhs)";
int a = jdbcTemplate.update(del_jyf_sql, params); //先刪除交易方數據
logger.info("刪除交易方數量:"+a);
int b = jdbcTemplate.update(del_fcjycjxx_sql, params); //再刪除交易信息
if(b != htcount) {
throw new RuntimeException("預計刪除的合同數量("+htcount+")與實際刪除的合同數量("+b+")不匹配");
}
logger.info("刪除合同數量:"+b);
}
}
- 把以上兩個dao注入到你的Service類中,並在你的Service方法中分別調用兩個dao的方法,唯一要注意的一點,Service的事務註解要像下面這樣寫
package com.bitservice.adi.business.dao;
import java.util.List;
import java.util.Map;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Repository;
@Service
@Transactional(value="jtaTransactionManager")
public class DataSynService{
}
Service的具體業務邏輯以及controller我就不寫了。
- SpringBoot啓動類。要排除掉默認的DataSourceAutoConfiguration和DataSourceTransactionManagerAutoConfiguration
package com.bitservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class,
DataSourceTransactionManagerAutoConfiguration.class })
// 開啓手動配置
@EnableTransactionManagement
// @SpringBootApplication
@EnableScheduling // 定時任務
public class ADIClientApplication {
public static void main(String[] args) {
SpringApplication.run(ADIClientApplication.class, args);
}
}
我在測試的時候,是通過手動拋出運行時異常測試的,無論是在數據源一的dao拋異常,還是在數據源二的dao拋異常,或者是在service中拋出異常,事務都正常回滾了!
謝謝閱讀,如果疑問,請給我留言。