SpringBoot2.X+MybatisPlus+Несколько источников данных+Транзакции

Java

предисловие

  1. Одно дело, используете ли вы в своем проекте несколько источников данных, а другое — будете ли вы самим собой.
  2. В SpringBoot 2.0.8 очень просто интегрировать MybatisPlus для реализации нескольких источников данных, но транзакция не всегда вступает в силу?
  3. MybatisPlus предоставляет несколько плагинов для источников данных (Ссылка на сайт), я могу не использовать его?
  4. На самом деле очень хорошо с несколькими источниками данных, то есть транзакция не имеет никакого эффекта. Сегодня окончательно решил.

Структура проекта:

Основными классами конфигурации являются следующие пять: DsAspect, DataSourceConfiguration, MyRoutingDataSource, MybatisConfiguration, TransactionConfig. Позже я объясню функции каждого класса один за другим.

файл конфигурации:

spring:
  # 数据源配置
  datasource:
    druid:
      type: com.alibaba.druid.pool.DruidDataSource
      defaultDs: master
      master:
        name: master
        url: jdbc:mysql://ip:3306/wx_edu?useUnicode=true&characterEncoding=UTF-8&useSSL=false
        username: root
        password: 123456
        driver-class-name: com.mysql.jdbc.Driver
        initial-size: 10
        min-idle: 10
        max-active: 100
        max-wait: 60000
        pool-prepared-statements: true
        max-pool-prepared-statement-per-connection-size: 20
        time-between-eviction-runs-millis: 60000
        min-evictable-idle-time-millis: 300000
        validation-query: SELECT version()
        validation-query-timeout: 10000
        test-while-idle: true
        test-on-borrow: false
        test-on-return: false
        remove-abandoned: true
        remove-abandoned-timeout: 86400
        filters: stat,wall
        connection-properties: druid.stat.mergeSql=true;
        web-stat-filter:
          enabled: true
          url-pattern: /*
          exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"
        stat-view-servlet:
          enabled: true
          url-pattern: /druid/*
          reset-enable: false
          login-username: admin
          login-password: admin
        filter:
          stat:
            log-slow-sql: true
            slow-sql-millis: 1000
            merge-sql: true
          wall:
            config:
              multi-statement-allow: true
          config:
            enabled: true

      # slave 数据源
      slave:
        name: slave
        url: jdbc:mysql://ip:3307/wx_edu?useUnicode=true&characterEncoding=UTF-8&useSSL=false
        username: root
        password: 123456
        driver-class-name: com.mysql.jdbc.Driver
        #连接参数
        initial-size: 10
        min-idle: 10
        max-active: 100
        max-wait: 60000
        pool-prepared-statements: true
        max-pool-prepared-statement-per-connection-size: 20
        time-between-eviction-runs-millis: 60000
        min-evictable-idle-time-millis: 300000
        validation-query: SELECT version()
        validation-query-timeout: 10000
        test-while-idle: true
        test-on-borrow: false
        test-on-return: false
        remove-abandoned: true
        remove-abandoned-timeout: 86400
        filters: stat,wall
        connection-properties: druid.stat.mergeSql=true;
        web-stat-filter:
          enabled: true
          url-pattern: /*
          exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"
        stat-view-servlet:
          enabled: true
          url-pattern: /druid/*
          reset-enable: false
          login-username: admin
          login-password: admin
        filter:
          stat:
            log-slow-sql: true
            slow-sql-millis: 1000
            merge-sql: true
          wall:
            config:
              multi-statement-allow: true
          config:
            enabled: true
mybatis-plus:
  global-config:
    #主键类型  0:"数据库ID自增", 1:"用户输入ID",2:"全局唯一ID (数字类型唯一ID)", 3:"全局唯一ID UUID";
    id-type: 0
    #字段策略 0:"忽略判断",1:"非 NULL 判断"),2:"非空判断"
    field-strategy: 0
    #驼峰下划线转换
    db-column-underline: true
    #刷新mapper 调试神器
    refresh-mapper: true
    #数据库大写下划线转换
    #capital-mode: true
    #逻辑删除配置(下面3个配置)
    logic-delete-value: 0
    logic-not-delete-value: 1
    # SQL 解析缓存,开启后多租户 @SqlParser 注解生效
  #    sql-parser-cache: true

DataSourceConfiguration:

В основном для настройки bean-компонентов из нескольких источников данных приведенный выше код:

@Configuration
public class DataSourceConfiguration {
    /**
     * 默认是数据源
     */
    @Value("${spring.datasource.druid.defaultDs}")
    private String defaultDs;

    @Bean(name = "dataSourceMaster")
    @Primary
    @ConfigurationProperties(prefix = "spring.datasource.druid.master")
    public DataSource dataSourceMaster() {
        DataSource druidDataSource = DruidDataSourceBuilder.create().build();
        DbContextHolder.addDataSource(CommonEnum.DsType.DS_MASTER.getValue(), druidDataSource);

        return druidDataSource;
    }

    @Bean(name = "dataSourceSlave")
    @ConfigurationProperties(prefix = "spring.datasource.druid.slave")
    public DataSource dataSourceSlave() {
        DataSource druidDataSource = DruidDataSourceBuilder.create().build();
        DbContextHolder.addDataSource(CommonEnum.DsType.DS_SLAVE.getValue(), druidDataSource);
        return druidDataSource;
    }

    @Bean(name = "myRoutingDataSource")
    public MyRoutingDataSource dataSource(@Qualifier("dataSourceMaster") DataSource dataSourceMaster, @Qualifier("dataSourceSlave") DataSource dataSourceSlave) {
        MyRoutingDataSource dynamicDataSource = new MyRoutingDataSource();
        Map<Object, Object> targetDataResources = new HashMap<>();
        targetDataResources.put(CommonEnum.DsType.DS_MASTER.getValue(), dataSourceMaster);
        targetDataResources.put(CommonEnum.DsType.DS_SLAVE.getValue(), dataSourceSlave);
        //设置默认数据源
        dynamicDataSource.setDefaultTargetDataSource(dataSourceMaster);
        dynamicDataSource.setTargetDataSources(targetDataResources);
        DbContextHolder.setDefaultDs(defaultDs);
        return dynamicDataSource;
    }

}

