Spring 动态数据源

动态数据源是什么?解决了什么问题?

在实际的开发中,同一个项目中使用多个数据源是很常见的场景。比如,一个读写分离的项目存在主数据源与读数据源。

所谓动态数据源,就是通过Spring的一些配置来自动控制某段数据操作逻辑是走哪一个数据源。举个读写分离的例子,项目中引用了两个数据源,master、slave。通过Spring配置或扩展能力来使得一个接口中调用了查询方法会自动使用slave数据源。

一般实现这种效果可以通过:

  1. 使用@MapperScan注解指定某个包下的所有方法走固定的数据源(这个比较死板些,会产生冗余代码,到也可以达到效果,可以作为临时方案使用);
  2. 使用注解+AOP+AbstractRoutingDataSource的形式来指定某个方法下的数据库操作是走那个数据源。

关键核心类

这里主要介绍通过注解+AOP+AbstractRoutingDataSource的联动来实现动态数据源的方式。

一切的起点是AbstractRoutingDataSource这个类,此类实现了 DataSource 接口

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {

    // .... 省略 ... 

    @Nullable
private Map<Object, Object> targetDataSources; @Nullable
private Map<Object, DataSource> resolvedDataSources; public void setTargetDataSources(Map<Object, Object> targetDataSources) {
this.targetDataSources = targetDataSources;
} public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
this.defaultTargetDataSource = defaultTargetDataSource;
} @Override
public void afterPropertiesSet() { // 初始化 targetDataSources、resolvedDataSources
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
}
this.resolvedDataSources = new HashMap<>(this.targetDataSources.size());
this.targetDataSources.forEach((key, value) -> {
Object lookupKey = resolveSpecifiedLookupKey(key);
DataSource dataSource = resolveSpecifiedDataSource(value);
this.resolvedDataSources.put(lookupKey, dataSource);
});
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
}
} @Override
public Connection getConnection() throws SQLException {
return determineTargetDataSource().getConnection();
} @Override
public Connection getConnection(String username, String password) throws SQLException {
return determineTargetDataSource().getConnection(username, password);
} /**
* Retrieve the current target DataSource. Determines the
* {@link #determineCurrentLookupKey() current lookup key}, performs
* a lookup in the {@link #setTargetDataSources targetDataSources} map,
* falls back to the specified
* {@link #setDefaultTargetDataSource default target DataSource} if necessary.
* @see #determineCurrentLookupKey()
*/
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized"); // @1 start
Object lookupKey = determineCurrentLookupKey();
DataSource dataSource = this.resolvedDataSources.get(lookupKey);
// @1 end if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
}
return dataSource;
} /**
* 返回一个key,这个key用来从 resolvedDataSources 数据源中获取具体的数据源对象 见 @1
*/
@Nullable
protected abstract Object determineCurrentLookupKey(); }

可以看到 AbstractRoutingDataSource 中有个可扩展抽象方法 determineCurrentLookupKey(),利用这个方法可以来实现动态数据源效果。

从0写一个简单动态数据源组件

从上一个part我们知道可以通过实现AbstractRoutingDataSource的 determineCurrentLookupKey() 方法动态设置一个key,然后

在配置类下通过setTargetDataSources()方法设置我们提前准备好的DataSource Map。

注解、常量定义、ThreadLocal 准备


/**
* @author axin
* @Summary 动态数据源注解定义
*/
public @interface MyDS {
String value() default "default";
} /**
* @author axin
* @Summary 动态数据源常量
*/
public interface DSConst { String 默认 = "default"; String 主库 = "master"; String 从库 = "slave"; String 统计 = "stat";
}
/**
* @author axin
* @Summary 动态数据源 ThreadLocal 工具
*/
public class DynamicDataSourceHolder { //保存当前线程所指定的DataSource
private static final ThreadLocal<String> THREAD_DATA_SOURCE = new ThreadLocal<>(); public static String getDataSource() {
return THREAD_DATA_SOURCE.get();
} public static void setDataSource(String dataSource) {
THREAD_DATA_SOURCE.set(dataSource);
} public static void removeDataSource() {
THREAD_DATA_SOURCE.remove();
}
}

自定一个 AbstractRoutingDataSource 类

/**
* @author axin
* @Summary 动态数据源
*/
public class DynamicDataSource extends AbstractRoutingDataSource { /**
* 从数据源中获取目标数据源的key
* @return
*/
@Override
protected Object determineCurrentLookupKey() {
// 从ThreadLocal中获取key
String dataSourceKey = DynamicDataSourceHolder.getDataSource();
if (StringUtils.isEmpty(dataSourceKey)) {
return DSConst.默认;
}
return dataSourceKey;
}
}

AOP 实现

