多数据源事务

引言

本文记录一下多数据源下的配置以及事务一致性问题,提供一种解决方案。

项目架构

  • 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>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值