Это непросто объяснить, это значит инкапсулировать файл конфигурации в bean-компонент dataSource, которыйMyRoutingDataSourceЭто источник данных, который мы используем, включая конфигурацию транзакций.

MyRoutingDataSource

public class MyRoutingDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return DbContextHolder.getCurrentDsStr();
    }
}

вAbstractRoutingDataSourceЭто абстрактный класс, предоставляемый модулем jdbc Spring, который действует какDataSourceПосредник маршрутизации может динамически переключаться на реальный в соответствии с определенным значением ключа во время выполнения.DataSourceвыше, переписатьdetermineCurrentLookupKey()способ переключения источника данных. Это означает, что если вы хотите играть с несколькими источниками данных, вы должны использовать этот класс. Я также использовалDbContextHolderКласс инструмента (эквивалент держателя источника данных), код выглядит следующим образом, в основном скопирован из Интернета, с небольшой модификацией:

public class DbContextHolder {

    /**
     * 项目中配置数据源
     */
    private static Map<String, DataSource> dataSources = new ConcurrentHashMap<>();

    /**
     * 默认数据源
     */
    private static String defaultDs = "";

    /**
     * 为什么要用链表存储(准确的是栈)
     * <pre>
     * 为了支持嵌套切换,如ABC三个service都是不同的数据源
     * 其中A的某个业务要调B的方法,B的方法需要调用C的方法。一级一级调用切换,形成了链。
     * 传统的只设置当前线程的方式不能满足此业务需求,必须模拟栈,后进先出。
     * </pre>
     */
    private static final ThreadLocal<Deque<String>> contextHolder = new ThreadLocal() {
        @Override
        protected Object initialValue() {
            return new ArrayDeque();
        }
    };

