需求背景


去年底,公司项目有一个需求中有个接口需要用到平台、算法、大数据等三个不同数据库的数据进行计算、组装以及最后的展示,当时这个需求是另一个老同事在做,我只是负责自己的部分。

直到今年回来了,这个项目也做得差不多了,这会儿才有时间区仔细看同事的代码,是怎么去实现多数据源动态切换的。


扩展:当业务也来越复杂,数据量越来越庞大时,就可能会对数据库进行分库分表、读写分离等设计来减轻压力、提高系统性能,那么多数据源动态切换势必是必不可少!

经过了一星期零零碎碎的下班时间,从了解原理、实现、优化的过程,自己终于总算是弄出来了,接下来一起看看!

思考


  1. 如何让Spring知道我们配置了多个数据源?

  2. 配置了多个数据源后,Spring是如何决定使用哪一个数据源?

  3. Spring是如何动态切换数据源?

分析及实现


  1. 配置多数据源信息

spring:
datasource:
local:
database: local
username: root
password:
jdbc-url: jdbc:mysql://ip:port/test_user?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver
server:
database: server
username: root
password:
jdbc-url: jdbc:mysql://ip:port/test_user?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver

这是我的两个数据库:本地数据库+个人服务器数据库


服务器数据库



本地数据库




  1. Spring如何获取配置好的多个数据源信息?

Spring提供了三种方式进行获取

@Value注解获取(实体类需配合@Component),最简单,但当配置信息较多时,写起来比较繁琐

@ConfigurationProperties注解获取,需要定义前缀,可大批量获取配置信息

@Environment注解从Spring环境中获取,实现较为复杂,本人很少用


同事使用的方式是第一种方式,但是我个人觉得这样侵入性较大,每增加一个数据源,就要重新定义变量然后用@Value去重新配置,很麻烦,所以我就选择了第二种方式


通过@ConfigurationProperties注解获取,需要定义前缀,可大批量获取配置信息


@Data
@Component
@ConfigurationProperties(prefix = "spring.datasource")
public class DBProperties { private HikariDataSource server; private HikariDataSource local;
}

将所有的数据源加载到Spring中,可供其选择使用


@Slf4j
@Configuration
public class DataSourceConfig { @Autowired
private DBProperties dbProperties; @Bean(name = "multiDataSource")
public MultiDataSource multiDataSource(){
MultiDataSource multiDataSource = new MultiDataSource();
//1.设置默认数据源
multiDataSource .setDefaultTargetDataSource(dbProperties.getLocal());
//2.配置多数据源
HashMap<Object, Object> dataSourceMap = Maps.newHashMap(); dataSourceMap.put("local", dbProperties.getLocal());
dataSourceMap.put("server", dbProperties.getServer());
//3.存放数据源集
multiDataSource.setTargetDataSources(dataSourceMap);
return multiDataSource;
}
}

如此之后,确实是可以读取YML中的数据源信息,但是总觉得怪怪的。

果然!当我实现了整个功能后,我发现,如果我想要再加一个数据源,我还是得去求改DBProperties和DataSourceConfig这两类的内容,就很烦,我这个人比较懒,所以我就将这部分内容优化了一下:


优化后的YML

spring:
datasource:
names:
- database: dataSource0
username: root
password:
jdbc-url: jdbc:mysql://ip:port/test_user?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver
- database: dataSource1
username: root
password:
jdbc-url: jdbc:mysql://ip:port/test_user?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver

优化后的DBProperties

@Data
@Component
@ConfigurationProperties(prefix = "spring.datasource")
public class DBProperties { private List<HikariDataSource> DBNames; }

优化后的DataSourceConfig


@Slf4j
@Configuration
public class DataSourceConfig { @Autowired
private DBProperties dbProperties; @Bean(name = "multiDataSource")
public MultiDataSource multiDataSource(){
MultiDataSource multiDataSource = new MultiDataSource(); List<HikariDataSource> names = dbProperties.getNames();
if (CollectionUtils.isEmpty(names)){
throw new RuntimeException("please configure the data source!");
} multiDataSource.setDefaultTargetDataSource(names.get(0)); HashMap<Object, Object> dataSourceMap = Maps.newHashMap();
int i = 0;
for (HikariDataSource name : names) {
dataSourceMap.put("dataSource-"+(i++),name);
} multiDataSource.setTargetDataSources(dataSourceMap);
return multiDataSource;
}
}

这样子,我之后无论配置了多少个数据源信息,我都不需要再去修改配置代码



  1. Spring如何选择使用数据源?

