SpringBoot+Druid實現多數據源監控及事務控制

背景:一個項目中可能存在多數據源的狀況,雖然微服務中,通常是單數據源,可是例如後臺管理這些管理接口則不適合使用微服務來
   提供接口,因此業務庫也須要共存於後臺管理項目,然後臺管理項目中則有本身自己的一個權限數據庫,則就會存在多數據源的狀況。

   思路:Spring自己已經有實現數據源切換的功能類,能夠實如今項目運行時根據相應key值切換到對應的數據源DataSource上。   
   咱們只需擴展實現便可。
   並結合數據源動態切換爲須要切換數據源的方法增長註解,從而實現對帶有註解的攔截切換。

   問題:事務控制,缺省數據源生效,而切換爲第二數據源時,事務的數據源默認採用了缺省的。
         網上有說更改切面和事務的執行順序,可是試驗後並未成功。

如下是爲動態數據源切換,及缺省事務第二數據源的事務控制的實現方案,以springboot做爲基礎框架。java

使用druid作數據源監控與管理

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driverClassName: com.mysql.jdbc.Driver
    druid:
        first:  #數據源1
            url: jdbc:mysql://127.0.0.01:63885/demo?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8
            username: demo
            password: demo
        rongyuan:  #數據源2
            url: jdbc:mysql://127.0.0.01:63885/demo?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8
            username: demo
            password: demo
        initial-size: 10
        max-active: 100
        min-idle: 10
        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 1 FROM DUAL
        test-while-idle: true
        test-on-borrow: false
        test-on-return: false
        stat-view-servlet:
            enabled: true
            url-pattern: /druid/*
            #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

構建數據源及注入到動態數據源中

package io.y.common.datasources;

import java.util.HashMap;
import java.util.Map;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;

/**
 * @title 
 * @author zengzp
 * @time 2018年7月25日 上午11:22:46
 * @Description 
 */
@Configuration
// 加上此註解禁用數據源自動配置
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class DynamicDataSourceConfig {

    @Bean(name="first")
    @ConfigurationProperties("spring.datasource.druid.first")
    public DataSource firstDataSource(){
        return DruidDataSourceBuilder.create().build();
    }

    @Bean(name="rongyuan")
    @ConfigurationProperties("spring.datasource.druid.rongyuan")
    public DataSource secondDataSource(){
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    @Primary
    public DynamicDataSource dataSource(@Qualifier("first")DataSource firstDataSource, @Qualifier("rongyuan")DataSource secondDataSource) {
        Map<String, DataSource> targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceNames.FIRST, firstDataSource);
        targetDataSources.put(DataSourceNames.SECOND, secondDataSource);
        return new DynamicDataSource(firstDataSource, targetDataSources);
    }
    
}

繼承spring的動態實現,及重寫數據源的獲取方法

package io.y.common.datasources;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;


/**
 * @title 
 * @author zengzp
 * @time 2018年7月25日 上午 10:20:31
 * @Description 
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

    public DynamicDataSource(DataSource defaultTargetDataSource, Map<String, DataSource> targetDataSources) {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        super.setTargetDataSources(new HashMap<>(targetDataSources));
        super.afterPropertiesSet();
    }

    @Override
    protected Object determineCurrentLookupKey() {
        return getDataSource();
    }

    public static void setDataSource(String dataSource) {
        contextHolder.set(dataSource);
    }

    public static String getDataSource() {
        return contextHolder.get();
    }

    public static void clearDataSource() {
        contextHolder.remove();
    }

}

定義數據源切換註解

package io.y.common.datasources.annotation;

import java.lang.annotation.*;


/**
 * @title 多數據源註解
 * @author zengzp
 * @time 2018年7月25日 下午14:50:53
 * @Description 
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource {
    String name() default "";
}

定義切面,用來攔截帶註解的方法,並在方法執行前實現數據源的切換

package io.y.common.datasources.aspect;

import java.lang.reflect.Method;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import io.y.common.datasources.DataSourceNames;
import io.y.common.datasources.DynamicDataSource;
import io.y.common.datasources.annotation.TargetDataSource;


/**
 * @title 多數據源切面處理類
 * @author zengzp
 * @time 2018年7月25日 下午11:56:43
 * @Description 
 */