    /**
     * 设置当前线程使用的数据源
     *
     * @param dsName
     */
    public static void setCurrentDsStr(String dsName) {
        if (StringUtils.isBlank(dsName)) {
            log.error("==========>dbType is null,throw NullPointerException");
            throw new NullPointerException();
        }
        if (!dataSources.containsKey(dsName)) {
            log.error("==========>datasource not exists,dsName={}", dsName);
            throw new RuntimeException("==========>datasource not exists,dsName={" + dsName +"}");
        }
        contextHolder.get().push(dsName);
    }


    /**
     * 获取当前使用的数据源
     *
     * @return
     */
    public static String getCurrentDsStr() {
        return contextHolder.get().peek();
    }

    /**
     * 清空当前线程数据源
     * <p>
     * 如果当前线程是连续切换数据源
     * 只会移除掉当前线程的数据源名称
     * </p>
     */
    public static void clearCurrentDsStr() {
        Deque<String> deque = contextHolder.get();
        deque.poll();
        if (deque.isEmpty()){
            contextHolder.remove();
        }
    }

    /**
     * 添加数据源
     *
     * @param dsName
     * @param dataSource
     */
    public static void addDataSource(String dsName, DataSource dataSource) {
        if (dataSources.containsKey(dsName)) {
            log.error("==========>dataSource={} already exist", dsName);
            //throw new RuntimeException("dataSource={" + dsName + "} already exist");
            return;
        }
        dataSources.put(dsName, dataSource);
    }

    /**
     * 获取指定数据源
     *
     * @return
     */
    public static DataSource getDefaultDataSource() {
        if (StringUtils.isBlank(defaultDs)) {
            log.error("==========>default datasource must be configured");
            throw new RuntimeException("default datasource must be configured.");
        }
        if (!dataSources.containsKey(defaultDs)) {
            log.error("==========>The default datasource must be included in the datasources");
            throw new RuntimeException("==========>The default datasource must be included in the datasources");
        }
        return dataSources.get(defaultDs);
    }

    /** 设置默认数据源
     * @param defaultDsStr
     */
    public static void setDefaultDs(String defaultDsStr) {
        defaultDs = defaultDsStr;
    }

    /**获取所有 数据源
     * @return
     */
    public static Map<String, DataSource> getDataSources() {
        return dataSources;
    }

    /**
     * @return
     */
    public static String getDefaultDs() {
        return defaultDs;
    }

Конфигурация Mybatis:

Это класс конфигурации MybatisPlus, который проще, если вы используете Mybatis. Поскольку Mybatis нужно только настроитьSqlSessionFactory, пока MybatisPlus настроенMybatisSqlSessionFactoryBean

@Slf4j
@Configuration
@AutoConfigureAfter({DataSourceConfiguration.class})
@MapperScan(basePackages = {"com.sqt.edu.*.mapper*","com.sqt.edu.*.api.mapper*"})
public class MybatisConfiguration {

    @Bean
    public SqlSessionFactory sqlSessionFactory(@Qualifier(value = "myRoutingDataSource") MyRoutingDataSource myRoutingDataSource) throws
            Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(myRoutingDataSource);
        return sqlSessionFactoryBean.getObject();
    }