/**
* @author axin
* @Summary 数据源切换AOP
*/
@Slf4j
@Aspect
@Service
public class DynamicDataSourceAOP { public DynamicDataSourceAOP() {
log.info("/*---------------------------------------*/");
log.info("/*---------- ----------*/");
log.info("/*---------- 动态数据源初始化... ----------*/");
log.info("/*---------- ----------*/");
log.info("/*---------------------------------------*/");
} /**
* 切点
*/
@Pointcut(value = "@annotation(xxx.xxx.MyDS)")
private void method(){} /**
* 方法执行前,切换到指定的数据源
* @param point
*/
@Before("method()")
public void before(JoinPoint point) {
MethodSignature methodSignature = (MethodSignature) point.getSignature();
//获取被代理的方法对象
Method targetMethod = methodSignature.getMethod();
//获取被代理方法的注解信息
CultureDS cultureDS = AnnotationUtils.findAnnotation(targetMethod, CultureDS.class); // 方法链条最外层的动态数据源注解优先级最高
String key = DynamicDataSourceHolder.getDataSource(); if (!StringUtils.isEmpty(key)) {
log.warn("提醒:动态数据源注解调用链上出现覆盖场景,请确认是否无问题");
return;
} if (cultureDS != null ) {
//设置数据库标志
DynamicDataSourceHolder.setDataSource(MyDS.value());
}
} /**
* 释放数据源
*/
@AfterReturning("method()")
public void doAfter() {
DynamicDataSourceHolder.removeDataSource();
}
}

DataSourceConfig 配置

通过以下代码来将动态数据源配置到 SqlSession 中去

/**
* 数据源的一些配置,主要是配置读写分离的sqlsession,这里没有使用mybatis annotation
*
@Configuration
@EnableTransactionManagement
@EnableAspectJAutoProxy
class DataSourceConfig { /** 可读写的SQL Session */
public static final String BEANNAME_SQLSESSION_COMMON = "sqlsessionCommon";
/** 事务管理器的名称,如果有多个事务管理器时,需要指定beanName */
public static final String BEANNAME_TRANSACTION_MANAGER = "transactionManager"; /** 主数据源,必须配置,spring启动时会执行初始化数据操作(无论是否真的需要),选择查找DataSource class类型的数据源 配置通用数据源,可读写,连接的是主库 */
@Bean
@Primary
@ConfigurationProperties(prefix = "datasource.common")
public DataSource datasourceCommon() {
// 数据源配置 可更换为其他实现方式
return DataSourceBuilder.create().build();
} /**
* 动态数据源
* @returnr
*/
@Bean
public DynamicDataSource dynamicDataSource() {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
LinkedHashMap<Object, Object> hashMap = Maps.newLinkedHashMap();
hashMap.put(DSConst.默认, datasourceCommon());
hashMap.put(DSConst.主库, datasourceCommon());
hashMap.put(DSConst.从库, datasourceReadOnly());
hashMap.put(DSConst.统计, datasourceStat()); // 初始化数据源 Map
dynamicDataSource.setTargetDataSources(hashMap);
dynamicDataSource.setDefaultTargetDataSource(datasourceCommon());
return dynamicDataSource;
} /**
* 配置事务管理器
*/
@Bean(name = BEANNAME_TRANSACTION_MANAGER)
public DataSourceTransactionManager createDataSourceTransactionManager() {
DataSource dataSource = this.datasourceCommon();
DataSourceTransactionManager manager = new DataSourceTransactionManager(dataSource);
return manager;
} /**
* 配置读写sqlsession
*/
@Primary
@Bean(name = BEANNAME_SQLSESSION_COMMON)
public SqlSession readWriteSqlSession() throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean(); // 设置动态数据源
factory.setDataSource(this.dynamicDataSource());
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
factory.setConfigLocation(resolver.getResource("mybatis/mybatis-config.xml"));
factory.setMapperLocations(resolver.getResources("mybatis/mappers/**/*.xml"));
return new SqlSessionTemplate(factory.getObject());
} }

总结

综上,实现了一个简单的Spring动态数据源功能,使用的时候,仅需要在目标方法上加上 @MyDS 注解即可。许多开源组件,会在现有的基础上增加一个扩展功能,比如路由策略等等。

