springboot 动态数据源的实现方法(Mybatis+Druid)
作者:时光沉旧了少年 发布时间:2021-07-26 04:27:00
Spring多数据源实现的方式大概有2中,一种是新建多个MapperScan扫描不同包,另外一种则是通过继承AbstractRoutingDataSource实现动态路由。今天作者主要基于后者做的实现,且方式1的实现比较简单这里不做过多探讨。
实现方式
方式1的实现(核心代码):
@Configuration
@MapperScan(basePackages = "com.goofly.test1", sqlSessionTemplateRef = "test1SqlSessionTemplate")
public class DataSource1Config1 {
@Bean(name = "dataSource1")
@ConfigurationProperties(prefix = "spring.datasource.test1")
@Primary
public DataSource testDataSource() {
return DataSourceBuilder.create().build();
}
// .....略
}
@Configuration
@MapperScan(basePackages = "com.goofly.test2", sqlSessionTemplateRef = "test1SqlSessionTemplate")
public class DataSourceConfig2 {
@Bean(name = "dataSource2")
@ConfigurationProperties(prefix = "spring.datasource.test2")
@Primary
public DataSource testDataSource() {
return DataSourceBuilder.create().build();
}
// .....略
}
方式2的实现(核心代码):
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
private static final Logger log = Logger.getLogger(DynamicRoutingDataSource.class);
@Override
protected Object determineCurrentLookupKey() {
//从ThreadLocal中取值
return DynamicDataSourceContextHolder.get();
}
}
第1种方式虽然实现比较加单,劣势就是不同数据源的mapper文件不能在同一包名,就显得不太灵活了。所以为了更加灵活的作为一个组件的存在,作者采用的第二种方式实现。
设计思路
当请求经过被注解修饰的类后,此时会进入到切面逻辑中。
切面逻辑会获取注解中设置的key值,然后将该值存入到ThreadLocal中
执行完切面逻辑后,会执行AbstractRoutingDataSource.determineCurrentLookupKey()方法,然后从ThreadLocal中获取之前设置的key值,然后将该值返回。
由于AbstractRoutingDataSource的targetDataSources是一个map,保存了数据源key和数据源的对应关系,所以能够顺利的找到该对应的数据源。
源码解读
org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource,如下:
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
private Map<Object, Object> targetDataSources;
private Object defaultTargetDataSource;
private boolean lenientFallback = true;
private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
private Map<Object, DataSource> resolvedDataSources;
private DataSource resolvedDefaultDataSource;
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = determineCurrentLookupKey();
DataSource dataSource = this.resolvedDataSources.get(lookupKey);
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;
}
/**
* Determine the current lookup key. This will typically be
* implemented to check a thread-bound transaction context.
* <p>Allows for arbitrary keys. The returned key needs
* to match the stored lookup key type, as resolved by the
* {@link #resolveSpecifiedLookupKey} method.
*/
protected abstract Object determineCurrentLookupKey();
//........略
targetDataSources是一个map结构,保存了key与数据源的对应关系;
dataSourceLookup是一个DataSourceLookup类型,默认实现是JndiDataSourceLookup。点开该类源码会发现,它实现了通过key获取DataSource的逻辑。当然,这里可以通过setDataSourceLookup()来改变其属性,因为关于此处有一个坑,后面会讲到。
public class JndiDataSourceLookup extends JndiLocatorSupport implements DataSourceLookup {
public JndiDataSourceLookup() {
setResourceRef(true);
}
@Override
public DataSource getDataSource(String dataSourceName) throws DataSourceLookupFailureException {
try {
return lookup(dataSourceName, DataSource.class);
}
catch (NamingException ex) {
throw new DataSourceLookupFailureException(
"Failed to look up JNDI DataSource with name '" + dataSourceName + "'", ex);
}
}
}
组件使用
多数据源
# db1
spring.datasource.master.url = jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&useSSL=false
spring.datasource.master.username = root
spring.datasource.master.password = 123456
spring.datasource.master.driverClassName = com.mysql.jdbc.Driver
spring.datasource.master.validationQuery = true
spring.datasource.master.testOnBorrow = true
## db2
spring.datasource.slave.url = jdbc:mysql://127.0.0.1:3306/test1?useUnicode=true&characterEncoding=utf8&useSSL=false
spring.datasource.slave.username = root
spring.datasource.slave.password = 123456
spring.datasource.slave.driverClassName = com.mysql.jdbc.Driver
spring.datasource.slave.validationQuery = true
spring.datasource.slave.testOnBorrow = true
#主数据源名称
spring.maindb=master
#mapperper包路径
mapper.basePackages =com.btps.xli.multidb.demo.mapper
单数据源
为了让使用者能够用最小的改动实现最好的效果,作者对单数据源的多种配置做了兼容。
示例配置1(配置数据源名称):
spring.datasource.master.url = jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&useSSL=false
spring.datasource.master.username = root
spring.datasource.master.password = 123456
spring.datasource.master.driverClassName = com.mysql.jdbc.Driver
spring.datasource.master.validationQuery = true
spring.datasource.master.testOnBorrow = true
# mapper包路径
mapper.basePackages = com.goofly.xli.multidb.demo.mapper
# 主数据源名称
spring.maindb=master
示例配置2(不配置数据源名称):
spring.datasource.url = jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&useSSL=false
spring.datasource.username = root
spring.datasource.password = 123456
spring.datasource.driverClassName = com.mysql.jdbc.Driver
spring.datasource.validationQuery = true
spring.datasource.testOnBorrow = true
# mapper包路径
mapper.basePackages = com.goofly.xli.multidb.demo.mapper
踩坑之路
多数据源的循环依赖
Description:
The dependencies of some of the beans in the application context form a cycle:
happinessController (field private com.db.service.HappinessService com.db.controller.HappinessController.happinessService)
↓
happinessServiceImpl (field private com.db.mapper.MasterDao com.db.service.HappinessServiceImpl.masterDao)
↓
masterDao defined in file [E:\GitRepository\framework-gray\test-db\target\classes\com\db\mapper\MasterDao.class]
↓
sqlSessionFactory defined in class path resource [com/goofly/xli/datasource/core/DynamicDataSourceConfiguration.class]
┌─────┐
| dynamicDataSource defined in class path resource [com/goofly/xli/datasource/core/DynamicDataSourceConfiguration.class]
↑ ↓
| firstDataSource defined in class path resource [com/goofly/xli/datasource/core/DynamicDataSourceConfiguration.class]
↑ ↓
| dataSourceInitializer
解决方案:
在Spring boot启动的时候排除DataSourceAutoConfiguration即可。如下:
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class DBMain {
public static void main(String[] args) {
SpringApplication.run(DBMain.class, args);
}
}
但是作者在创建多数据源的时候由于并未创建多个DataSource的Bean,而是只创建了一个即需要做动态数据源的那个Bean。 其他的DataSource则直接创建实例然后存放在Map里面,然后再设置到DynamicRoutingDataSource#setTargetDataSources即可。
因此这种方式也不会出现循环依赖的问题!
动态刷新数据源
笔者在设计之初是想构建一个动态刷新数据源的方案,所以利用了SpringCloud的@RefreshScope去标注数据源,然后利用RefreshScope#refresh实现刷新。但是在实验的时候发现由Druid创建的数据源会因此而关闭,由Spring的DataSourceBuilder创建的数据源则不会发生任何变化。
最后关于此也没能找到解决方案。同时思考,如果只能的可以实现动态刷新的话,那么数据源的原有连接会因为刷新而中断吗还是会有其他处理?
多数据源事务
有这么一种特殊情况,一个事务中调用了两个不同数据源,这个时候动态切换数据源会因此而失效。
翻阅了很多文章,大概找了2中解决方案,一种是Atomikos进行事务管理,但是貌似性能并不是很理想。
另外一种则是通过优先级控制,切面的的优先级必须要大于数据源的优先级,用注解@Order控制。
此处留一个坑!
git代码地址
来源:https://segmentfault.com/a/1190000017989247
![](https://www.aspxhome.com/images/zang.png)
![](https://www.aspxhome.com/images/jiucuo.png)
猜你喜欢
- 一.概述在微服务框架中,一个由客户端发起的请求在后端系统中会经过多个不同的的服务节点调用来协同产生最后的请求结果,每一个前段请求都会形成一条
- 调整数组顺序使奇数位于偶数之前1. 题目描述输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的
- 本文实例讲述了Java实现求解一元n次多项式的方法。分享给大家供大家参考,具体如下:项目需要做趋势预测,采用线性拟合、2阶曲线拟合和指数拟合
- 本文实例为大家分享了Java实现五子棋游戏的具体代码,供大家参考,具体内容如下学习目的:熟悉java中swing类与java基础知识的巩固.
- 前言JAVA中在运用数组进行排序功能时,一般有四种方法:快速排序法、冒泡法、选择排序法、插入排序法。本文就给大家介绍了关于最简单易懂的jav
- 任务超时处理是比较常见的需求,比如在进行一些比较耗时的操作(如网络请求)或者在占用一些比较宝贵的资源(如数据库连接)时,我们通常需要给这些操
- JavaFX主要致力于富客户端开发,以弥补swing的缺陷,主要提供图形库与media库,支持audio,video,graphic,ani
- public static String getCharset(File file) { &n
- 如下所示:public static String reThreeStr(String ss){boolean result= ss.mat
- springboot集成swagger3swagger3的springboot启动器jar包<!-- https://mvnrepos
- File 类:文件和目录路径名的抽象表示。注意:File 类只能操作文件的属性,文件的内容是不能操作的。1、File 类的字段我们知道,各个
- 一、案例场景遇到过这样的场景,在定义一个static修饰的Map时,使用了大量的put()方法赋值,就类似这样——public static
- 目前很多业务使用微服务架构,服务模块划分有这2种方式:服务功能划分业务划分不管哪种方式,一次接口调用都需要多个服务协同完成,其中一个服务出现
- 1.封装1.介绍封装是指把抽象出的属性和方法封装在一起,数据被保护在内部,程序的其他部分只能通过被授权的方法,才能对数据操作。2.封装的理解
- 这篇文章主要介绍了Spring如何在一个事务中开启另一个事务,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需
- 本文实例讲述了Hibernate批量处理海量数据的方法。分享给大家供大家参考,具体如下:Hibernate批量处理海量其实从性能上考虑,它是
- 咱们废话不多说进入主题、系统主页展示:用户登录后进行系统首页:主要功能模块如下、分角色管理、超级管理员拥有最高权限、可以进行菜单灵活控制、用
- 业务场景通常微服务对于用户认证信息解析有两种方案在 gateway 就解析用户的 token 然后路由的时候把 userId 等相关信息添加
- 面向对象语言对事物的体现都是以对象的形式,所以为了方便对多个对象的操作,就对对象进行存储,集合就是存储对象最常用的一种方式。数组虽然也可以存
- 背景在接口请求过程中,传递json对象,springboot转换为实体VO对象后,所有属性都为null。post请求:后台接收请求:当时就懵