    @Bean(name = "mybatisSqlSessionFactoryBean")
    @Primary
    public MybatisSqlSessionFactoryBean sqlSessionFactoryBean(@Qualifier(value = "myRoutingDataSource") DataSource dataSource) throws Exception {
        log.info("==========>开始注入 MybatisSqlSessionFactoryBean");
        MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
        Set<Resource> result = new LinkedHashSet<>(16);
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        try {
            result.addAll(Arrays.asList(resolver.getResources("classpath*:mapper/*.xml")));
            result.addAll(Arrays.asList(resolver.getResources("classpath*:config/mapper/*/*.xml")));
            result.addAll(Arrays.asList(resolver.getResources("classpath*:mapper/*/*.xml")));
        } catch (IOException e) {
            log.error("获取【classpath:mapper/*/*.xml,classpath:config/mapper/*/*.xml】资源错误!异常信息:{}", e);
        }
        bean.setMapperLocations(result.toArray(new org.springframework.core.io.Resource[0]));
        bean.setDataSource(dataSource);
        bean.setVfs(SpringBootVFS.class);
        com.baomidou.mybatisplus.core.MybatisConfiguration configuration = new com.baomidou.mybatisplus.core.MybatisConfiguration();
        configuration.setLogImpl(StdOutImpl.class);
        configuration.setMapUnderscoreToCamelCase(true);
        //添加 乐观锁插件
        configuration.addInterceptor(optimisticLockerInterceptor());
        bean.setConfiguration(configuration);
        GlobalConfig globalConfig = GlobalConfigUtils.defaults();
        //设置 字段自动填充处理
        globalConfig.setMetaObjectHandler(new MyMetaObjectHandler());
        bean.setGlobalConfig(globalConfig);
        log.info("==========>注入 MybatisSqlSessionFactoryBean 完成!");
        return bean;
    }

}

настроено здесьSqlSessionFactoryиMybatisSqlSessionFactoryBeanвсе нужноMyRoutingDataSourceэтот источник данных.

DsAspect:

Класс конфигурации аспекта переключения источника данных

@Order(0)
@Aspect
@Component
@Slf4j
public class DsAspect {
    /**
     * 配置AOP切面的切入点
     * 切换放在service接口的方法上
     */
    @Pointcut("execution(* com.sqt..service..*Service.*(..))")
    public void dataSourcePointCut() {
    }

    /**
     * 根据切点信息获取调用函数是否用TargetDataSource切面注解描述,
     * 如果设置了数据源,则进行数据源切换
     */
    @Before("dataSourcePointCut()")
    public void before(JoinPoint joinPoint) {
        if (StringUtils.isNotBlank(DbContextHolder.getCurrentDsStr())) {
            log.info("==========>current thread {} use dataSource[{}]",
                    Thread.currentThread().getName(), DbContextHolder.getCurrentDsStr());
            return;
        }
        String method = joinPoint.getSignature().getName();
        Method m = ((MethodSignature) joinPoint.getSignature()).getMethod();
        try {
            if (null != m && m.isAnnotationPresent(DS.class)) {
                // 根据注解 切换数据源
                DS td = m.getAnnotation(DS.class);
                String dbStr = td.value();
                DbContextHolder.setCurrentDsStr(dbStr);
                log.info("==========>current thread {} add dataSource[{}] to ThreadLocal, request method name is : {}",
                        Thread.currentThread().getName(), dbStr, method);
            } else {
                DbContextHolder.setCurrentDsStr(DbContextHolder.getDefaultDs());
                log.info("==========>use default datasource[{}] , request method name is :  {}",
                        DbContextHolder.getDefaultDs(), method);
            }
        } catch (Exception e) {
            log.error("==========>current thread {} add data to ThreadLocal error,{}", Thread.currentThread().getName(), e);
            throw e;
        }
    }


    /**
     * 执行完切面后,将线程共享中的数据源名称清空,
     * 数据源恢复为原来的默认数据源
     */
    @After("dataSourcePointCut()")
    public void after(JoinPoint joinPoint) {
        log.info("==========>clean datasource[{}]", DbContextHolder.getCurrentDsStr());
        DbContextHolder.clearCurrentDsStr();
    }
}

Этот класс представляет собой простую конфигурацию аспекта, функция состоит в том, чтобы переключить источник данных перед методом службы и настроитьDS()Аннотация, действующая на метод Service и указывающая, является ли он ведущим или подчиненным.