Spring 注解动态数据源设计实践的更多相关文章

  1. Spring实现动态数据源,支持动态加入、删除和设置权重及读写分离

    当项目慢慢变大,訪问量也慢慢变大的时候.就难免的要使用多个数据源和设置读写分离了. 在开题之前先说明下,由于项目多是使用Spring,因此下面说到某些操作可能会依赖于Spring. 在我经历过的项目中 ...

  2. Spring Boot 动态数据源(Spring 注解数据源)

    本文实现案例场景:某系统除了需要从自己的主要数据库上读取和管理数据外,还有一部分业务涉及到其他多个数据库,要求可以在任何方法上可以灵活指定具体要操作的数据库. 为了在开发中以最简单的方法使用,本文基于 ...

  3. Spring配置动态数据源-读写分离和多数据源

    在现在互联网系统中,随着用户量的增长,单数据源通常无法满足系统的负载要求.因此为了解决用户量增长带来的压力,在数据库层面会采用读写分离技术和数据库拆分等技术.读写分离就是就是一个Master数据库,多 ...

  4. spring boot动态数据源方案

    动态数据源 1.背景 动态数据源在实际的业务场景下需求很多,而且想要沟通多数据库确实需要封装这种工具,针对于bi工具可能涉及到从不同的业务库或者数据仓库中获取数据,动态数据源就更加有意义. 2.依赖 ...

  5. 43. Spring Boot动态数据源(多数据源自动切换)【从零开始学Spring Boot】

    [视频&交流平台] àSpringBoot视频 http://study.163.com/course/introduction.htm?courseId=1004329008&utm ...

  6. Spring Boot 动态数据源(多数据源自己主动切换)

    本文实现案例场景: 某系统除了须要从自己的主要数据库上读取和管理数据外.另一部分业务涉及到其它多个数据库,要求能够在不论什么方法上能够灵活指定详细要操作的数据库. 为了在开发中以最简单的方法使用,本文 ...

  7. (43). Spring Boot动态数据源(多数据源自动切换)【从零开始学Spring Boot】

    在上一篇我们介绍了多数据源,但是我们会发现在实际中我们很少直接获取数据源对象进行操作,我们常用的是jdbcTemplate或者是jpa进行操作数据库.那么这一节我们将要介绍怎么进行多数据源动态切换.添 ...

  8. Spring Boot 动态数据源(多数据源自动切换)

    本文实现案例场景: 某系统除了需要从自己的主要数据库上读取和管理数据外,还有一部分业务涉及到其他多个数据库,要求可以在任何方法上可以灵活指定具体要操作的数据库. 为了在开发中以最简单的方法使用,本文基 ...

  9. SaaS 系统架构,Spring Boot 动态数据源实现!

    这段时候在准备从零开始做一套SaaS系统,之前的经验都是开发单数据库系统并没有接触过SaaS系统,所以接到这个任务的时候也有也些头疼,不过办法部比困难多,难得的机会. 在网上找了很多关于SaaS的资料 ...

随机推荐

  1. Trie、并查集、堆、Hash表学习过程以及遇到的问题

    Trie.并查集.堆.Hash表: Trie 快速存储和查找字符串集合 字符类型统一,将单词在最后一个字母结束的位置上打上标记 练习题:Trie字符串统计 import java.util.*; pu ...

  2. 前端 | JS Promise:axios 请求结果后面的 .then() 是什么意思?

    Promise 是JS中一种处理异步操作的机制,在现在的前端代码中使用频率很高.Promise 这个词可能有点眼生,但你肯定见过 axios.get(...).then(res => {...} ...

  3. 显示目录下的内容--ls

    ls               显示当前目录下的所有文件或者文件夹,但不包括 . 和 .. ls -a                  显示当前目录下的所有文件或者文件夹 ls -l        ...

  4. DSP代码搬运至RAM运行

    程序运行过程中,有些函数或程序段和数据等经常调用,正常情况下在FLASH中运行处理消耗时间和资源较大,通常将其移植至RAM中运行,可提高运行效率. 如: 1 #pragma CODE_SECTION( ...

  5. Dubbo 编解码那些事

    一.背景 笔者在一次维护基础公共组件的过程中,不小心修改了类的包路径.糟糕的是,这个类被各业务在facade中进行了引用.传递.幸运的是,同一个类,在提供者和消费者的包路径不一致,没有引起各业务报错. ...

  6. 01_pytorch和tensorflow的区别

    Pytorch和TensorFlow的区别 目录 引言 pytorch和tensorflow的功能 torch和tf的区别 torch tf Torch和tf到底用哪个 总结 引言 在这里,我们长话短 ...

  7. OO_Unit1总结

    OO的第一单元作业告一段落,这周是总结而不是码代码,甚至心中有点落空感.OO课给我的一周构建了一个完整的循环,从周二的作业发布到接下来几天的思考和构建程序,再到面向中测进行一部分的bug修复,最后到互 ...

  8. 中小型前端团队代码规范工程化最佳实践 - ESLint

    前言 There are a thousand Hamlets in a thousand people's eyes. 一千个程序员,就有一千种代码风格.在前端开发中,有几个至今还在争论的代码风格差 ...

  9. Day14_79_IO+Properties联合应用

    IO+Properties联合应用 - dbinfo文件中可以存放<key=value> - 像dbinfo这样的文件我们叫做配置文件,配置文件的作用是使程序更加灵活 - 一般在程序中可变 ...

  10. 01-Verilog基本语法元素

    不知道能不能更新完,毕竟咱学校计院对硬件向来不太重视,现在对竞赛也不咋地重视了,也不加分,也没啥用.嘛,就随便写写玩玩吧. 一只狸无聊的时候对Verilog的业余描述笔记:以<Verilog数字 ...