SpringBoot2.X+MybatisPlus+多數據源+事務

前言

  1. 項目中用不用多數據源是一回事,你本身會不會又是另外一回事。
  2. SpringBoot2.0.8版本整合MybatisPlus實現多數據源很簡單,可是事務老是不生效?
  3. MybatisPlus提供了多數據源插件(連接),我可不能夠不用?
  4. 其實多數據源挺好配的,就是事務一直不生效。今天終於解決了。

項目結構:

主要的配置類就是這五個: DsAspect、 DataSourceConfiguration 、MyRoutingDataSource、MybatisConfiguration、TransactionConfig。後面我逐個的解釋下每一個類的做用。css

配置文件:

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,上代碼:html

@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;
    }

}
複製代碼

這個沒啥好解釋的,就是把配置文件封裝成了dataSource的Bean,其中MyRoutingDataSource纔是咱們要用的數據源,包括事務配置也要用它。java

MyRoutingDataSourcemysql

public class MyRoutingDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return DbContextHolder.getCurrentDsStr();
    }
}
複製代碼

其中AbstractRoutingDataSource是Spring的jdbc模塊下提供的一個抽象類,該類充當了DataSource的路由中介, 能在運行時, 根據某種key值來動態切換到真正的DataSource上,重寫其中的determineCurrentLookupKey()方法,能夠實現數據源的切換。意思就是想玩多數據源就使用這個類就對了。我這裏還用到了一個DbContextHolder工具類(至關於數據源的持有者),代碼以下,基本上是在網上拷貝的,其中作了一點點修改:web

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;
    }
複製代碼

MybatisConfiguration:

這是MybatisPlus配置類,若是你用的是Mybatis要簡單一點。由於Mybatis只須要配置SqlSessionFactory,而 MybatisPlus是配置MybatisSqlSessionFactoryBeanspring

@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;
    }

}
複製代碼

這裏配置的SqlSessionFactoryMybatisSqlSessionFactoryBean都須要MyRoutingDataSource這個數據源。sql

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();
    }
}
複製代碼

這個類就是一個簡單的切面配置,做用就是在Service方法以前切換數據源,自定義一個DS()註解,做用到Service方法上而且標明是master仍是slave便可。api

事務配置:

重點來了!重點來了!通過上面那些配置,多數據源已經配置好了。可是此時事務是不生效的,不管你是把@Transactional做用到Service類上仍是方法上,都不生效!此時你還須要配置一個事務管理器,而且把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是重點! ! ! 配置DataSourceTransactionManager是重點! ! !

因爲我是自定義的切面配置事務,因此這個代碼略長。重點是配置事務管理器,而且把咱們動態路由數據源(MyRoutingDataSource)交給事務管理器,這樣咱們的事務纔會回滾!

總結:

  1. 配置多數據源的重點是自定義一個數據源繼承AbstractRoutingDataSource,並將多個數據源註冊進去。
  2. 事務不生效緣由是Spring的默認事務管理器沒有接管咱們自定義的數據源.解決方法是配置一個事務管理器將咱們自定義的數據源塞給它
相關文章
相關標籤/搜索