分布式服务,经常是一个服务对应一个库,但也有的时候一个服务需要用到两个库,这个时候,一个服务就需要配置两个数据源,来支持业务的需要。
单数据源配置如下:
/**
* Druid的配置文件
* 用于监控数据库SQL
*/
@Configuration
@Slf4j
@RefreshScope
public class DruidConfiguration {
@Value("${spring.datasource.url}")
private String dbUrl;
………………………………
@Value("${spring.datasource.username}")
private String username;
/**
* 配置数据库连接池
* @return
*/
// @Bean //声明其为Bean实例
// @Primary //在同样的DataSource中,首先使用被标注的DataSource
public DataSource dataSource(){
DruidDataSource datasource = new DruidDataSource();
log.info(">>>>>>>>>>>>>开始配置druid连接池<<<<<<<<<<<<<<<<<<");
log.info(">>>>>>>>>>>>>>>>>>>>数据库连接 : dbUrl<<<<<<<<<<<<<<<<<<<"+dbUrl);
datasource.setUrl(this.dbUrl);
datasource.setUsername(username);
datasource.setPassword(password);
datasource.setDriverClassName(driverClassName);
//configuration
datasource.setInitialSize(initialSize);
datasource.setMinIdle(minIdle);
datasource.setMaxActive(maxActive);
datasource.setMaxWait(maxWait);
datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
datasource.setValidationQuery(validationQuery);
datasource.setTestWhileIdle(testWhileIdle);
datasource.setTestOnBorrow(testOnBorrow);
datasource.setTestOnReturn(testOnReturn);
datasource.setPoolPreparedStatements(poolPreparedStatements);
datasource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize);
try {
datasource.setFilters(filters);
} catch (SQLException e) {
log.error("druid configuration initialization filter", e);
}
datasource.setConnectionProperties(connectionProperties);
return datasource;
}
}
单数据源时,在服务启动时,会加载数据源配置类,将config服务中的数据源配置信息加载出来。
Q:多数据源是不应该配置多个数据源类嘞?
A:当然是配置多个喽,多数据源类如下:
@Configuration
@Slf4j
@RefreshScope
public class SecondConfig {
@Value("${spring.appprofiledatasource.url}")
private String dbUrl;
…………………………………………
@Value("${spring.appprofiledatasource.username}")
private String username;
private DataSource ds = null;
/**
* 配置数据库连接池
* @return
*/
public DataSource dataSource(){
if(ds != null) return ds;
DruidDataSource datasource = new DruidDataSource();
log.info(">>>>>>>>>>>>>开始配置AppProfileDataSource连接池<<<<<<<<<<<<<<<<<<");
log.info(">>>>>>>>>>>>>>>>>>>>数据库连接 : dbUrl<<<<<<<<<<<<<<<<<<<"+dbUrl);
datasource.setUrl(this.dbUrl);
datasource.setUsername(username);
datasource.setPassword(password);
datasource.setDriverClassName(driverClassName);
//configuration
datasource.setInitialSize(initialSize);
datasource.setMinIdle(minIdle);
datasource.setMaxActive(maxActive);
datasource.setMaxWait(maxWait);
datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
datasource.setValidationQuery(validationQuery);
datasource.setTestWhileIdle(testWhileIdle);
datasource.setTestOnBorrow(testOnBorrow);
datasource.setTestOnReturn(testOnReturn);
datasource.setPoolPreparedStatements(poolPreparedStatements);
datasource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize);
try {
datasource.setFilters(filters);
} catch (SQLException e) {
log.error("druid configuration initialization filter", e);
}
datasource.setConnectionProperties(connectionProperties);
ds=datasource;
return ds;
}
}
Q:此时有两数据源后,他俩谁首先加载呢?
A:由主数据源登场来指挥
@Configuration
@ConditionalOnClass({DruidConfiguration.class, AppProfileConfig.class})
public class PrimaryDataSource {
@Autowired
DruidConfiguration mysql;
@Autowired
SecondConfig odps;
@Bean
@Primary
public DataSource dataSource() {
MultipleDataSource dynamicDataSource = new MultipleDataSource();
// 默认数据源
DataSource o = odps.dataSource();
DataSource m = mysql.dataSource();
dynamicDataSource.setDefaultTargetDataSource(m);
// 配置多数据源
Map<Object, Object> datasources = new HashMap<>();
//datasources.put("dataSource", mysql);
datasources.put("secondDataSource", o);
dynamicDataSource.setTargetDataSources(datasources);
return dynamicDataSource;
}
}
如何保证主数据源可以指挥DruidOnfiguration和SecondConfig呢?此时就靠@Primary了,单数据源时,原本也有@Primary注解的,但是配置了多数据源后,@Primary注解就改放到主数据源中了。
Q:接下来的问题就是在使用时如何切换数据源呢?
A:继承spring的AbstractRountingDataSource来定义自己的动态数据源,这样就可以根据需要动态切换不同数据库的数据源了。
public class MultipleDataSource extends AbstractRoutingDataSource {
/**
* 实现AbstractRoutingDataSource的抽象方法,获取与数据源相关的key
* 跟进源码可以发现,此key与数据源绑定的key值,然后用于获取连接
*/
@Override
protected Object determineCurrentLookupKey() {
return DataSourceHolder.getDataSourceType();
}
}
与数据源相关的key,我们是在DataSourceHolder类中用ThreadLocal来保存的,
/**
* 保存当前线程数据源的key
*/
public final class DataSourceHolder {
private static final ThreadLocal<String> HOLDER = new InheritableThreadLocal<>();
/**
* 绑定当前线程数据源路由的key
* 在使用完成之后,必须调用removeRouteKey()方法删除
*/
public static void setDataSourceType(String dataSourceType) {
HOLDER.set(dataSourceType);
}
/**
* 获取当前线程的数据源路由的key
* @return
*/
public static String getDataSourceType() {
return HOLDER.get();
}
public static void clearDataSourceType() {
HOLDER.remove();
}
}
Q:那么问题又来了,数据源key,啥时候传进来嘞?
A:此时我们就需要使用spring的AOP编程在业务逻辑方法运行前将当前方法使用数据源的key从业务逻辑方法上自定义注解中解析数据源key,并添加到DataSourceHolder中。
切面类如下:
@Aspect
@Order(1)
@Component
public class MyAspect {
//指定要切的自定义注解类
@Pointcut("@annotation(cn.aa.common.aspect.TargetDataSource)")
public void pointcut() {
}
// @Before("execution(* cn.lefull.service..*(..))")
@Before("@annotation(cn.lefull.common.aspect.TargetDataSource)")
public void before(JoinPoint point) throws Exception {
TargetDataSource targetDataSource = AnnotationUtils.findAnnotation(((MethodSignature) point.getSignature()).getMethod(), TargetDataSource.class);
if (Objects.isNull(targetDataSource)) {
targetDataSource = AnnotationUtils.findAnnotation(point.getClass(), TargetDataSource.class);
if (Objects.isNull(targetDataSource)) {
Object proxy = point.getThis();
Field h = FieldUtils.getDeclaredField(proxy.getClass().getSuperclass(), "h", true);
AopProxy aopProxy = (AopProxy) h.get(proxy);
ProxyFactory advised = (ProxyFactory) FieldUtils.readDeclaredField(aopProxy, "advised", true);
Class<?> targetClass = advised.getProxiedInterfaces()[0];
targetDataSource = AnnotationUtils.findAnnotation(targetClass, TargetDataSource.class);
}
}
DataSourceHolder.setDataSourceType(Objects.nonNull(targetDataSource) ? targetDataSource.value() : "dataSource");
}
@After("pointcut()")
public void after(JoinPoint point) {
DataSourceHolder.clearDataSourceType();
}
}
自定义注解类:
@Documented
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TargetDataSource {
String value() default "dataSource";
}
应用
//在方法上加上自定义注解,值即为数据源key
@Override
@TargetDataSource("secondDataSource")
public int updateByPrimaryKey(IdfaInfo idfaInfo) {
return idfaInfoRepository.updateByPrimaryKey(idfaInfo);
}
使用时,在方法上加上自定义注解,并将值赋为第二数据源,然后在方法执行时,便可以加载第二数据源。
以上的配置,切面注解只能加载方法上,加到类上无法获取数据源,想要加到类上,需要修改切面类。