选择一个数据源


通过继承AbstractRoutingDataSource接口,重写determineCurrentLookupKey方法,选择具体的数据源


@Slf4j
public class MultiDataSource extends AbstractRoutingDataSource { @Override
protected Object determineCurrentLookupKey() { return MultiDataSourceHolder.getDatasource(); } }

利用ThreadLocal实现数据源线程隔离


public class MultiDataSourceHolder {

    private static final ThreadLocal<String> threadLocal =new ThreadLocal<>();

    public static void setDatasource(String datasource){
threadLocal.set(datasource);
} public static String getDatasource(){
return threadLocal.get();
} public static void clearDataSource(){
threadLocal.remove();
} }

准备工作做好,下面开始将动态切换操作串联起来


利用AOP切面+自定义注解


自定义注解


@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MultiDataSource { String DBName(); }

AOP切面


@Slf4j
@Aspect
@Component
public class DataSourceAspect { @Pointcut(value = "@within(com.xiaozhao.base.aop.annotation.MultiDataSource) || @annotation(com.xiaozhao.base.aop.annotation.MultiDataSource)")
public void dataSourcePointCut(){} @Before("dataSourcePointCut() && @annotation(multiDataSource)")
public void before(MultiDataSource multiDataSource){ String dbName = multiDataSource.DBName(); if (StringUtils.hasLength(dbName)){ MultiDataSourceHolder.setDatasource(multiDataSource.DBName());
log.info("current dataSourceName ====== "+dbName); }else { log.info("switch datasource fail, use default, or please configure the data source for the annotations,"); }
} @After("dataSourcePointCut()")
public void after(){
MultiDataSourceHolder.clearDataSource();
}
}

好了!功能依然实现,打完收工!


。。。。


如果我工作中也这样,估计要被测试打死!为了敷衍一下,来进行一下测试


一套代码直接打完:

Controller+Service+Dao


@RestController
@RequestMapping("user")
public class UserController { @Autowired
private UserService userService; @GetMapping("/info")
public UserVO getUser(){
return userService.creatUser();
}
} public interface UserService {
UserVO creatUser(); UserVO setUserInfo(String phone);
} @Service
@EnableAspectJAutoProxy(exposeProxy = true)
public class UserServiceImpl implements UserService { @Autowired
private UserMapper userMapper; @Autowired
private InfoMapper infoMapper; @Override
public UserVO creatUser() {
UserVO userVO = userMapper.getUserInfoMapper(); return ((UserService) AopContext.currentProxy()).setUserInfo(userVO.getPhone());
} @MultiDataSource(DBName = "dataSource-1")
public UserVO setUserInfo(String phone) { UserVO userInfo = infoMapper.getUserInfo(); UserVO user = new UserVO();
user.setUserName(userInfo.getUserName());
user.setPassword(userInfo.getPassword());
user.setAddress(userInfo.getAddress());
user.setPhone(phone);
return user;
}
} @Mapper
public interface InfoMapper { @Select("select id,user_name as userName,password,phone,address from test_user")
UserVO getUserInfo();
} @Mapper
public interface UserMapper { @Select("select id,user_name as userName,password,phone from user")
UserVO getUserInfoMapper(); }

测试结果:红框数据来自于服务器数据库,绿框数据来自于本地数据库



遇到的问题

  • 同一个类中,A方法调用B方法用AopContext.currentProxy()报错问题:在类上加@EnableAspectJAutoProxy(exposeProxy = true)————解决!
  • 切面时,用JoinPoint获取方法,判断是否被注解修饰(虽然纯属多余)结果为false————有待考究!

结语


小菜鸡的学习成长之路,拒绝无味的CRUD,每过一段时间,就会把工作中用到,或者别人实现的功能解析、实现,并分享!下一篇,Redission实现分布式锁

