Springboot+Mybatis-plus多数据源以及实现事务一致性

Springboot+Mybatis-plus多数据源以及实现事务一致性

在实际项目开发中,会同时连接2个或者多个数据库进行开发,因此我们需要配置多数据源,在使用多数据源的时候,在业务中可能会对2个不同的数据库进行插入、修改等操作,如何保证多数据源的事务一致性问题?主要解决如下问题:

  • 如何配置多数据源
  • 如何保证事务一致性

1.多数据源配置

如果只是配置多数据可以使用mybatis-plus的注解@DS,@DS 可以注解在方法上或类上,同时存在就近原则 方法上注解 优先于 类上注解

官方文档: https://baomidou.com/pages/a61e1b/#文档-documentation

Springboot+Mybatis-plus多数据源以及实现事务一致性

2.事务一致性

现在有2个数据库,需要同时对2个数据库中的表都进行插入操作,此时如果使用注解@Transactional就不行了。

通过配置不同的Mapper接口扫描路径使用不同的SqlSessionTemplate来实现。不同的SqlSessionTemplate就是不同的SqlSessionFactory,也就是不同的DataSource。

2.1添加POM文件

<!-- MyBatis Plus-->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.4.2</version>
</dependency>
<!-- 多数据源-->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
    <version>3.3.2</version>
</dependency>

2.2 配置2个不同的数据源

spring:
  datasource:
    dynamic:
      primary: master
      datasource:
        master:
          jdbc-url: jdbc:mysql://xxxx?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&verifyServerCertificate=false&useSSL=false
          username: root
          password: root
        slave:
          jdbc-url: jdbc:mysql://xxx?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&verifyServerCertificate=false&useSSL=false
          username: root
          password: 123

2.3 创建2个mapper包

2个mapper包分别对应存放2个数据源对应的mapper文件,这个里面没有什么特殊的,和之前怎么做现在还是怎么做

