SpringBoot+Druid实现多数据源监控及事务控制

   背景:一个项目中可能存在多数据源的情况,虽然微服务中,一般是单数据源,但是例如后台管理这些管理接口则不适合使用微服务来
   提供接口,所以业务库也需要共存于后台管理项目,而后台管理项目中则有自己本身的一个权限数据库,则就会存在多数据源的情况。

   思路:Spring本身已经有实现数据源切换的功能类,可以实现在项目运行时根据相应key值切换到对应的数据源DataSource上。   
   我们只需扩展实现即可。
   并结合数据源动态切换为需要切换数据源的方法增加注解,从而实现对带有注解的拦截切换。

   问题:事务控制,缺省数据源生效,而切换为第二数据源时,事务的数据源默认采用了缺省的。
         网上有说更改切面和事务的执行顺序,但是试验后并未成功。

以下是为动态数据源切换,及缺省事务第二数据源的事务控制的实现方案,以springboot作为基础框架。

使用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 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 contextHolder = new ThreadLocal<>();

    public DynamicDataSource(DataSource defaultTargetDataSource, Map 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. 问题分析:

       大多数项目只需要一个事务管理器。如果存在多数据源的情况,事务管理器是否会生效,由于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);
    }

你可能感兴趣的:(javaspringboot)