实战:Spring AOP实现多数据源动态切换的更多相关文章

  1. Spring多数据源动态切换

    title: Spring多数据源动态切换 date: 2019-11-27 categories: Java Spring tags: 数据源 typora-root-url: ...... --- ...

  2. Springboot多数据源配置--数据源动态切换

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

  3. Spring3.3 整合 Hibernate3、MyBatis3.2 配置多数据源/动态切换数据源 方法

    一.开篇 这里整合分别采用了Hibernate和MyBatis两大持久层框架,Hibernate主要完成增删改功能和一些单一的对象查询功能,MyBatis主要负责查询功能.所以在出来数据库方言的时候基 ...

  4. Spring AOP实现Mysql数据库主从切换(一主多从)

    设置数据库主从切换的原因:数据库中经常发生的是“读多写少”,这样读操作对数据库压力比较大,通过采用数据库集群方案, 一个数据库是主库,负责写:其他为从库,负责读,从而实现读写分离增大数据库的容错率.  ...

  5. Spring3.3 整合 Hibernate3、MyBatis3.2 配置多数据源/动态切换数据源方法

    一.开篇 这里整合分别采用了Hibernate和MyBatis两大持久层框架,Hibernate主要完成增删改功能和一些单一的对象查询功能,MyBatis主要负责查询功能.所以在出来数据库方言的时候基 ...

  6. mybatis 多数据源动态切换

    笔者主要从事c#开发,近期因为项目需要,搭建了一套spring-cloud微服务框架,集成了eureka服务注册中心. gateway网关过滤.admin服务监控.auth授权体系验证,集成了redi ...

  7. springboot多数据源动态切换和自定义mybatis分页插件

    1.配置多数据源 增加druid依赖 完整pom文件 数据源配置文件 route.datasource.driver-class-name= com.mysql.jdbc.Driver route.d ...

  8. spring+myBatis 配置多数据源,切换数据源

    注:本文来源于  tianzhiwuqis <spring+myBatis 配置多数据源,切换数据源> 一个项目里一般情况下只会使用到一个数据库,但有的需求是要显示其他数据库的内容,像这样 ...

  9. Spring主从数据源动态切换

    参考文档: http://uule.iteye.com/blog/2126533 http://lanjingling.github.io/2016/02/15/spring-aop-dynamicd ...

随机推荐

  1. 【ASP.NET Core】设置Web API 响应的数据格式——Produces 特性篇

    开春首文,今天老周就跟各位大伙伴们聊一个很简单的话题:怎么设定API响应的数据格式. 说本质一点,就是设置所返回内容的 MIME 类型(Content-Type 头).当然了,咱们不会使用在HTTP管 ...

  2. 洛谷P5019 [NOIP2018 提高组] 铺设道路

    题目描述 春春是一名道路工程师,负责铺设一条长度为 n 的道路. 铺设道路的主要工作是填平下陷的地表.整段道路可以看作是 n 块首尾相连的区域,一开始,第 i 块区域下陷的深度为 di. 春春每天可以 ...

  3. Java8特性大全(最新版)

    一.序言 Java8 是一个里程碑式的版本,凭借如下新特性,让人对其赞不绝口. Lambda 表达式给代码构建带来了全新的风格和能力: Steam API 丰富了集合操作,拓展了集合的能力: 新日期时 ...

  4. 【转】C# / Extension 扩展方法

    扩展方法简介扩展方法使你能够向现有类型"添加"方法,而无需创建新的派生类型.重新编译或以其他方式修改原始类型. 扩展方法是一种特殊的静态方法,但可以像扩展类型上的实例方法一样进行调 ...

  5. JS let, var, const的用法以及区别

    本文摘自多位前辈的博文,另外还有一些我的多余补充,摘自地址已补充.非常感谢各位前辈.仅以笔记学习为目的! 深入学习ES6的知识还请访问阮一峰老师的ES6教程 如果不使用let或者const,在JS只有 ...

  6. 生成"cmd.exe"错误

    转载请注明来源:https://www.cnblogs.com/hookjc/ 在VC2005里打开"工具"菜单,选择"选项",打开"选项" ...

  7. Java UDP实现聊天功能代码【转】

    感谢大佬大佬!!!:https://www.cnblogs.com/woshijpf/p/3735684.html 我以前经常写的是基于TCP的网络编程,由于TCP建立连接鼻血要经过三次握手连接,服务 ...

  8. OSPF多区域的进阶强化版

    OSPF多区域 1.OSPF多区域原理 2.末梢区域配置 1.生成OSPF多区域的的原因:改善网络的可扩展性,快速收敛. OSPF的三种通信量:a域内通信量(单个区域内的路由器之间交换数据包构成的通信 ...

  9. 帆软报表(finereport) 组合地图 保持系列名和值居中

    自定义JavaScript代码,使用HTML解析 function(){ var name = this.name; var total = '<div style="width:10 ...

  10. 帆软报表(finereport) 饼图联动

    饼图联动:点击饼图1,饼图2和饼图3显示饼图1的关联数据,接着点击饼图2,饼图3显示饼图2的关联数据,点击上方清除级联,饼图则恢复默认展示状态 下面以上图示例效果为例,说明制作过程. 1.为每个饼图准 ...