1.多數(shù)據(jù)源事務管控
????之前寫了一篇基于注解來動態(tài)切換數(shù)據(jù)源的demo琉历,但是那個demo是不支持多數(shù)據(jù)源的事務的且改,也就是說在執(zhí)行多數(shù)據(jù)源數(shù)據(jù)改動
操作的時候幽纷,如果其中某個數(shù)據(jù)源發(fā)生異常渴杆,之前操作的數(shù)據(jù)源的事務已經(jīng)提交不會回滾挠羔,只有發(fā)生異常的數(shù)據(jù)庫才會回滾事務墓陈,這就導致了事務的不一致性恶守,這里我參考了網(wǎng)上的大量文章,在基于注解動態(tài)切換數(shù)據(jù)源的基礎上改進贡必,使用atomikos分布式事務
來對多個數(shù)據(jù)源資源的事務進行管控兔港,從而實現(xiàn)多數(shù)據(jù)源的事務一致性
2 項目搭建
-
2.1 pom依賴和yml配置
-
2.1.1 pom.xml
pom依賴和上篇文章差不多,只是額外添加了atomikos的依賴仔拟,以及apache下的commons-lang3的jar包依賴衫樊,mysql的版本需要使用6.0.6,版本高了利花,啟動會報錯
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!--<parent>-->
<!--<groupId>org.springframework.boot</groupId>-->
<!--<artifactId>spring-boot-starter-parent</artifactId>-->
<!--<version>2.2.1.RELEASE</version>-->
<!--<relativePath/> <!– lookup parent from repository –>-->
<!--</parent>-->
<groupId>com.sccl</groupId>
<artifactId>data_source_change</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>data_source_change</name>
<description>Demo project for Spring Boot</description>
<!--不繼承spring-boot-starter-parent科侈,使用依賴管理-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.1.6.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- SpringBoot Web容器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- SpringBoot 攔截器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- SpringBoot集成mybatis框架 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<!--可以不使用mybatis-plus中的會話工廠來完成數(shù)據(jù)源切換了,故而注釋此依賴-->
<!--<dependency>-->
<!--<groupId>com.baomidou</groupId>-->
<!--<artifactId>mybatis-plus-boot-starter</artifactId>-->
<!--<version>3.1.0</version>-->
<!--</dependency>-->
<!--阿里數(shù)據(jù)庫連接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!-- Mysql驅(qū)動包 這里請使用6.0.6版本的mysql,版本高了會報錯-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>6.0.6</version>
<!--<scope>runtime</scope>-->
</dependency>
<!--atomikos分布式事務-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
<!--防止@ConfigurationProperties屬性注入爆錯炒事,引入此依賴-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
-
2.1.2 yml文件
yml文件中臀栈,主要改動是將type改成xa類型的數(shù)據(jù)源,這樣才能讓多個數(shù)據(jù)源資源被atomoikos管理
server:
port: 8099
servlet:
context-path: /data
spring:
datasource:
druid:
# 注意(名稱不支持大寫和下劃線可用中橫線 比如 錯誤 的命名(slave_**, slaveTwo))
master: #主庫(數(shù)據(jù)源-1)
type: com.alibaba.druid.pool.xa.DruidXADataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/chapter05-1?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowMultiQueries=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
slave: #從庫(數(shù)據(jù)源-2)
open: true
type: com.alibaba.druid.pool.xa.DruidXADataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/chapter05-2?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowMultiQueries=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
#jta相關參數(shù)配置
jta:
log-dir: classpath:tx-logs
transaction-manager-id: txManager
#mybatis的配置在會話工廠里面配置挠乳,在這里配置會報錯
#mybatis:
# type-aliases-package: com.sccl.data_source_change.*.domain #包別名
# mapper-locations: classpath*:mybatis/**/*.xml #掃描mapper映射文件
-
2.2 項目代碼重構
項目目錄:
-
2.2.1 自定義注解和切面
代碼基本沒改動
DataSource自定義注解
package com.sccl.data_source_change.aspectj.annotation;
import com.sccl.data_source_change.enumConst.DataSourceEnum;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**自定義多數(shù)據(jù)源切換注解
* Create by wangbin
* 2019-11-18-15:25
*/
/**
* 注解說明:
* @author wangbin
* @date 2019/11/18 15:36
源碼樣例:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface MthCache {
String key();
}
@Target 注解
功能:指明了修飾的這個注解的使用范圍权薯,即被描述的注解可以用在哪里。
ElementType的取值包含以下幾種:
TYPE:類睡扬,接口或者枚舉
FIELD:域盟蚣,包含枚舉常量
METHOD:方法
PARAMETER:參數(shù)
CONSTRUCTOR:構造方法
LOCAL_VARIABLE:局部變量
ANNOTATION_TYPE:注解類型
PACKAGE:包
=======================================================================================
@Retention 注解
功能:指明修飾的注解的生存周期,即會保留到哪個階段威蕉。
RetentionPolicy的取值包含以下三種:
SOURCE:源碼級別保留刁俭,編譯后即丟棄。
CLASS:編譯級別保留韧涨,編譯后的class文件中存在牍戚,在jvm運行時丟棄,這是默認值虑粥。
RUNTIME: 運行級別保留如孝,編譯后的class文件中存在,在jvm運行時保留娩贷,可以被反射調(diào)用第晰。
====================================================================================
@Documented 注解
功能:指明修飾的注解,可以被例如javadoc此類的工具文檔化,只負責標記茁瘦,沒有成員取值品抽。
========================================================================================
@Inherited注解
功能:允許子類繼承父類中的注解。
注意L鹑邸:
@interface意思是聲明一個注解圆恤,方法名對應參數(shù)名,返回值類型對應參數(shù)類型腔稀。
*/
@Target(ElementType.METHOD) //此注解使用于方法上
@Retention(RetentionPolicy.RUNTIME) //此注解的生命周期為:運行時盆昙,在編譯后的class文件中存在,在jvm運行時保留焊虏,可以被反射調(diào)用
public @interface DataSource {
/**
* 切換數(shù)據(jù)源值
*/
DataSourceEnum value() default DataSourceEnum.MASTER;
}
DsAspect數(shù)據(jù)源動態(tài)切換的切面
package com.sccl.data_source_change.aspectj;
import com.sccl.data_source_change.aspectj.annotation.DataSource;
import com.sccl.data_source_change.datasource.DynamicDataSourceContextHolder;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* 多數(shù)據(jù)源處理切面
* 事務管理:
* 事務管理在開啟時淡喜,需要確定數(shù)據(jù)源,也就是說數(shù)據(jù)源切換要在事務開啟之前诵闭,
* 我們可以使用Order來配置執(zhí)行順序炼团,在AOP實現(xiàn)類上加Order注解,
* 就可以使數(shù)據(jù)源切換提前執(zhí)行涂圆,order值越小们镜,執(zhí)行順序越靠前。
* Create by wangbin
* 2019-11-18-15:55
*/
@Aspect
@Order(1) //order值越小润歉,執(zhí)行順序越靠前。<!-- 設置切換數(shù)據(jù)源的優(yōu)先級 -->
@Component
public class DsAspect {
protected Logger logger = LoggerFactory.getLogger(getClass());
/**
* 所有添加了DataSource自定義注解的方法都進入切面
*/
@Pointcut("@annotation(com.sccl.data_source_change.aspectj.annotation.DataSource)")
public void dsPointCut() {
}
// 這里使用@Around颈抚,在調(diào)用目標方法前踩衩,進行aop攔截,通過解析注解上的值來切換數(shù)據(jù)源贩汉。
// 在調(diào)用方法結束后驱富,清除數(shù)據(jù)源。
// 也可以使用@Before和@After來編寫匹舞,原理一樣褐鸥,這里就不多說了。
@Around("dsPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
if (method.isAnnotationPresent(DataSource.class)) {
//獲取方法上的注解
DataSource dataSource = method.getAnnotation(DataSource.class);
if (dataSource != null) {
//切換數(shù)據(jù)源
DynamicDataSourceContextHolder.setDB(dataSource.value().getName());
}
}
try {
return point.proceed();
} finally {
// 銷毀數(shù)據(jù)源 在執(zhí)行方法之后
DynamicDataSourceContextHolder.clearDB();
}
}
}
-
2.2.2 數(shù)據(jù)源枚舉
枚舉 DataSourceEnum
package com.sccl.data_source_change.enumConst;
/**
* Create by wangbin
* 2019-11-19-16:54
*/
public enum DataSourceEnum {
MASTER("master"),
SLAVE("slave");
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
DataSourceEnum(String name) {
this.name = name;
}
}
-
2.2.3 動態(tài)數(shù)據(jù)源和動態(tài)數(shù)據(jù)源環(huán)境變量
DynamicDataSource動態(tài)數(shù)據(jù)源
package com.sccl.data_source_change.datasource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import javax.sql.DataSource;
import java.util.Map;
/** 動態(tài)數(shù)據(jù)源
* Create by wangbin
* 2019-11-18-16:06
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources){
super.setDefaultTargetDataSource(defaultTargetDataSource);
super.setTargetDataSources(targetDataSources);
super.afterPropertiesSet();
}
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDB();
}
}
DynamicDataSourceContextHolder動態(tài)數(shù)據(jù)源環(huán)境變量控制
package com.sccl.data_source_change.datasource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** 當前線程數(shù)據(jù)源,負責管理數(shù)據(jù)源的環(huán)境變量
* Create by wangbin
* 2019-11-18-16:11
*/
public class DynamicDataSourceContextHolder {
public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);
/**
* 使用ThreadLocal維護變量赐稽,ThreadLocal為每個使用該變量的線程提供獨立的變量副本叫榕,
* 所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應的副本姊舵。
*/
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
/**
* 設置數(shù)據(jù)源名
*/
public static void setDB(String dbType){
log.info("切換到{}數(shù)據(jù)源", dbType);
CONTEXT_HOLDER.set(dbType);
}
/**
* 獲取數(shù)據(jù)源名
*/
public static String getDB(){
return CONTEXT_HOLDER.get();
}
/**
* 清理數(shù)據(jù)源名
*/
public static void clearDB(){
CONTEXT_HOLDER.remove();
}
}
-
2.2.4 數(shù)據(jù)源配置晰绎,分布式事務管理器,多數(shù)據(jù)源事務管理器括丁,重寫的mybatis會話工廠
數(shù)據(jù)源配置 DruidMutilConfig(項目代碼的主要改動位置)
在數(shù)據(jù)源配置中荞下,我們需要將master和slave數(shù)據(jù)庫的druid數(shù)據(jù)庫驅(qū)動換成xa的,同時要使用動態(tài)數(shù)據(jù)源來創(chuàng)建會話連接,這里和網(wǎng)上很多代碼的不同之處在于尖昏,網(wǎng)上很多文章都是給每個數(shù)據(jù)源單獨創(chuàng)建一個連接會話仰税,然后進行切換和事務管理,同時還需要分包抽诉,一個數(shù)據(jù)源就要分一個包陨簇,這里只使用動態(tài)數(shù)據(jù)源來創(chuàng)建會話連接,切換到哪個數(shù)據(jù)源的時候就用該數(shù)據(jù)源來獲取連接并管控事務很靈活掸鹅,
采坑經(jīng)歷:
1.在這個配置中加入事務后動態(tài)數(shù)據(jù)源沒法切換塞帐,需要重寫Transaction,讓我們能夠動態(tài)的根據(jù)DatabaseType獲取不同的Connection巍沙,并且要求不能影響整個事物的特性葵姥。
詳情參考了:
springboot+mybatis解決多數(shù)據(jù)源切換事務控制不生效的問題
2.在yml文件中配置的mybatis掃描xml包路徑和配置包別名,不起作用句携,所以在mybatis的會話工廠中配置的這兩個屬性榔幸,這里又有一個坑是mybatis的會話工廠不支持通配符配置包別名,所以參考了網(wǎng)上的文章矮嫉,寫一個類繼承mybatis的會話工廠重寫了配置包別名的方法削咆,然后使用這個類來配置包別名和xml路徑以及數(shù)據(jù)源和多數(shù)據(jù)源事務管理器
詳情參考了:
typeAliasesPackage支持通配符包路徑掃描
package com.sccl.data_source_change.config;
import com.sccl.data_source_change.datasource.DynamicDataSource;
import com.sccl.data_source_change.enumConst.DataSourceEnum;
import com.sccl.data_source_change.utils.PackagesSqlSessionFactoryBean;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.Environment;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.lang.Nullable;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
/**
* druid 配置多數(shù)據(jù)源
*
* @author sccl
*/
@Configuration
@EnableTransactionManagement //開啟事務
@MapperScan("com.sccl.data_source_change.**.mapper")
public class DruidMutilConfig {
@Bean(name = "masterDataSource")
public DataSource masterDataSource(Environment env) {
String sourceName = "master";
Properties prop = build(env, "spring.datasource.druid.master.");
AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
//druid的數(shù)據(jù)庫驅(qū)動換成xa的
xaDataSource.setXaDataSourceClassName("com.alibaba.druid.pool.xa.DruidXADataSource");
xaDataSource.setUniqueResourceName(sourceName);
xaDataSource.setPoolSize(5);
xaDataSource.setXaProperties(prop);
return xaDataSource;
}
@Bean(name = "slaveDataSource")
public DataSource slaveDataSource(Environment env) {
String sourceName = "slave";
Properties prop = build(env, "spring.datasource.druid.slave.");
AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
//druid的數(shù)據(jù)庫驅(qū)動換成xa的
xaDataSource.setXaDataSourceClassName("com.alibaba.druid.pool.xa.DruidXADataSource");
xaDataSource.setUniqueResourceName(sourceName);
xaDataSource.setPoolSize(5);
xaDataSource.setXaProperties(prop);
return xaDataSource;
}
private Properties build(Environment env, String prefix) {
Properties prop = new Properties();
prop.put("url", env.getProperty(prefix + "url"));
prop.put("username", env.getProperty(prefix + "username"));
prop.put("password", env.getProperty(prefix + "password"));
prop.put("driverClassName", env.getProperty(prefix + "driverClassName", ""));
//這里只設置了簡單的幾個屬性,如果想做更多的配置可以繼續(xù)往下添加即可
return prop;
}
/**
* 動態(tài)數(shù)據(jù)源,在這繼續(xù)添加 DataSource Bean
*/
@Bean(name = "dynamicDataSource")
@Primary
public DynamicDataSource dataSource(@Qualifier("masterDataSource") DataSource masterDataSource, @Nullable @Qualifier("slaveDataSource") DataSource slaveDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DataSourceEnum.MASTER.getName(), masterDataSource);
if (slaveDataSource != null){
targetDataSources.put(DataSourceEnum.SLAVE.getName(), slaveDataSource);
}
// 還有數(shù)據(jù)源,在targetDataSources中繼續(xù)添加
return new DynamicDataSource(masterDataSource, targetDataSources);
}
@Bean(name = "sqlSessionFactory")
@Primary
public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dataSource)
throws Exception {
//參照的別人的代碼說需要將會話工廠改成mybatis-plus的sql會話工廠蠢笋,
//經(jīng)測試發(fā)現(xiàn)使用mybatis的會話工廠也可以運行拨齐,不會報錯
// MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
//使用了PackagesSqlSessionFactoryBean繼承SqlSessionFactoryBean,重寫了配置別名的方法
PackagesSqlSessionFactoryBean bean = new PackagesSqlSessionFactoryBean();
bean.setDataSource(dataSource);
//設置多數(shù)據(jù)源分布式事務
bean.setTransactionFactory(new MultiDataSourceTransactionFactory());
bean.setTypeAliasesPackage("com.sccl.data_source_change.*.domain");//通配符設置包別名
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mybatis/**/*.xml"));// 掃描指定目錄的xml
return bean.getObject();
}
@Bean(name = "sqlSessionTemplate")
@Primary
public SqlSessionTemplate sqlSessionTemplate(
@Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
多數(shù)據(jù)源事務管理器和相應的工廠(參考別人的代碼)
MultiDataSourceTransaction昨寞,MultiDataSourceTransactionFactory
MultiDataSourceTransaction
package com.sccl.data_source_change.config;
import com.alibaba.druid.support.logging.Log;
import com.alibaba.druid.support.logging.LogFactory;
import com.sccl.data_source_change.datasource.DynamicDataSource;
import com.sccl.data_source_change.datasource.DynamicDataSourceContextHolder;
import org.apache.ibatis.transaction.Transaction;
import org.springframework.jdbc.CannotGetJdbcConnectionException;
import org.springframework.jdbc.datasource.DataSourceUtils;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import static org.apache.commons.lang3.Validate.notNull;
/**
* <P>多數(shù)據(jù)源切換瞻惋,支持事務</P>
*
* @author lishuangqi
* @date 2019/5/16 15:09
* @since
*/
public class MultiDataSourceTransaction implements Transaction {
private static final Log LOGGER = LogFactory.getLog(MultiDataSourceTransaction.class);
private final DataSource dataSource;
private Connection mainConnection;
private String mainDatabaseIdentification;
private ConcurrentMap<String, Connection> otherConnectionMap;
private boolean isConnectionTransactional;
private boolean autoCommit;
public MultiDataSourceTransaction(DataSource dataSource) {
notNull(dataSource, "No DataSource specified");
this.dataSource = dataSource;
otherConnectionMap = new ConcurrentHashMap<>();
mainDatabaseIdentification= DynamicDataSourceContextHolder.getDB();
}
/**
* {@inheritDoc}
*/
@Override
public Connection getConnection() throws SQLException {
String databaseIdentification = DynamicDataSourceContextHolder.getDB();
if (databaseIdentification.equals(mainDatabaseIdentification)) {
if (mainConnection != null) return mainConnection;
else {
openMainConnection();
mainDatabaseIdentification =databaseIdentification;
return mainConnection;
}
} else {
if (!otherConnectionMap.containsKey(databaseIdentification)) {
try {
Connection conn = dataSource.getConnection();
otherConnectionMap.put(databaseIdentification, conn);
} catch (SQLException ex) {
throw new CannotGetJdbcConnectionException("Could not get JDBC Connection", ex);
}
}
return otherConnectionMap.get(databaseIdentification);
}
}
private void openMainConnection() throws SQLException {
this.mainConnection = DataSourceUtils.getConnection(this.dataSource);
this.autoCommit = this.mainConnection.getAutoCommit();
this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.mainConnection, this.dataSource);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(
"JDBC Connection ["
+ this.mainConnection
+ "] will"
+ (this.isConnectionTransactional ? " " : " not ")
+ "be managed by Spring");
}
}
/**
* {@inheritDoc}
*/
@Override
public void commit() throws SQLException {
if (this.mainConnection != null && !this.isConnectionTransactional && !this.autoCommit) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Committing JDBC Connection [" + this.mainConnection + "]");
}
this.mainConnection.commit();
for (Connection connection : otherConnectionMap.values()) {
connection.commit();
}
}
}
/**
* {@inheritDoc}
*/
@Override
public void rollback() throws SQLException {
if (this.mainConnection != null && !this.isConnectionTransactional && !this.autoCommit) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Rolling back JDBC Connection [" + this.mainConnection + "]");
}
this.mainConnection.rollback();
for (Connection connection : otherConnectionMap.values()) {
connection.rollback();
}
}
}
/**
* {@inheritDoc}
*/
@Override
public void close() throws SQLException {
DataSourceUtils.releaseConnection(this.mainConnection, this.dataSource);
for (Connection connection : otherConnectionMap.values()) {
DataSourceUtils.releaseConnection(connection, this.dataSource);
}
}
@Override
public Integer getTimeout() throws SQLException {
return null;
}
}
MultiDataSourceTransactionFactory
package com.sccl.data_source_change.config;
import org.apache.ibatis.session.TransactionIsolationLevel;
import org.apache.ibatis.transaction.Transaction;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import javax.sql.DataSource;
/**
* <P>支持Service內(nèi)多數(shù)據(jù)源切換的Factory</P>
*
* @author lishuangqi
* @date 2019/5/16 15:09
* @since
*/
public class MultiDataSourceTransactionFactory extends SpringManagedTransactionFactory {
@Override
public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {
return new MultiDataSourceTransaction(dataSource);
}
}
XATransactionManagerConfig (分布式事務管理器)
上面重寫的多數(shù)據(jù)源事務管理器是為了讓我們能根據(jù)數(shù)據(jù)源的不同類型,動態(tài)獲取數(shù)據(jù)庫連接援岩,而不是從原來的緩存中獲取導致數(shù)據(jù)源沒法切換歼狼,這里配置的分布式事務管理器是為了讓多數(shù)據(jù)源操作發(fā)生異常時,讓多數(shù)據(jù)源的事務進行同步回滾享怀,由于之前配置的數(shù)據(jù)源都是換成了支持xa協(xié)議的羽峰,所以多數(shù)據(jù)源的資源都在atomikos的管控下了,能夠進行多數(shù)據(jù)源的事務回滾
package com.sccl.data_source_change.config;
import com.atomikos.icatch.jta.UserTransactionImp;
import com.atomikos.icatch.jta.UserTransactionManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.jta.JtaTransactionManager;
import javax.transaction.TransactionManager;
import javax.transaction.UserTransaction;
/** 分布式事務管理器
* Create by wangbin
* 2019-11-21-18:10
*/
@Configuration
public class XATransactionManagerConfig {
@Bean(name = "userTransaction")
public UserTransaction userTransaction() throws Throwable {
UserTransactionImp userTransactionImp = new UserTransactionImp();
userTransactionImp.setTransactionTimeout(10000);
return userTransactionImp;
}
@Bean(name = "atomikosTransactionManager")
public TransactionManager atomikosTransactionManager() throws Throwable {
UserTransactionManager userTransactionManager = new UserTransactionManager();
userTransactionManager.setForceShutdown(false);
return userTransactionManager;
}
@Bean(name = "transactionManager")
@DependsOn({ "userTransaction", "atomikosTransactionManager" })
public PlatformTransactionManager transactionManager() throws Throwable {
return new JtaTransactionManager(userTransaction(),atomikosTransactionManager());
}
}
PackagesSqlSessionFactoryBean
這個類繼承mybatis的SqlSessionFactoryBean添瓷,重寫設置包別名的方法梅屉,使其支持通配符配置
package com.sccl.data_source_change.utils;
import org.apache.commons.lang3.StringUtils;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.util.ClassUtils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/** 配置typeAliasesPackage支持通配符包路徑掃描
* 通過繼承重寫包路徑讀取方式來實現(xiàn)支持通配符配置,以前的SqlSessionFactoryBean
* 不支持通配符設置包別名仰坦,所以重寫該方法
* Create by wangbin
* 2019-11-25-17:18
*/
public class PackagesSqlSessionFactoryBean extends SqlSessionFactoryBean {
private static final Logger logger = LoggerFactory.getLogger(PackagesSqlSessionFactoryBean.class);
static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";
@Override
public void setTypeAliasesPackage(String typeAliasesPackage) {
ResourcePatternResolver resolver = (ResourcePatternResolver) new PathMatchingResourcePatternResolver();
MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resolver);
typeAliasesPackage = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
ClassUtils.convertClassNameToResourcePath(typeAliasesPackage) + "/" + DEFAULT_RESOURCE_PATTERN;
//將加載多個絕對匹配的所有Resource
//將首先通過ClassLoader.getResource("META-INF")加載非模式路徑部分
//然后進行遍歷模式匹配
try {
List<String> result = new ArrayList<String>();
Resource[] resources = resolver.getResources(typeAliasesPackage);
if(resources != null && resources.length > 0){
MetadataReader metadataReader = null;
for(Resource resource : resources){
if(resource.isReadable()){
metadataReader = metadataReaderFactory.getMetadataReader(resource);
try {
result.add(Class.forName(metadataReader.getClassMetadata().getClassName()).getPackage().getName());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
}
if(result.size() > 0) {
super.setTypeAliasesPackage(StringUtils.join(result.toArray(), ","));
}else{
logger.warn("參數(shù)typeAliasesPackage:"+typeAliasesPackage+"履植,未找到任何包");
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
-
2.2.5 domain,controller,service,mapper,mapper.xml
這些都是常規(guī)的層了,只有一些小細節(jié)需要注意悄晃,這里為了方便直觀測試玫霎,特意建了master,slave的包分開凿滤,其實可以不用分開的,mapper.xml就也分了包
Domain 實體類
Book
import lombok.Data;
/**
* Create by wangbin
* 2019-08-07-0:55
*/
@Data
public class Book {
private Integer id;
private String name;
private String author;
}
User
package com.sccl.data_source_change.slave.domain;
import lombok.Data;
/**
* Create by wangbin
* 2019-11-21-18:13
*/
@Data
public class User {
private Integer id;
private Integer age;
private String gender;
private String name;
}
Controller層
直接在controller層測試多庫讀取和多庫寫入
package com.sccl.data_source_change.controller;
import com.sccl.data_source_change.master.domain.Book;
import com.sccl.data_source_change.master.service.BookService;
import com.sccl.data_source_change.slave.domain.User;
import com.sccl.data_source_change.slave.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**在controller層中注入不同的mapper實例翁脆,操作不同的數(shù)據(jù)源
* Create by wangbin
* 2019-08-07-1:26
*/
@RestController
public class BookController {
@Autowired
private BookService bookService;
@Autowired
private UserService userService;
@GetMapping("/test1")//測試查詢主從庫的數(shù)據(jù)
public String test1(){
List<Book> books1 = bookService.getAllBooks();
List<Book> books2 = bookService.getAllBooks2();
System.out.println("books1:"+books1);
System.out.println("books2:"+books2);
List<User> users1 = userService.getAllUsers();
System.out.println("user1:"+users1);
return "OK";
}
@Transactional
@GetMapping("/test2")//測試主從雙庫寫入
public String test2(){
Book book = new Book();
book.setName("羅賓遜");
book.setAuthor("漂流記");
int bookNumber = bookService.addBook(book);
Book book2 = new Book();
book2.setName("飛馳人生");
book2.setAuthor("韓寒");
int bookNumber2 = bookService.addBook2(book2);
System.out.println("向master數(shù)據(jù)庫添加數(shù)據(jù):"+bookNumber);
System.out.println("向slave數(shù)據(jù)庫添加數(shù)據(jù):"+bookNumber2);
int number = 1/0;//自定義錯誤,查看事務是否回滾
return "OK";
}
@Transactional
@GetMapping("/test3")
public String test3(){
Book book = new Book();
book.setName("master");
book.setAuthor("master");
int bookNumber = bookService.addBook(book);
User user = new User();
user.setAge(18);
user.setGender("男");
user.setName("slave");
int userNumber = userService.addUser(user);
int number = 1/0;
return "OK";
}
}
Service鼻种,ServiceImpl 服務層與實現(xiàn)類
BookService
import com.sccl.data_source_change.master.domain.Book;
import java.util.List;
/**
* Create by wangbin
* 2019-11-18-17:56
*/
public interface BookService {
List<Book> getAllBooks();
List<Book> getAllBooks2();
int addBook(Book book);
int addBook2(Book book);
BookServiceImpl
package com.sccl.data_source_change.master.service;
import com.sccl.data_source_change.aspectj.annotation.DataSource;
import com.sccl.data_source_change.enumConst.DataSourceEnum;
import com.sccl.data_source_change.master.domain.Book;
import com.sccl.data_source_change.master.mapper.BookMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/**
* Create by wangbin
* 2019-11-18-17:57
*/
@Transactional
@Service
public class BookServiceImpl implements BookService {
@Autowired
private BookMapper bookMapper;
@DataSource(value = DataSourceEnum.MASTER)
@Override
public List<Book> getAllBooks() {
return bookMapper.getAllBooks();
}
@DataSource(value = DataSourceEnum.SLAVE)
@Override
public List<Book> getAllBooks2() {
return bookMapper.getAllBooks();
}
@DataSource(value = DataSourceEnum.MASTER)
@Override
public int addBook(Book book) {
return bookMapper.addBook(book);
}
@DataSource(value = DataSourceEnum.SLAVE)
@Override
public int addBook2(Book book) {
return bookMapper.addBook(book);
}
}
UserService
package com.sccl.data_source_change.slave.service;
import com.sccl.data_source_change.slave.domain.User;
import java.util.List;
/**
* Create by wangbin
* 2019-11-21-18:18
*/
public interface UserService {
int addUser(User user);
List<User> getAllUsers();
}
UserServiceImpl
package com.sccl.data_source_change.slave.service;
import com.sccl.data_source_change.aspectj.annotation.DataSource;
import com.sccl.data_source_change.enumConst.DataSourceEnum;
import com.sccl.data_source_change.slave.domain.User;
import com.sccl.data_source_change.slave.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/**
* Create by wangbin
* 2019-11-21-18:19
*/
@Transactional
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@DataSource(value = DataSourceEnum.SLAVE)
@Override
public int addUser(User user) {
return userMapper.addUser(user);
}
@DataSource(value = DataSourceEnum.SLAVE)
@Override
public List<User> getAllUsers() {
return userMapper.getAllUsers();
}
}
mapper層
BookMapper
package com.sccl.data_source_change.master.mapper;
import com.sccl.data_source_change.master.domain.Book;
import java.util.List;
/**
* Create by wangbin
* 2019-08-07-1:18
*/
public interface BookMapper {
List<Book> getAllBooks();
int addBook(Book book);
}
UserMapper
package com.sccl.data_source_change.slave.mapper;
import com.sccl.data_source_change.slave.domain.User;
import java.util.List;
/**
* Create by wangbin
* 2019-11-21-18:20
*/
public interface UserMapper {
int addUser(User user);
List<User> getAllUsers();
}
mapper.xml文件
BookMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.sccl.data_source_change.master.mapper.BookMapper">
<select id="getAllBooks" resultType="Book">
select * from book
</select>
<insert id="addBook" parameterType="Book">
insert into book (name,author) values (#{name},#{author})
</insert>
</mapper>
UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.sccl.data_source_change.slave.mapper.UserMapper">
<select id="getAllUsers" resultType="com.sccl.data_source_change.slave.domain.User">
select * from user
</select>
<insert id="addUser" parameterType="com.sccl.data_source_change.slave.domain.User">
insert into user (age,gender,name) values (#{age},#{gender},#{name})
</insert>
</mapper>
3 測試
3.1 測試雙庫讀取
訪問 http://localhost:8099/data/test1
斷點調(diào)試:
第一次訪問切換到master數(shù)據(jù)源
第二次訪問切換到slave數(shù)據(jù)源
第三次訪問切換到slave數(shù)據(jù)源
后臺打印結果:
前端頁面顯示結果:
后臺數(shù)據(jù)庫:
master庫反番,book表數(shù)據(jù)
slave庫叉钥,book表數(shù)據(jù)
slave庫,user表數(shù)據(jù)
測試結果:成功查詢了master庫中的book數(shù)據(jù)枫疆,slave庫中的book和user數(shù)據(jù)
小坑:這里要注意一下,要將@Transactional事務注解方法serviceImpl層敷鸦,不能放到controller層息楔,不然測試會發(fā)現(xiàn)在查詢book數(shù)據(jù)的時候,數(shù)據(jù)庫沒有切換扒披,但是查user數(shù)據(jù)的時候切換了值依,原因是在查book的時候,查詢不同庫中的book數(shù)據(jù)碟案,方法都寫在同一個bookService中的愿险,要在同一個service中用不同方法訪問不同的數(shù)據(jù)庫需要將事務控制的注解加到serviceImpl層,如果是controller層調(diào)用不同service中的方法訪問不同數(shù)據(jù)庫价说,可以直接將事務控制的注解加在controller層
3.2 測試雙庫寫入
訪問:http://localhost:8099/data/test2
1.先進行正常測試
后臺打印結果:
后臺數(shù)據(jù)庫:
maser庫的book中成功添加一條數(shù)據(jù)
slave庫的book中成功添加一條數(shù)據(jù)
前端頁面結果:
2.進行異常測試拯啦,看事務是否同步回滾
前端結果:
后臺打印結果:
后臺數(shù)據(jù)庫: 查看數(shù)據(jù)是否有沒有加入進去唁情,事務有沒有同步回滾
maste庫中數(shù)據(jù)編號還是9疑苔,是之前正常測試加進去的數(shù)據(jù),可以看到book數(shù)據(jù)沒有加進去甸鸟,事務回滾了
slave庫中的數(shù)據(jù)編號還是5惦费,也是之前正常測試加入的數(shù)據(jù),book的數(shù)據(jù)也沒有加進去抢韭,事務也回滾了
測試結果:master,slave庫中的數(shù)據(jù)都沒有添加成功薪贫,事務都進行了同步回滾
3.進行異常測試,向不同庫的不同表添加數(shù)據(jù)
前端結果:
后臺打印結果:
后臺數(shù)據(jù)庫:
master庫中的book數(shù)據(jù)編號還是9扯夭,依舊是第一次正常測試添加的數(shù)據(jù),本次添加的數(shù)據(jù)進行了事務回滾
slave庫中的user數(shù)據(jù)只有最早擁有的第一條數(shù)據(jù)鞍匾,很顯然新的數(shù)據(jù)也沒有加進去交洗,事務回滾了
測試結果:多庫操作異常發(fā)生,多庫事務同步回滾
4.進行正常測試橡淑,向不同庫的不同表添加數(shù)據(jù)
前端結果:
后臺打印結果:
后臺數(shù)據(jù)庫結果:
master庫book表中新加了一條數(shù)據(jù)
slave庫user表中新加了一條數(shù)據(jù)
測試結果:正常測試梁棠,向不同庫的不同表中添加數(shù)據(jù)成功
最終結論:采用Atomikos統(tǒng)一管控了多數(shù)據(jù)源資源操作的事務置森,通過重寫多數(shù)據(jù)源事務管理器使得在事務管控下能通過注解正常切換數(shù)據(jù)源,當多數(shù)據(jù)源操作出現(xiàn)異常時符糊,Atomikos會對管控的多數(shù)據(jù)源事務進行同步回滾凫海,未發(fā)生異常時,數(shù)據(jù)庫正常操作執(zhí)行
本Demo采用注解形式動態(tài)切換數(shù)據(jù)源濒蒋,并且可以管控分布式事務盐碱,可以不用分包,如果需要添加更多的數(shù)據(jù)源可以在枚舉和yml中簡單配置一下即可沪伙,比較靈活
本項目參照了很多網(wǎng)上的文章瓮顽,文章出處在文中已標注