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