SpringBoot+JPA+Druid+Oracle+jta+atomikos實現分佈式事務

一. 需求採集

有一個業務操作,需要同時對兩個數據庫執行DML操作,要麼都執行成功,要麼都不成功。

二. 需求分析

這是一個典型的分佈式事務應用場景,但以前只是聽說過分佈式事務,卻沒有親自動手實踐過。項目本來是使用的SpringBoot框架,因此拿“springboot 分佈式事務”作爲關鍵詞交給度娘,發現可參考的資料很多,心中竊喜,但仔細一看,絕大多數文章都使用了mybatis,mybatis雖盛行,但我們公司卻沒用過,咱用的是JPA,Hibernate實現的。所以覺得有必要爲基於JPA的分佈式事務實現方式留下點什麼,故有此一篇文章。

三.程序設計

  1. 首先,看看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>
  1. 然後是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
#####以上爲數據源連接的拓展配置屬性結束#####
  1. 在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同理。

  1. 在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);
	}
}
  1. 創建數據源一的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;
	}
}
  1. 創建數據源二的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);
	}	
}
  1. 把以上兩個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我就不寫了。

  1. 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中拋出異常,事務都正常回滾了!
謝謝閱讀,如果疑問,請給我留言。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章