引言
本文记录一下多数据源下的配置以及事务一致性问题,提供一种解决方案。
项目架构
- Spring Boot
- Mybatis Plus
多数据源定义
在 yml 新增数据源信息,用于数据源的创建。
spring:
datasource:
mbplus:
master:
username: xxx
password: xxx
url: xxx
driver-class-name: oracle.jdbc.driver.OracleDriver
encrypt: false
initialSize: 10
minIdle: 10
maxActive: 500
#druid wait timeout
maxWait: 60000
#druid check idletime
timeBetweenEvictionRunsMillis: 60000
#druid pool circle
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
#druid filters
filters: stat,wall,log4j
logSlowSql: true
second:
username: xxx
password: xxx
url: xxx
driver-class-name: oracle.jdbc.driver.OracleDriver
encrypt: false
initialSize: 10
minIdle: 10
maxActive: 500
#druid wait timeout
maxWait: 60000
#druid check idletime
timeBetweenEvictionRunsMillis: 60000
#druid pool circle
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
#druid filters
filters: stat,wall,log4j
logSlowSql: true
1、新建数据源属性实体类,使用@ConfigurationProperties
注解注入数据源属性值, 如@ConfigurationProperties(prefix = "spring.datasource.mbplus.master")
注入主数据源的一些属性
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Configuration
@ConfigurationProperties(prefix = "spring.datasource.mbplus.master")
public class MasterDataSourcePropetry {
public String url;
public String username;
public String password;
public String driverClassName;
public int initialSize;
public int minIdle;
public int maxActive;
public int maxWait;
public int timeBetweenEvictionRunsMillis;
public int minEvictableIdleTimeMillis;
public String validationQuery;
public String filters;
public String logSlowSql;
public String encrypt;
}
2、创建主数据源的 sqlMapper 类, 使用@Qualifier
注解限定使用主数据源私有的 SqlSessionTemplate,注入 masterSqlMapper 实例。
@Component
@ConditionalOnClass({SqlSession.class})
@NotProguard
public class MybatisPlusMasterSqlMapperConfig {
private final SqlSessionTemplate sqlSessionTemplate;
@Autowired
public MybatisPlusMasterSqlMapperConfig(@Qualifier("masterSqlSessionTemplate") SqlSessionTemplate sqlSessionTemplate) {
this.sqlSessionTemplate = sqlSessionTemplate;
}
@Bean(name = {"masterSqlMapper"})
public SqlMapper sqlMapper() {
return new SqlMapper(this.sqlSessionTemplate);
}
}
3、创建主数据源的 SqlSession 类, 使用 @MapperScan
注解标记数据源的扫描范围,以此在调用时区分使用哪个数据源。
注意:此种方式将以包名来区分数据源, 如主数据源下的dao需全部在basedao.masterdao目录下,第二数据源下的dao需全部在basedao.seconddao目录下,以此类推。
@MapperScan(
basePackages = {"com.**.basedao.masterdao"},
sqlSessionTemplateRef = "masterSqlSessionTemplate"
)
@ConditionalOnProperty(
prefix = "spring.database",
name = {"type"},
havingValue = "0",
matchIfMissing = true
)
@Configuration
public class MybatisPlusMasterSqlSessionConfig {
private final CustMybatisProperties properties;
private GlobalConfig globalConfig;
public MybatisPlusMasterSqlSessionConfig(CustMybatisProperties properties) {
this.properties = properties;
}
@Bean
public MybatisPlusInterceptor paginationInterceptor() {
//分页拦截器
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(paginationInnerInterceptor);
return mybatisPlusInterceptor;
}
@Bean("masterSqlSessionFactory")
public SqlSessionFactory sqlSessionFactory(@Qualifier("masterDataSource") DataSource dataSource) throws Exception {
MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
factory.setDataSource(dataSource);
MybatisConfiguration configuration = new MybatisConfiguration();
configuration.setCallSettersOnNulls(true);
BeanUtils.copyProperties(this.properties.getConfiguration(), configuration);
this.globalConfig = GlobalConfigUtils.defaults();
factory.setConfiguration(configuration);
configuration.setMapUnderscoreToCamelCase(false);
factory.setMapperLocations(this.properties.resolveMapperLocations());
//添加分页功能
factory.setPlugins(new Interceptor[]{
paginationInterceptor()
});
return factory.getObject();
}
@Bean(name = {"masterSqlSessionTemplate"})
public SqlSessionTemplate sqlSession(@Qualifier("masterSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
ExecutorType executorType = this.properties.getExecutorType();
return executorType != null ? new SqlSessionTemplate(sqlSessionFactory, executorType) : new SqlSessionTemplate(sqlSessionFactory);
}
@Bean(name = {"masterPlatformTransactionManager"})
public PlatformTransactionManager txManager1(@Qualifier("masterDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
3、创建主数据源 DataSource 配置类, 设置数据库连接时的各种属性。
@Configuration
@ConditionalOnClass({SqlSession.class})
@EnableConfigurationProperties(MybatisPlusProperties.class)
@ConditionalOnProperty(
prefix = "spring.database",
name = {"type"},
havingValue = "0",
matchIfMissing = true
)
public class MybatisPlusMasterDataSourceConfiguration {
private static Logger log = LoggerFactory.getLogger(MybatisPlusMasterDataSourceConfiguration.class);
@Autowired
MasterDataSourcePropetry propetry;
@Bean(name = {"masterDataSource"})
public DataSource getDataSource() {
if (!LicenseUtil.verifyLic("license.xml", "core")) {
return null;
} else {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(propetry.url);
dataSource.setUsername(propetry.username);
if (propetry.encrypt.equals("true")) {
try {
dataSource.setPassword(SimpCrypt.decryptByPriKey(propetry.password));
} catch (Exception var3) {
var3.printStackTrace();
log.info("密码解密失败" + var3);
}
} else {
dataSource.setPassword(propetry.password);
}
dataSource.setDriverClassName(propetry.driverClassName);
dataSource.setInitialSize(propetry.initialSize);
dataSource.setMinIdle(propetry.minIdle);
dataSource.setMaxActive(propetry.maxActive);
dataSource.setMaxWait((long) propetry.maxWait);
dataSource.setTimeBetweenEvictionRunsMillis((long) propetry.timeBetweenEvictionRunsMillis);
dataSource.setMinEvictableIdleTimeMillis((long) propetry.minEvictableIdleTimeMillis);
dataSource.setValidationQuery(propetry.validationQuery);
if (log.isInfoEnabled()) {
log.info("使用Druid数据库缓冲池创建mybatis-plus数据源.......");
log.info("数据源url:{}, username:{}", dataSource.getUrl(), dataSource.getUsername());
}
return dataSource;
}
}
}
4、重复1-3的步骤,创建第二数据源。
自定义多数据源事务注解
定义数据源管理器枚举
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum MultipleTransactionManager {
/**
* 默认使用第一数据源和第二数据源
*/
DEFAULT(new String[]{"masterPlatformTransactionManager", "secondPlatformTransactionManager"}),
/**
* 使用所有数据源
*/
All(new String[]{"masterPlatformTransactionManager", "msecondPlatformTransactionManager"});
private String[] value;
}
自定义多数据源事务注解,使用枚举类型定义事务管理器变量。
import java.lang.annotation.*;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface MultipleTransactional {
MultipleTransactionManager transactionManagers() default MultipleTransactionManager.DEFAULT;
}
实现自定义注解切面类,在切中方法调用前,使用 ThreadLocal 缓存所需的数据源事务管理器;在切中方法调用成功时,将缓存的所有事务管理器的事务进行提交;在切中方法异常时,将缓存的所有事务管理器的事务回滚。以此实现多数据源下的事务一致性。
import javafx.util.Pair;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Component;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import java.util.Objects;
import java.util.Stack;
@Slf4j
@Aspect
@Component
public class MultipleTransactionalAspect {
private static final ThreadLocal<Stack<Pair<DataSourceTransactionManager, TransactionStatus>>> THREAD_LOCAL = new ThreadLocal<>();
@Autowired
private ApplicationContext applicationContext;
@Pointcut("@annotation(com.service.config.annotation.MultipleTransactional)")
public void pointcut() {
}
/**
* 声明事务
* @param transactional 注解
*/
@Before("pointcut() && @annotation(transactional)")
public void before(MultipleTransactional transactional) {
// 根据设置的事务名称按顺序声明,并放到ThreadLocal里
String[] transactionManagerNames = transactional.transactionManagers().getValue();
Stack<Pair<DataSourceTransactionManager, TransactionStatus>> pairStack = new Stack<>();
for (String transactionManagerName : transactionManagerNames) {
DataSourceTransactionManager transactionManager = applicationContext.getBean(transactionManagerName, DataSourceTransactionManager.class);
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
// 非只读模式
def.setReadOnly(false);
// 事务隔离级别:采用数据库的
def.setIsolationLevel(TransactionDefinition.ISOLATION_DEFAULT);
// 事务传播行为
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
TransactionStatus transactionStatus = transactionManager.getTransaction(def);
pairStack.push(new Pair(transactionManager, transactionStatus));
}
THREAD_LOCAL.set(pairStack);
}
@AfterReturning("pointcut()")
public void afterReturning() {
// 栈顶弹出(后进先出)
Stack<Pair<DataSourceTransactionManager, TransactionStatus>> pairStack = THREAD_LOCAL.get();
if(Objects.nonNull(pairStack)){
while (!pairStack.empty()) {
Pair<DataSourceTransactionManager, TransactionStatus> pair = pairStack.pop();
pair.getKey().commit(pair.getValue());
}
}
THREAD_LOCAL.remove();
}
/**
* 回滚事务
*/
@AfterThrowing(value = "pointcut()")
public void afterThrowing() {
log.info("事务异常回滚!");
Stack<Pair<DataSourceTransactionManager, TransactionStatus>> pairStack = THREAD_LOCAL.get();
if(Objects.nonNull(pairStack)){
while (!pairStack.empty()) {
Pair<DataSourceTransactionManager, TransactionStatus> pair = pairStack.pop();
pair.getKey().rollback(pair.getValue());
}
}
THREAD_LOCAL.remove();
}
}
使用示例
1、采用默认事务管理器 MultipleTransactionManager.DEFAULT
@MultipleTransactional
public void writeDate() {}
2、修改注解属性 transactionManagers , 按需使用相应事务管理器
@MultipleTransactional(transactionManagers = MultipleTransactionManager.All)
public void writeDate(){}
查看运行时数据源
苞米多自身提供了对运行时数据源的支持,可以通过 DynamicRoutingDataSource
类的determineDataSource()
方法查看。
我们可以通过切中Dao层的方法在其被调用时打印出数据源信息。
import com.alibaba.druid.pool.DruidDataSource;
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Slf4j
@Aspect
@Component
public class DataSourceInfoAspact {
@Autowired
DynamicRoutingDataSource dataSourceRoute;
@Pointcut("execution(* com..*.*Dao.*(..))")
public void pointcut() {
}
@Before("pointcut()")
public void before() {
DruidDataSource dataSource1 = (DruidDataSource)dataSourceRoute.determineDataSource();
System.out.println("数据源:" + dataSource1.getUrl());
}
}
可能发生的异常
1、OpenJDK 与 OracleJDK 版本差异导致在 Linux 环境运行时发生 java.lang.ClassNotFoundException: javafx.util.Pair
原因: OpenJDK8 缺少 javafx.util.Pair
, OracleJDK 含有javafx.util.Pair
,故而运行在Windows时由于使用的是 OracleJDK 并不会发生 java.lang.ClassNotFoundException
异常, 运行在 Linux 环境时由于使用的是 OpenJDK 发生 java.lang.ClassNotFoundException
异常。
解决方案一: 将javafx.util.Pair
复制一份到本地项目,包名保持一致。对其中的原理有兴趣的话可以去了解一下类加载机制。
package javafx.util;
import java.io.Serializable;
import javafx.beans.NamedArg;
/**
* <p>A convenience class to represent name-value pairs.</p>
* @since JavaFX 2.0
*/
public class Pair<K,V> implements Serializable{
/**
* Key of this <code>Pair</code>.
*/
private K key;
/**
* Gets the key for this pair.
* @return key for this pair
*/
public K getKey() { return key; }
/**
* Value of this this <code>Pair</code>.
*/
private V value;
/**
* Gets the value for this pair.
* @return value for this pair
*/
public V getValue() { return value; }
/**
* Creates a new pair
* @param key The key for this pair
* @param value The value to use for this pair
*/
public Pair(@NamedArg("key") K key, @NamedArg("value") V value) {
this.key = key;
this.value = value;
}
/**
* <p><code>String</code> representation of this
* <code>Pair</code>.</p>
*
* <p>The default name/value delimiter '=' is always used.</p>
*
* @return <code>String</code> representation of this <code>Pair</code>
*/
@Override
public String toString() {
return key + "=" + value;
}
/**
* <p>Generate a hash code for this <code>Pair</code>.</p>
*
* <p>The hash code is calculated using both the name and
* the value of the <code>Pair</code>.</p>
*
* @return hash code for this <code>Pair</code>
*/
@Override
public int hashCode() {
// name's hashCode is multiplied by an arbitrary prime number (13)
// in order to make sure there is a difference in the hashCode between
// these two parameters:
// name: a value: aa
// name: aa value: a
return key.hashCode() * 13 + (value == null ? 0 : value.hashCode());
}
/**
* <p>Test this <code>Pair</code> for equality with another
* <code>Object</code>.</p>
*
* <p>If the <code>Object</code> to be tested is not a
* <code>Pair</code> or is <code>null</code>, then this method
* returns <code>false</code>.</p>
*
* <p>Two <code>Pair</code>s are considered equal if and only if
* both the names and values are equal.</p>
*
* @param o the <code>Object</code> to test for
* equality with this <code>Pair</code>
* @return <code>true</code> if the given <code>Object</code> is
* equal to this <code>Pair</code> else <code>false</code>
*/
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o instanceof Pair) {
Pair pair = (Pair) o;
if (key != null ? !key.equals(pair.key) : pair.key != null) return false;
if (value != null ? !value.equals(pair.value) : pair.value != null) return false;
return true;
}
return false;
}
}
解决方案二: 使用其他第三方的Pair类,如hutool
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.14</version>
</dependency>