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

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

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

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

1.多数据源配置

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

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

image-20211224132010843

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文件,这个里面没有什么特殊的,和之前怎么做现在还是怎么做

image-20211224144854659

  • 创建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.setIdentifierGenerator(new CustomIdGenerator());
            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.setIdentifierGenerator(new CustomIdGenerator());
        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());
        }
    }


}

需要用到的工具类:BeanUtil

@Component
public class BeanUtil implements ApplicationContextAware {
    protected static ApplicationContext context;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }

    public static Object getBean(String name) {
        return context.getBean(name);
    }

    public static <T> T getBean(Class<T> c){
        return context.getBean(c);
    }
}

MybatisPlusConfig配置类


@Configuration
@EnableTransactionManagement
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
        paginationInnerInterceptor.setDbType(DbType.MYSQL);
        paginationInnerInterceptor.setOverflow(true);
        interceptor.addInnerInterceptor(paginationInnerInterceptor);
        return interceptor;
    }
}

自定义主键生成策略

@Slf4j
@Component
public class CustomIdGenerator implements IdentifierGenerator {

    @Override
    public Long nextId(Object entity) {
        return  UIDGenerator.getUID();

    }
}
@Slf4j
public class UIDGenerator {
	/** 开始时间截 (2017-11-06) */
    private final long twepoch = 1509976472321L;

    private final long workerIdBits = 3L;

    //最大为7
    private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
    
    private final long timestampLeftShift = workerIdBits;

    private long workerId;
    
    /** 上次生成ID的时间截 */
    private long lastTimestamp = -1L;
    
    private static class UIDGeneratorHolder {
        private static final UIDGenerator instance = new UIDGenerator();
    }
    
    private static UIDGenerator get(){
        return UIDGeneratorHolder.instance;
    }
    
	public static long getUID() {
    	return getUID(null);
	}

	public static long getUID(Long workerId) {
		UIDGenerator generator = get();
		if(workerId == null){
			workerId = 0l;
		}else if (workerId.longValue() > generator.maxWorkerId || workerId.longValue() < 0) {
            throw new IllegalArgumentException(String.format("workId不能大于%d或小于0", generator.maxWorkerId));
        }
		generator.workerId = workerId;
        return generator.nextId();
	}

    /**
     * 获得下一个ID (该方法是线程安全的)
     * @return SnowflakeId
     */
    private synchronized long nextId() {
        long timestamp = timeGen();

        //如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
        if (timestamp < lastTimestamp) {
//            throw new RuntimeException(
//                    String.format("时间被回退,生成的无效时间戳%d", lastTimestamp - timestamp));
            log.error("时间被回退,生成的无效时间戳{}", lastTimestamp - timestamp);

        }

        //如果是同一时间生成的,则重新获取
        if (lastTimestamp == timestamp) {
            //阻塞到下一个毫秒,获得新的时间戳
            timestamp = tilNextMillis(lastTimestamp);
        }

        //上次生成ID的时间截
        lastTimestamp = timestamp;

        return ((timestamp - twepoch) << timestampLeftShift) | workerId;
                
    }

    /**
     * 阻塞到下一个毫秒,直到获得新的时间戳
     * @param lastTimestamp 上次生成ID的时间截
     * @return 当前时间戳
     */
    private long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    /**
     * 返回以毫秒为单位的当前时间
     * @return 当前时间(毫秒)
     */
    private long timeGen() {
        return System.currentTimeMillis();
    }
}

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

image-20211226200323876

参考文档:

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

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

posted @ 2021-12-27 13:23  肖恩雷  阅读(4414)  评论(7编辑  收藏  举报