Java:基于AOP的动态数据源切换(附源码)
1 动态数据源的必要性
我们知道,物理服务机的CPU、内存、存储空间、连接数等资源都是有限的,某个时段大量连接同时执行操作,会导致数据库在处理上遇到性能瓶颈。而在复杂的互联网业务场景下,系统流量日益膨胀。为了解决这个问题,行业先驱门充分发扬了分而治之的思想,对大库表进行分割,然后实施更好的控制和管理,同时使用多台机器的CPU、内存、存储,提供更好的性能。参考我这篇《分库分表》。
数据库有水平拆分(Scale Out) 和垂直拆分(Scale Up)的区别,但是无论怎么变化,当你对同一业务库进行分库的时候。必然要考虑到,在你的同一个业务服务(Service),会有同时访问多个数据源的情况。如下图
另外一种场景是ABTesting业务场景,可能不同的用户看到的业务数据是不一样的,这就需要根据业务特性动态的获取数据。
按照Spring boot的常规做法,maven添加依赖,在Yaml中配置对应的datasource、jpa等属性即可使用了。但是多数据源的情况下无论是配置 还是数据上下文的切换都变得无比繁琐。如果能使用注解声明的方式,粒度细化到方法级别的,那用起来就简单多了。那我们来写一个这样的实现。
2 实现过程
2.1 Maven依赖
pom文件中增加一些依赖,这边我们以Jpa为案例说明:
<!-- 增加了4.3.8版本jdbc的支持-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>4.3.8.RELEASE</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.20</version>
</dependency>
<!-- Jpa与Hibernate相关:开始-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<exclusions>
<exclusion>
<artifactId>byte-buddy</artifactId>
<groupId>net.bytebuddy</groupId>
</exclusion>
<exclusion>
<artifactId>hibernate-entitymanager</artifactId>
<groupId>org.hibernate</groupId>
</exclusion>
<exclusion>
<artifactId>hibernate-core</artifactId>
<groupId>org.hibernate</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>5.3.7.Final</version>
</dependency>
<dependency>
<groupId>com.vladmihalcea</groupId>
<artifactId>hibernate-types-52</artifactId>
<version>2.9.7</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Jpa与Hibernate相关:结束-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
2.2 yaml配置
可以看到我们配置了一个默认的数据源basic,然后再扩展了一个跟basic同级的节点mutil-data-core,包含三个数据源,basic、cloudoffice、attend。
spring:
mutildata:
basic:
driver-class-name: com.mysql.jdbc.Driver
filters: stat
initial-size: 20
logAbandoned: true
maxActive: 300
maxPoolPreparedStatementPerConnectionSize: 20
maxWait: 60000
min-idle: 5
minEvictableIdleTimeMillis: 300000
poolPreparedStatements: true
removeAbandoned: true
removeAbandonedTimeout: 1800
testOnBorrow: false
testOnReturn: false
testWhileIdle: true
timeBetweenEvictionRunsMillis: 60000
validationQuery: SELECT 1
password: 123456
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://127.0.0.1:3306/basic?autoReconnect=true&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true
username: root
mutil-data-core:
basic:
password: 123456
url: jdbc:mysql://127.0.0.1:3306/basic?autoReconnect=true&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true
username: root
cloud:
password: 123456
url: jdbc:mysql://127.0.0.1:3307/cloudoffice?autoReconnect=true&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true
username: root
attend:
password: 123456
url: jdbc:mysql://127.0.0.1:3308/attend?autoReconnect=true&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true
username: root
2.3 编写配置类Configuration
扫描我们上面的配置,spring.mutildata.basic下面的默认数据源,以及 mutil-data-core下面的多个动态数据源,有多少个扫描多少个出来,并进行组装,放到一个数据源map集合中:dataSourceMap。
@Bean(name = "basicDataSource")
@ConfigurationProperties(prefix = "spring.mutildata.basic") // 这是我们动态数据源的配置位置
public DruidDataSource basicDataSource() {
return new DruidDataSource();
}
@Autowired
private DataSourceCoreConfig dataSourceCoreConfig;
/**
* 动态集成可选的数据库路由,改掉之前硬编码的方式
* @param basicDataSource
* @return
*/
@Bean(name = "routingDataSource")
@Primary
public RoutingDataSource routingDataSource(DruidDataSource basicDataSource) {
RoutingDataSource routingDataSource = new RoutingDataSource();
Map<Object, Object> dataSourceMap = new HashMap<>(16);
HashMap<String, DataSourceCore> mutildatacore = dataSourceCoreConfig.getMutilDataCore();
routingDataSource.setDefaultTargetDataSource(basicDataSource);
try {
Iterator iter = mutildatacore.entrySet().iterator();
while (iter.hasNext()) { // 轮询出所有的动态数据源
Map.Entry entry = (Map.Entry) iter.next();
String key = entry.getKey().toString();
DataSourceCore dsc = (DataSourceCore) entry.getValue();
DruidDataSource ds = (DruidDataSource) basicDataSource.clone();
// 3个核心关键数据源头重新赋值
ds.setUrl(dsc.getUrl());
ds.setUsername(dsc.getUserName());
ds.setPassword(dsc.getPassWord());
dataSourceMap.put(key, ds);
}
}
catch (Exception ex) {
// Todo
}
routingDataSource.setTargetDataSources(dataSourceMap);
return routingDataSource;
}
2.4 数据源集合
数据源的管理:包含组织数据源、读值、赋值、清空数据源等。
/**
* @author brand
* @Description: 动态数据源
* @Copyright: Copyright (c) 2021
* @Company: Helenlyn, Inc. All Rights Reserved.
* @date 2021/12/16 10:33 上午
* @Update Time:
* @Updater:
* @Update Comments:
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* 用来保存数据源与获取数据源
*/
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<String>();
/**
* 构造,包含一个默认数据源,和一个数据源集合
* @param defaultTargetDataSource
* @param targetDataSources
*/
public DynamicDataSource(DataSource defaultTargetDataSource, Map<String, DataSource> targetDataSources) {
super.setDefaultTargetDataSource(defaultTargetDataSource);
super.setTargetDataSources(new HashMap<Object, Object>(targetDataSources));
super.afterPropertiesSet();
}
@Override
protected Object determineCurrentLookupKey() {
return getDataSource();
}
public static void setDataSource(String dataSource) {
CONTEXT_HOLDER.set(dataSource);
}
public static String getDataSource() {
return CONTEXT_HOLDER.get();
}
public static void clearDataSource() {
CONTEXT_HOLDER.remove();
}
}
2.5 按键查找
无注解的情况下,lookupKey是空的,这边直接提供默认数据源。
有注解的时候,按照注解中的信息进行查找。
/**
* 根据 lookupkey 获取到真正的目标数据源
* @return
*/
@Override
protected DataSource determineTargetDataSource() {
Assert.notNull(this.targetDataSources, "DataSource router not initialized");
Object lookupKey = this.determineCurrentLookupKey();
DataSource dataSource = (DataSource) this.targetDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) { // 无注解的情况下,lookupKey是空的,会走到这边,这时候给默认值
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
} else {
return dataSource;
}
}
2.6 初始化后的数据源结构
注意它的key,跟我们配置中的一模一样,basic、cloudoffice、attend。这个很重要,注解用这个来匹配。
2.7 编写Annotation
写一个注解,映射的目标范围为 类型和方法。
/**
* @author brand
* @Description: 数据源切换注解
* @Copyright: Copyright (c) 2021
* @Company: Helenlyn, Inc. All Rights Reserved.
* @date 2021/12/15 7:36 下午
*/
@Target({ ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
String name() default "";
}
2.8 编写AOP实现
编写切面代码,以实现对注解的PointCut。
/**
* @author brand
* @Description:
* @Copyright: Copyright (c) 2021
* @Company: Helenlyn, Inc. All Rights Reserved.
* @date 2021/12/15 7:49 下午
*/
@Aspect
@Component
public class DataSourceAspect implements Ordered {
/**
* 定义一个切入点,匹配到上面的注解DataSource
*/
@Pointcut("@annotation(com.helenlyn.dataassist.annotation.DataSource)")
public void dataSourcePointCut() {
}
/**
* Around 环绕方式做切面注入
* @param point
* @return
* @throws Throwable
*/
@Around("dataSourcePointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
DataSource ds = method.getAnnotation(DataSource.class);
String routeKey = ds.name(); // 从头部中取出注解的name(basic 或 cloudoffice 或 attend),用这个name进行数据源查找。
String dataSourceRouteKey = DynamicDataSourceRouteHolder.getDataSourceRouteKey();
if (StringUtils.isNotEmpty(dataSourceRouteKey)) {
// StringBuilder currentRouteKey = new StringBuilder(dataSourceRouteKey);
routeKey = ds.name();
}
DynamicDataSourceRouteHolder.setDataSourceRouteKey(routeKey);
try {
return point.proceed();
} finally { // 最后做清理,这个步骤很重要,因为我们的配置中有一个默认的数据源,执行完要回到默认的数据源。
DynamicDataSource.clearDataSource();
DynamicDataSourceRouteHolder.clearDataSourceRouteKey();
}
}
@Override
public int getOrder() {
return 1;
}
}
2.9 测试与效果
2.9.1 数据源key信息
数据源key 信息,有多少个数据源,这边就配置多少个,注意值须与yaml配置中的值保持一致。
/**
* 数据源key 信息,有多少个数据源,这边就配置多少个,
* 值须与yaml配置中的保持一致
*/
public static final String DATA_SOURCE_BASIC_NAME = "basic";
public static final String DATA_SOURCE_ATTEND_NAME = "attend";
public static final String DATA_SOURCE_CLOUD_NAME = "cloud";
2.9.2 测试方法
在Control中写三个测试方法
/**
* 无注解默认情况:数据源指向basic
* @return
*/
@RequestMapping(value = "/default/{user_code}", method = RequestMethod.GET)
public UserInfoDto getUserInfo(@PathVariable("user_code") String userCode) {
return userInfoService.getUserInfo(userCode);
}
/**
* 数据源指向attend
* @return
*/
@DataSource(name= Constant.DATA_SOURCE_ATTEND_NAME)
@RequestMapping(value = "/attend/{user_code}", method = RequestMethod.GET)
public UserInfoDto getUserInfoAttend(@PathVariable("user_code") String userCode) {
return userInfoService.getUserInfo(userCode);
}
/**
* 数据源指向cloud
* @return
*/
@DataSource(name= Constant.DATA_SOURCE_CLOUD_NAME)
@RequestMapping(value = "/cloud/{user_code}", method = RequestMethod.GET)
public UserInfoDto getUserInfoCloud(@PathVariable("user_code") String userCode) {
return userInfoService.getUserInfo(userCode);
}
2.9.3 效果
3 总结和代码参考
如果需要扩展数据源,在yaml的节点mutil-data-core下加配置数据就行了,简单方便。后面再写个MySQL的实现方式。
github代码:https://github.com/WengZhiHua/Helenlyn.Grocery/tree/master/parent/DynamicDataSource
Java:基于AOP的动态数据源切换(附源码)的更多相关文章
- Cesium专栏-雷达遮罩动态扫描(附源码下载)
Cesium 是一款面向三维地球和地图的,世界级的JavaScript开源产品.它提供了基于JavaScript语言的开发包,方便用户快速搭建一款零插件的虚拟地球Web应用,并在性能,精度,渲染质量以 ...
- 基于Redis缓存的Session共享(附源码)
基于Redis缓存的Session共享(附源码) 在上一篇文章中我们研究了Redis的安装及一些基本的缓存操作,今天我们就利用Redis缓存实现一个Session共享,基于.NET平台的Seesion ...
- 【开发笔记】- AbstractRoutingDataSource动态数据源切换,AOP实现动态数据源切换
AbstractRoutingDataSource动态数据源切换 上周末,室友通宵达旦的敲代码处理他的多数据源的问题,搞的非常的紧张,也和我聊了聊天,大概的了解了他的业务的需求.一般的情况下我们都是使 ...
- AbstractRoutingDataSource动态数据源切换,AOP实现动态数据源切换
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明.本文链接:https://blog.csdn.net/u012881904/article/de ...
- 【原】继承AbstractRoutingDataSource再通过AOP实现动态数据源切换
关于AbstractRoutingDataSource动态切换数据源是我在研究某开源项目时候才注意到的,大概就看懂了Spring AOP切面这里,根据作者的意思是通过继承这个抽象类可以实现数据源的动态 ...
- 继承AbstractRoutingDataSource再通过AOP实现动态数据源切换
package com.zdd.data.aspect; import java.util.ArrayList; import java.util.HashMap; import java.util. ...
- 继承AbstractRoutingDataSource再通过AOP实现动态数据源切换(转)
关于AbstractRoutingDataSource我在研究开源中国的开源项目时候才发现,好奇的看了一下代码发现自己看不明白,大概就看懂了Spring AOP切面这里,根据注释作者的意思是通过这个可 ...
- Spring AOP实现方式二【附源码】
自动代理模式[和我们说的方式一 配置 和 测试调用不一样哦~~~] 纯POJO切面 源码结构: 1.首先我们新建一个接口,love 谈恋爱接口. package com.spring.aop; /* ...
- Spring AOP实现方式一【附源码】
基本代理模式 纯POJO切面 源码结构: 1.首先我们新建一个接口,love 谈恋爱接口. package com.spring.aop; /** * 谈恋爱接口 * * @author Admin ...
随机推荐
- homestead 入坑安装
1.在使用 Homestead 之前,需要先安装 Virtual Box.VMWare.Parallels 或 Hyper-V (四选一,我们通常选择 VirtualBox,因为只有它是免费的)以及 ...
- 推荐的php安全配置选项
推荐安全配置选项 这里有几个会影响安全功能的 PHP 配置设置.下面是一些显然应该用于生产服务器的: register_globals 设置为 offsafe_mode 设置为 offerror_re ...
- Git Push 免输 用户名和密码
前言 在大家使用github的过程中,一定会碰到这样一种情况,就是每次要push 和pull时总是要输入github的账号和密码,这样不仅浪费了大量的时间且降低了工作效率.在此背景下,本文在网上找了两 ...
- C++实现对Json数据的友好处理
背景 C/C++客户端需要接收和发送JSON格式的数据到后端以实现通讯和数据交互.C++没有现成的处理JSON格式数据的接口,直接引用第三方库还是避免不了拆解拼接.考虑到此项目将会有大量JSON数据需 ...
- 已完成的python项目-环境离线部署
python环境离线部署 当前生产环境中,有很多基于python开发的工具需要使用. 由于python工具往往涉及到很多依赖,在线状态下,可以通过pip requirements来管理安装. 但有时候 ...
- iptables 的使用 与 模块
今日内容 Iptables 的使用 模块· 内容详细 一.Iptables 的使用 1.使用前奏 1.安装Iptables [root@m01 ~]# yum install iptables* 2. ...
- Solution -「BZOJ 4316」小C的独立集
\(\mathcal{Description}\) Link. 求包含 \(n\) 个结点 \(m\) 条边的仙人掌的最大独立集. \(n\le5\times10^4\),\(m\le6\ ...
- Solution -「CF 793G」Oleg and Chess
\(\mathcal{Description}\) Link. 给一个 \(n\times n\) 的棋盘,其中 \(q\) 个互不重叠的子矩阵被禁止放棋.问最多能放多少个互不能攻击的车. ...
- mysql数据库 Window下安装
关系数据库,是建立在关系数据库模型基础上的数据库,借助于集合代数等概念和方法来处理数据 库中的数据,同时也是一个被组织成一组拥有正式描述性的表格,该形式的表格作用的实质是装载着数 据项的特殊收集体,这 ...
- pytest(11)-Allure生成测试报告(一)
Allure是一个开源的测试报告生成框架,提供了测试报告定制化功能,相较于我们之前使用过pytest-html插件生成的html格式的测试报告,通过Allure生成的报告更加规范.清晰.美观. pyt ...