JAVA对多个数据源的监控,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";

}

以上已经完成了动态数据源的切换,只需在Service方法上加上@TargetDataScoure注解并且指定需要切换的数据源名称,first数据源为缺省数据源。

如果使用@Transactional,缺省数据源的事务正常执行,如果使@TargetDataScoure切换为第二数据源并执行事务时,则数据源切换失败。

问题分析:

大多数项目只需要一个事务管理器。如果存在多数据源的情况,事务管理器是否会生效,由于spingboot约定大于配置的理念,

默认事务管理器无需我们再声明定义,而是默认加载时已经指定了其数据源,其数据源则为缺省数据源,如果执行事务时是第二数据源,则

还会以第一数据源做处理,这时则会异常。

第二数据源事务控制处理

定义事务管理器 并指定其对应管理的数据源和声明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);

}