@Aspect
@Component
@Order(0)
public class DataSourceAspect {
    protected Logger logger = LoggerFactory.getLogger(getClass());

    @Pointcut("@annotation(io.y.common.datasources.annotation.TargetDataSource)")
    public void dataSourcePointCut() {

    }

    @Before("dataSourcePointCut()")
    public void around(JoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();

        TargetDataSource ds = method.getAnnotation(TargetDataSource.class);
        if(ds == null){
            DynamicDataSource.setDataSource(DataSourceNames.FIRST);
            logger.debug("set datasource is " + DataSourceNames.FIRST);
        }else {
            DynamicDataSource.setDataSource(ds.name());
            logger.debug("set datasource is " + ds.name());
        }
    }
    
    @AfterReturning("dataSourcePointCut()")
    public void after(){
        DynamicDataSource.clearDataSource();
        logger.debug("clean datasource");
    }

}

數據源名稱常量類

package io.y.common.datasources;

/**
 * @title 增長多數據源,在此配置
 * @author zengzp
 * @time 2018年7月25日 下午4:55:20
 * @Description 
 */
public interface DataSourceNames {
    String FIRST = "first";
    String SECOND = "rongyuan";

}
  1. 以上已經完成了動態數據源的切換,只需在Service方法上加上@TargetDataScoure註解而且指定須要切換的數據源名稱,first數據源爲缺省數據源。
  2. 若是使用@Transactional,缺省數據源的事務正常執行,若是使@TargetDataScoure切換爲第二數據源並執行事務時,則數據源切換失敗。
  3. 問題分析:mysql

    大多數項目只須要一個事務管理器。若是存在多數據源的狀況,事務管理器是否會生效,因爲spingboot約定大於配置的理念,
       默認事務管理器無需咱們再聲明定義,而是默認加載時已經指定了其數據源,其數據源則爲缺省數據源,若是執行事務時是第二數據源,則
       還會以第一數據源作處理,這時則會異常。

第二數據源事務控制處理

  1. 定義事務管理器 並指定其對應管理的數據源和聲明name
package io.y.common.datasources;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;

/**
 * @title 多事物管理器配置
 * @author zengzp
 * @time 2018年7月25日 下午4:55:33
 * @Description 
 */
@Configuration
public class TransactionConfig {
    
    public final static String DEFAULT_TX = "defaultTx";
    
    public final static String RONGYUAN_TX = "rongyuanTx";
    
    @Bean(name=TransactionConfig.DEFAULT_TX)
    public DataSourceTransactionManager transaction(@Qualifier(DataSourceNames.FIRST)DataSource firstDataSource){
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(firstDataSource);
        return dataSourceTransactionManager;
    }
    
    @Bean(name=TransactionConfig.RONGYUAN_TX)
    public DataSourceTransactionManager rongyuanTransaction(@Qualifier(DataSourceNames.SECOND) DataSource rongyuanDataScoure){
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(rongyuanDataScoure);
        return dataSourceTransactionManager;
    }

}
2.事務管理器使用

在@Transactional上指定使用哪一個名稱的事務管理器

@Override
    @Transactional(value=TransactionConfig.RONGYUAN_TX, rollbackFor=Exception.class)
    @TargetDataSource(name = "rongyuan")
    public void deleteBatch(Integer[] advertIds) {
        if (advertIds == null || advertIds.length <= 0) {
            throw new IllegalArgumentException("參數異常");
        }
        advertDao.deleteBatch(advertIds);
    }
相關文章
相關標籤/搜索