Конфигурация транзакции:

Вот в чем дело! Суть в том, что после описанных выше настроек несколько источников данных были настроены. Но транзакция недействительна в это время, независимо от того, ставили ли вы@TransactionalЭто не влияет ни на класс службы, ни на метод!MyRoutingDataSourceНаш пользовательский источник данных для менеджера транзакций. См. TransactionConfig:

@Aspect
@Configuration
@Slf4j
public class TransactionConfig {
    @Autowired
    ConfigurableApplicationContext applicationContext;
    private static final int TX_METHOD_TIMEOUT = 300;
    private static final String AOP_POINTCUT_EXPRESSION = "execution(*com.sqt..service..*Service.*(..))";
    
    @Bean(name = "txAdvice")
    public TransactionInterceptor txAdvice() {

        NameMatchTransactionAttributeSource source = new NameMatchTransactionAttributeSource();
        // 只读事务,不做更新操作
        RuleBasedTransactionAttribute readOnlyTx = new RuleBasedTransactionAttribute();
        readOnlyTx.setReadOnly(true);
        readOnlyTx.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

        // 当前存在事务就使用当前事务,当前不存在事务就创建一个新的事务
        RuleBasedTransactionAttribute requiredTx = new RuleBasedTransactionAttribute();
        requiredTx.setRollbackRules(Collections.singletonList(new RollbackRuleAttribute(Exception.class)));
        requiredTx.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        requiredTx.setTimeout(TX_METHOD_TIMEOUT);
        Map<String, TransactionAttribute> txMap = new HashMap<>();
        txMap.put("add*", requiredTx);
        txMap.put("save*", requiredTx);
        txMap.put("insert*", requiredTx);
        txMap.put("create*", requiredTx);
        txMap.put("update*", requiredTx);
        txMap.put("batch*", requiredTx);
        txMap.put("modify*", requiredTx);
        txMap.put("delete*", requiredTx);
        txMap.put("remove*", requiredTx);
        txMap.put("exec*", requiredTx);
        txMap.put("set*", requiredTx);
        txMap.put("do*", requiredTx);
        txMap.put("get*", readOnlyTx);
        txMap.put("query*", readOnlyTx);
        txMap.put("find*", readOnlyTx);
        txMap.put("*", requiredTx);
        source.setNameMap(txMap);
        TransactionInterceptor txAdvice = new TransactionInterceptor(transactionManager(), source);
        return txAdvice;
    }

    @Bean
    public Advisor txAdviceAdvisor(@Qualifier("txAdvice") TransactionInterceptor txAdvice) {
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression(AOP_POINTCUT_EXPRESSION);
        return new DefaultPointcutAdvisor(pointcut, txAdvice);
    }
    /**自定义 事务管理器 管理我们自定义的 MyRoutingDataSource 数据源
     * @return
     */
    @Bean(name = "transactionManager")
    public DataSourceTransactionManager transactionManager() {
        DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(applicationContext.getBean(MyRoutingDataSource.class));
        return transactionManager;
    }

Конфигурирование DataSourceTransactionManager — это главное!

Этот код немного длиннее, так как я настраиваю транзакцию конфигурации аспекта. Суть в том, чтобы настроить менеджер транзакций и передать наш источник данных динамической маршрутизации (MyRoutingDataSource) менеджеру транзакций, чтобы наша транзакция была отброшена!

Суммировать:

  1. Смысл настройки нескольких источников данных заключается в настройке наследования источников данных.AbstractRoutingDataSourceи зарегистрировать в нем несколько источников данных.
  2. Причина, по которой транзакция не вступает в силу, заключается в том, что менеджер транзакций Spring по умолчанию не берет на себя наш пользовательский источник данных.Решение состоит в том, чтобы настроить менеджер транзакций, чтобы вставить в него наш пользовательский источник данных.