Springboot+Mybatis-plus多数据源以及实现事务一致性

  • 创建MasterDataSourceConfig配置文件

    import com.baomidou.mybatisplus.annotation.IdType;
    import com.baomidou.mybatisplus.core.MybatisConfiguration;
    import com.baomidou.mybatisplus.core.config.GlobalConfig;
    import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
    import com.qz.soft.sampling.config.MybatisPlusConfig;
    import org.apache.ibatis.plugin.Interceptor;
    import org.apache.ibatis.session.SqlSessionFactory;
    import org.apache.ibatis.type.JdbcType;
    import org.mybatis.spring.SqlSessionFactoryBean;
    import org.mybatis.spring.SqlSessionTemplate;
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.boot.jdbc.DataSourceBuilder;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Primary;
    import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
    import org.springframework.jdbc.datasource.DataSourceTransactionManager;
    import org.springframework.transaction.PlatformTransactionManager;
    
    import javax.annotation.Resource;
    import javax.sql.DataSource;
    
    /**
     * @author sean
     * @date 2021/12/23
     */
    
    @Configuration
    @MapperScan(basePackages = "com.sean.soft.sampling.mapper.master",sqlSessionFactoryRef = "masterSqlSessionFactory")
    public class MasterDataSourceConfig {
        @Resource
        private MybatisPlusConfig mybatisPlusConfig;
    
        @Primary
        @Bean("masterDataSource")
        @ConfigurationProperties(prefix = "spring.datasource.dynamic.datasource.master")
        public DataSource masterDataSource()
        {
            return DataSourceBuilder.create().build();
        }
        @Primary
        @Bean("masterSqlSessionFactory")
        public SqlSessionFactory masterSqlSessionFactory(@Qualifier("masterDataSource") DataSource dataSource) throws Exception {
            //如果要使用mybatis-plus的功能的话需要使用MybatisSqlSessionFactoryBean,不要使用SqlSessionFactoryBean,否则使用mybatis-plus里面的方法会报错找不到该方法
            MybatisSqlSessionFactoryBean  bean = new MybatisSqlSessionFactoryBean();
            
            bean.setDataSource(dataSource);
    
            MybatisConfiguration configuration = new MybatisConfiguration();
            configuration.setJdbcTypeForNull(JdbcType.NULL);
            configuration.setMapUnderscoreToCamelCase(true);
            configuration.setCacheEnabled(false);
            bean.setConfiguration(configuration);
            //添加分页功能
            Interceptor[] plugins = {mybatisPlusConfig.mybatisPlusInterceptor()};
            bean.setPlugins(plugins);
            //设置全局配置
            GlobalConfig globalConfig = new GlobalConfig();
            globalConfig.setDbConfig(new GlobalConfig.DbConfig().setIdType(IdType.ASSIGN_ID));
            globalConfig.setBanner(false);
            bean.setGlobalConfig(globalConfig);
    
            bean.setTypeAliasesPackage("com.qz.soft.sampling.entity");
            bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/master/*.xml"));
            return bean.getObject();
        }
    
        @Primary
        @Bean("masterSqlSessionTemplate")
        public SqlSessionTemplate masterSqlSessionTemplate(@Qualifier("masterSqlSessionFactory")SqlSessionFactory sqlSessionFactory)
        {
            return new SqlSessionTemplate(sqlSessionFactory);
        }
    
        @Primary
        @Bean("masterTransactionManager")
        public PlatformTransactionManager masterTransactionManager(@Qualifier("masterDataSource")DataSource dataSource)
        {
            return new DataSourceTransactionManager(dataSource);
        }
    
    }
    
    
  • 创建SlaveDataSourceConfig配置文件

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.qz.soft.sampling.config.MybatisPlusConfig;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.type.JdbcType;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.annotation.Resource;
import javax.sql.DataSource;

/**
 * @author sean
 * @date 2021/12/23
 */

@Configuration
@MapperScan(basePackages = "com.sean.soft.sampling.mapper.slave",sqlSessionFactoryRef = "slaveSqlSessionFactory")
public class SlaveDataSourceConfig {
    @Resource
    private MybatisPlusConfig mybatisPlusConfig;


    @Bean("slaveDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.dynamic.datasource.slave")
    public DataSource masterDataSource()
    {
        return DataSourceBuilder.create().build();
    }

    @Bean("slaveSqlSessionFactory")
    public SqlSessionFactory slaveSqlSessionFactory(@Qualifier("slaveDataSource") DataSource dataSource) throws Exception {
        MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
        bean.setDataSource(dataSource);

        MybatisConfiguration configuration = new MybatisConfiguration();
        configuration.setJdbcTypeForNull(JdbcType.NULL);
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.setCacheEnabled(false);
        bean.setConfiguration(configuration);

        //添加分页功能
        Interceptor[] plugins = {mybatisPlusConfig.mybatisPlusInterceptor()};
        bean.setPlugins(plugins);
        //全局配置
        GlobalConfig globalConfig = new GlobalConfig();
        globalConfig.setDbConfig(new GlobalConfig.DbConfig().setIdType(IdType.ASSIGN_ID));
        globalConfig.setBanner(false);
        bean.setGlobalConfig(globalConfig);

        bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/slave/*.xml"));
        return bean.getObject();
    }


    @Bean("slaveSqlSessionTemplate")
    public SqlSessionTemplate slaveSqlSessionTemplate(@Qualifier("slaveSqlSessionFactory")SqlSessionFactory sqlSessionFactory)
    {
        return new SqlSessionTemplate(sqlSessionFactory);
    }


    @Bean("slaveTransactionManager")
    public PlatformTransactionManager slaveTransactionManager(@Qualifier("slaveDataSource")DataSource dataSource)
    {
        return new DataSourceTransactionManager(dataSource);
    }

}

2.4 创建自定义注解@CustomTransaction

/**
 * @author sean
 * @date 2021/12/23
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER,ElementType.METHOD})
public @interface CustomTransaction {
    String[] value() default {};

}

2.5 创建AOP切面,解析自定义注解

import cn.hutool.core.util.ArrayUtil;
import com.qz.soft.sampling.util.BeanUtil;
import lombok.extern.slf4j.Slf4j;
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.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;

import java.util.Stack;

/**
 * @author sean
 * @date 2021/12/23
 */
@Slf4j
@Aspect
@Configuration
public class TransactionAop {

    @Pointcut("@annotation(com.qz.soft.sampling.annotation.CustomTransaction)")
    public void CustomTransaction() {
    }

    @Around(value = "CustomTransaction() && @annotation(annotation)")
    public Object syncLims(ProceedingJoinPoint joinPoint, CustomTransaction annotation) throws Throwable {
        Stack<DataSourceTransactionManager> dataSourceTransactionManagerStack = new Stack<>();
        Stack<TransactionStatus> transactionStatusStack = new Stack<>();
        try {
            if (!openTransaction(dataSourceTransactionManagerStack, transactionStatusStack, annotation)) {
                return null;
            }
            Object ret = joinPoint.proceed();
            commit(dataSourceTransactionManagerStack,transactionStatusStack);
            return ret;
        }catch (Throwable e)
        {
            rollback(dataSourceTransactionManagerStack,transactionStatusStack);
            log.error(String.format("MultTransactionAspect, method:%s-%s occors error:",joinPoint.getTarget().getClass().getSimpleName(),
                    joinPoint.getSignature().getName()),e);
            throw e;
        }
    }

    /**
     * 开启事务处理方法
     *
     * @param dataSourceTransactionManagerStack
     * @param transactionStatusStack
     * @param multiTransaction
     * @return
     */
    public Boolean openTransaction(Stack<DataSourceTransactionManager> dataSourceTransactionManagerStack,
                                   Stack<TransactionStatus> transactionStatusStack, CustomTransaction multiTransaction) {


        String[] transactionManagerNames = multiTransaction.value();
        if (ArrayUtil.isEmpty(transactionManagerNames)) {
            return false;
        }

        for (String beanName : transactionManagerNames) {
            DataSourceTransactionManager dataSourceTransactionManager = (DataSourceTransactionManager) BeanUtil.getBean(beanName);
            TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(new DefaultTransactionDefinition());
            transactionStatusStack.push(transactionStatus);
            dataSourceTransactionManagerStack.push(dataSourceTransactionManager);
        }
        return true;
    }

    /**
     * 提交处理方法
     *
     * @param dataSourceTransactionManagerStack
     * @param transactionStatusStack
     */
    private void commit(Stack<DataSourceTransactionManager> dataSourceTransactionManagerStack,
                        Stack<TransactionStatus> transactionStatusStack) {
        while (!dataSourceTransactionManagerStack.isEmpty()) {
            dataSourceTransactionManagerStack.pop().commit(transactionStatusStack.pop());
        }
    }

    /**
     * 回滚处理方法
     * @param dataSourceTransactionManagerStack
     * @param transactionStatusStack
     */
    private void rollback(Stack<DataSourceTransactionManager> dataSourceTransactionManagerStack,
                          Stack<TransactionStatus> transactionStatusStack) {
        while (!dataSourceTransactionManagerStack.isEmpty()) {
            dataSourceTransactionManagerStack.pop().rollback(transactionStatusStack.pop());
        }
    }


}

这样我们就完成了整个代码的编写,下面就进行测试,测试的时候只需要在方法上使用自定义注解@CustomTransaction(value = {"masterTransactionManager","slaveTransactionManager"})

Springboot+Mybatis-plus多数据源以及实现事务一致性

参考文档:

https://www.cnblogs.com/red-star/p/12535919.html

https://blog.csdn.net/qq_31142553/article/details/102768696

上一篇:uniapp判断第三方应用是否存在并打开应用


下一篇:vue3+element-plus搭建后台管理系统(三)