Spring实现动态数据源,支持动态加入、删除和设置权重及读写分离
当项目慢慢变大,訪问量也慢慢变大的时候。就难免的要使用多个数据源和设置读写分离了。
在开题之前先说明下,由于项目多是使用Spring,因此下面说到某些操作可能会依赖于Spring。
在我经历过的项目中,见过比較多的读写分离处理方式,主要分为两步:
1、对于开发者,要求serivce类的方法名必须遵守规范,读操作以query、get等开头,写操作以update、delete开头。
2、配置一个拦截器,根据方法名推断是读操作还是写操作,设置对应的数据源。
以上做法能实现最简单的读写分离。但对应的也会有非常多不方便的地方,印象最深的应该是下面几点:
1、数据源的管理不太方便。基本上仅仅有2个数据源了,一个读一个写。这个能够在spring中声明多个bean来解决该问题。但bean的id和数据源的功能也就绑定了。
2、由于读写分离往往是在项目慢慢变大后增加的。不是一開始就有。上面说到的第二点方法名可能会各式各样。find、insert、save、exe等等,这些都要一一改动。且要保证以后读的方法名中不能有写操作。也能够拦截的底层一点如JdbcTemplate。但这样会导致交叉设置数据源。
3、数据源无法动态改动,仅仅能在项目启动时载入。
以上问题我想开发者多多少少都会遇到。这也是本文要讨论的问题。
动态数据源结构
在我看来一个好的动态数据源,应该跟单数据源一样让使用者感觉不到他是动态的。至少dao层的开发人员应该感觉不到。
先来看张图:
看了上图应该就明确我的思路了,对使用者来说仅仅有一个数据源DynamicDataSource
,以下我们来谈谈怎样实现它。
基于spring实现动态数据源
事实上spring早就想到了这一点,也已经为我们准备好了扩展类AbstractRoutingDataSource
,我们仅仅须要一个简单的实现就可以。网上关于这个类文章非常多,但都比較粗浅没有讲到点子上,仅仅是实现了多个数据源而已。
这里我们相同来实现AbstractRoutingDataSource
。它仅仅要求实现一个方法:
- /**
- * 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();
你能够简单的理解它:spring把全部的数据源都存放在了一个map中,这种方法返回一个key告诉spring用这个key从map中去取。
它还有个targetDataSources
和defaultTargetDataSource
属性,网上的一堆做法是继承这个类,然后在声明bean的时候注入dataSource:
- <bean id="dynamicdatasource" class="......">
- <property name="targetDataSources">
- <map>
- <entry key="dataSource1" value-ref="dataSource1" />
- <entry key="dataSource2" value-ref="dataSource2" />
- <entry key="dataSource3" value-ref="dataSource3" />
- </map>
- </property>
- <property name="defaultTargetDataSource" ref="dataSource1" />
- </bean>
这样尽管简单。可是弊端也是显而易见的。除了使用了多个数据源之外没有我们想要的不论什么操作。可是假设不配置targetDataSources
。spring在启动的时候就会抛出异常而无法执行。
事实上我们全然能够在spring启动的时候,我们自己来解析数据源。然后把解析出并实例化的dataSource设置到targetDataSources
。以下是解析的核心代码。数据源配置文件的格式能够看这里:数据源配置例子
- /**
- * 初始化数据源
- *
- * @param dataSourceList
- */
- public void initDataSources(List<Map<String, String>> dataSourceList) {
- LOG.info("開始初始化动态数据源");
- readDataSourceKeyList = new ArrayList<String>();
- writeDataSourceKeyList = new ArrayList<String>();
- Map<Object, Object> targetDataSource = new HashMap<Object, Object>();
- Object defaultTargetDataSource = null;
- for (Map<String, String> map : dataSourceList) {
- String dataSourceId = DynamicDataSourceUtils.getAndRemoveValue(map, ATTR_ID,
- UUIDUtils.getUUID8());
- String dataSourceClass = DynamicDataSourceUtils
- .getAndRemoveValue(map, ATTR_CLASS, null);
- String isDefaultDataSource = DynamicDataSourceUtils.getAndRemoveValue(map,
- ATTR_DEFAULT, "false");
- String weight = DynamicDataSourceUtils.getAndRemoveValue(map, DS_WEIGHT, "1");
- String mode = DynamicDataSourceUtils.getAndRemoveValue(map, DS_MODE, "rw");
- DataSource dataSource = (DataSource) ClassUtils.newInstance(dataSourceClass);
- DynamicDataSourceUtils.setDsProperties(map, dataSource);
- targetDataSource.put(dataSourceId, dataSource);
- if (Boolean.valueOf(isDefaultDataSource)) {
- defaultTargetDataSource = dataSource;
- }
- DynamicDataSourceUtils.addWeightDataSource(readDataSourceKeyList,
- writeDataSourceKeyList, dataSourceId, Integer.valueOf(weight), mode);
- LOG.info("dataSourceId={},dataSourceClass={},isDefaultDataSource={},weight={},mode={}",
- new Object[] { dataSourceId, dataSourceClass, isDefaultDataSource, weight, mode });
- }
- this.setTargetDataSources(targetDataSource);
- if (defaultTargetDataSource == null) {
- defaultTargetDataSource = (CollectionUtils.isEmpty(writeDataSourceKeyList) ? targetDataSource
- .get(readDataSourceKeyList.iterator().next()) : targetDataSource
- .get(writeDataSourceKeyList.iterator().next()));
- }
- this.setDefaultTargetDataSource(defaultTargetDataSource);
- super.afterPropertiesSet();
- LOG.info("初始化动态数据源完毕");
- }
在解析出来之后,我们调用父类的this.setTargetDataSources(targetDataSource);
和this.setDefaultTargetDataSource(defaultTargetDataSource);
方法将它们存入进去。而dataSource的key则依据读写和权重的不同,分别保存到readDataSourceKeyList
和writeDataSourceKeyList
。
那么什么时候来执行这个解析的方法呢?有些同学可能一下就想到了spring声明bean时的init-method
属性,可是这里不行。
由于init-method
是在bean初始化完毕之后调用的,当spring在初始化DynamicDataSource
时发现这两个属性是空的异常就抛出来了。根本就没有机会去执行init-method
。
所以我们要在bean的初始化过程中来解析并存入我们的数据源。要实现这个操作,我们能够实现spring的InitializingBean
接口。因为AbstractRoutingDataSource
已经实现了该接口,我们仅仅须要重写该方法即可。也就是说DynamicDataSource
要实现下面两个方法:
- @Override
- protected Object determineCurrentLookupKey() {
- ...
- }
- @Override
- public void afterPropertiesSet() {
- this.initDataSources();
- }
在afterPropertiesSet
方法中实现我们解析数据源的操作。可是这样还不够,由于spring容器并不知道你做了这些,所以最后的一行super.afterPropertiesSet();
千万别忘了,用来通知spring容器。
到这里数据源的解析已经完毕了,我们又怎么样来取数据源呢?
这个我们能够利用ThreadLocal
来实现。
编写DynamicDataSourceHolder
类,核心代码:
- private static final ThreadLocal<DataSourceContext> DATASOURCE_LOCAL = new ThreadLocal<DataSourceContext>();
- /**
- * 设置数据源读写模式
- *
- * @param isWrite
- */
- public static void setIsWrite(boolean isWrite) {
- DataSourceContext dsContext = DATASOURCE_LOCAL.get();
- //已经持有且可写,直接返回
- if (dsContext != null && dsContext.getIsWrite()) {
- return;
- }
- if (dsContext == null || isWrite) {
- dsContext = new DataSourceContext();
- dsContext.setIsWrite(isWrite);
- DATASOURCE_LOCAL.set(dsContext);
- }
- }
- /**
- * 获取dsKey
- *
- * @return
- */
- public static DataSourceContext getDsContent() {
- return DATASOURCE_LOCAL.get();
- }
仅仅有简单的设置读写模式和获取dataSource的key。
动态数据源”读已之所写”问题
在设置读写模式时须要注意。假设当前线程已经拥有数据源了且是可写的。则直接返回使用当前的数据源。
这是一个简单的操作却会影响到整个项目。为什么要这样做呢?要是我方法中写操作后有读操作不是也用写数据源了?没错。
这涉及到一个多数据源主从同步时的读已之所写
问题,这里简单的来解说一下。
数据库主从同步时,事务一般分两种:
1、硬事务,当往数据库保存数据时,程序读到全部数据库的数据都是一致的,但对应的性能会变低,假设数据库操作时间较长,有可能会引起线程堵塞。
2、软事务,当往数据库保存数据时,程序读到的数据不一定是一致的,但终于是一致的。
举个样例。当你往主库(写库)存入数据时。数据可能无法实时同步到从库(读库),这中间可能会有那么几秒钟的误差,假设这时候刚好读到这批数据。数据就是不一致的。
当数据库都要分主从和读写分离了,肯定是有性能压力了。所以大多数都会选择另外一种(仅仅是大部分不是绝对,银行等机构可能会第一种)。
这时候数据就会有一个实时展示的问题了。以当前较流行的微信朋友圈为例,我自己发表了一条朋友圈动态,肯定是希望可以立即看到,假设隔个三五秒才干显示我会怀疑是不是公布失败了?用户体验感也会直线下降。但对别人来说,就算时时关注着我也不会知道我这个时候公布了动态,迟个三五秒显示并无大碍,对整个系统也没有影响。
讲到这里相信应该已经明确了吧,简单说就是自己写的数据要可以立即读到。这就是原因了。
指定了读写模式。接下来就是获取数据源了。代码:
- @Override
- protected Object determineCurrentLookupKey() {
- DataSourceContext dsContent = DynamicDataSourceHolder.getDsContent();
- //已设置过数据源。直接返回
- if (StringUtils.isNotBlank(dsContent.getDsKey())) {
- return dsContent.getDsKey();
- }
- if (dsContent.getIsWrite()) {
- String dsKey = writeDataSourceKeyList.get(RandomUtils.nextInt(writeDataSourceKeyList
- .size()));
- dsContent.setDsKey(dsKey);
- } else {
- String dsKey = readDataSourceKeyList.get(RandomUtils.nextInt(readDataSourceKeyList
- .size()));
- dsContent.setDsKey(dsKey);
- }
- if (LOG.isDebugEnabled()) {
- LOG.debug("当前操作使用数据源:{}", dsContent.getDsKey());
- }
- return dsContent.getDsKey();
- }
这里相同注意假设已经设置过数据源了,直接返回,这样就能保证当前线程用的始终是同一个数据源(读改写时会变化一次)。
假设未设置过数据源则依据读写模式,随机的从key列表中取一个使用。为什么要随机呢?这就牵扯到详细的权重实现了。
动态数据源权重实现
这里的权重实现十分简单。也是当前非常多组件的权重实现方式。
如果一个读dataSource的权重是5,则对应的往readDataSourceKeyList
中存入5个key,写dataSource也一样。读写则两边都存。
这样依据权重的不同key列表中存入的数量也就不尽同样。取时生成一个小于列表大小的随机数随机取一个即可了。
使用拦截器设置读写模式
各个组件的功能都实现了,仅仅差东风了,什么时候来设置读写模式呢?
这个简单,使用一个拦截器就能搞定。由于是基于Spring JdbcTemplate,所以仅仅要拦截对应的方法就可以。JdbcTemplate的方法命名还是十分规范的,开发者修改的可能性也差点儿为零,这里我们拦截接口:
- /**
- * 动态数据源拦截器
- *
- * Created by liyd on 2015-11-2.
- */
- @Aspect
- @Component
- public class DynamicDsInterceptor {
- @Pointcut("execution(* org.springframework.jdbc.core.JdbcOperations.*(..))")
- public void executeMethod() {
- }
- @Around("executeMethod()")
- public Object methodAspect(ProceedingJoinPoint pjp) throws Throwable {
- String methodName = pjp.getSignature().getName();
- if (StringUtils.startsWith(methodName, "query")) {
- DynamicDataSourceHolder.setIsWrite(false);
- } else {
- DynamicDataSourceHolder.setIsWrite(true);
- }
- return pjp.proceed();
- }
- }
动态改动数据源
到这里我们的动态数据源就实现的差点儿相同了。有的同学可能会问,那我怎么动态的去改动它呢?
事实上看到上面的initDataSources
方法答案就已经有了,它的參数是 List<Map<String,
,仅仅须要将数据源的參数封装成map的list传入调用该方法就能实现动态改动了,这也是我为什么把
String>> dataSourceListsuper.afterPropertiesSet();
这一行放到这里面而不是重写方法本身的原因。下面是一个简单的候演示样例:
- List<Map<String, String>> dsList = new ArrayList<Map<String, String>>();
- Map<String, String> map = new HashMap<String, String>();
- map.put("id", "dataSource4");
- map.put("class", "org.apache.commons.dbcp.BasicDataSource");
- map.put("default", "true");
- map.put("weight", "10");
- map.put("mode", "rw");
- map.put("driverClassName", "com.mysql.jdbc.Driver");
- map.put("url",
- "jdbc:mysql://localhost:3306/db1? useUnicode=true&characterEncoding=utf-8");
- map.put("username", "root");
- map.put("password", "");
- dsList.add(map);
- dynamicDataSource.initDataSources(dsList);
在实际的场景中,依据项目使用技术的不同,你能够使用监听器、socket、配置中心等来实现该数据源动态改动的功能,仅仅要保存调用initDataSources
方法时传入的数据源信息是正确的就能够了。
动态数据源的实现就到这里了,我希望很多其它的是提供了一种思维,能够依据这个思维做些改变将它应用到详细的场景中。而不只限于JdbcTemplate和Spring,不过做了一个抛砖引玉而已。
全部的源代码都能够在上方供下载的dexcoder-assistant
工具包中找到,欢迎各位讨论,留下自己的意见和想法。
Spring实现动态数据源,支持动态加入、删除和设置权重及读写分离的更多相关文章
- Spring简单实现数据源的动态切换
Spring简单实现数据源的动态切换: 1. 创建一个数据源切换类: 2. 继承AbstractRoutingDataSource,创建多数据源路由类,并注入到spring的配置文件中: 3. AOP ...
- 多数据源系统接入mybatis-plus, 实现动态数据源、动态事务。
目录: 实现思想 导入依赖.配置说明 代码实现 问题总结 一.实现思想 接手一个旧系统,SpringBoot 使用的是纯粹的 mybatis ,既没有使用规范的代码生成器,也没有使用 JPA 或者 m ...
- spring+mybatis多数据源,动态切换
有时我们项目中需要配置多个数据源,不同的业务使用的数据库不同 实现思路:配置多个dataSource ,再配置多个sqlSessionFactory,和dataSource一一对应.重写SqlSess ...
- Spring Cloud Gateway 扩展支持动态限流
之前分享过 一篇 <Spring Cloud Gateway 原生的接口限流该怎么玩>, 核心是依赖Spring Cloud Gateway 默认提供的限流过滤器来实现 原生Request ...
- Spring Boot + Mybatis多数据源和动态数据源配置
文章转自 https://blog.csdn.net/neosmith/article/details/61202084 网上的文章基本上都是只有多数据源或只有动态数据源,而最近的项目需要同时使用两种 ...
- springboot 双数据源+aop动态切换
# springboot-double-dataspringboot-double-data 应用场景 项目需要同时连接两个不同的数据库A, B,并且它们都为主从架构,一台写库,多台读库. 多数据源 ...
- SpringBoot整合MyBatisPlus配置动态数据源
目录 SpringBoot整合MyBatisPlus配置动态数据源 SpringBoot整合MyBatisPlus配置动态数据源 推文:2018开源中国最受欢迎的中国软件MyBatis-Plus My ...
- 搞定SpringBoot多数据源(2):动态数据源
目录 1. 引言 2. 动态数据源流程说明 3. 实现动态数据源 3.1 说明及数据源配置 3.1.1 包结构说明 3.1.2 数据库连接信息配置 3.1.3 数据源配置 3.2 动态数据源设置 3. ...
- SpringBoot多数据源:动态数据源
目录 1. 引言 2. 动态数据源流程说明 3. 实现动态数据源 3.1 说明及数据源配置 3.1.1 包结构说明 3.1.2 数据库连接信息配置 3.1.3 数据源配置 3.2 动态数据源设置 3. ...
随机推荐
- 【AIX】在命令前显示完整路径
登录到AIX系统,发现在#前没有目录展示,这样我们在查看当前目前时很不方便,需要借助命令PWD才可以实现 解决方案: 在.profile文件中添加命令:export PS1="[LONGNA ...
- 查看Window系列本地账户密码
mimikatz,很出名的查看Window本地账户密码(经测试,不支持探测Window在线账户认证密码的探测) github: https://github.com/gentilkiwi/mimika ...
- PHP执行insert语句报错“Data too long for column”解决办法
PHP执行mysql 插入语句, insert语句在Navicat for mysql(或任意的mysql管理工具) 中可以正确执行,但是用mysql_query()函数执行却报错. 错误 : “Da ...
- CentOS 7 使用 Yum 软件源安装谷歌 Chrome 浏览器
Google Chrome是一款由 Google 公司开发的网页浏览器,新版的 Chrome 浏览器使用的是 Blink 内核,具有运行速度快,稳定的特性.Chrome 能够运行在 Windows,L ...
- centos 7 系统启动不了 出现报错dependency failed for /mnt , dependency failed for local file systems
阿里云一台Ecs重启后启动不了,出现报错 dependency failed for /mnt , dependency failed for local file systems , 报错的原因 ...
- MBProgressHUD 的类扩展方法用法
#import "MBProgressHUD.h" @interface MBProgressHUD (Add) + (void)showError:(NSString *)err ...
- 有可能挑战Java优势的四种技术
2012-02-22 Java是一种杰出的产业开发语言,这是因为它带来了伟大的统一和对事实上以前并不存在的重要标准的关注.但是和所有语言一样,Java将来也会褪色.依据我做的超越Java的研究,一个 ...
- java struts2入门学习--OGNL语言基本用法
一.知识点学习 1.struts2中包含以下6种对象,requestMap,sessionMap,applicationMap,paramtersMap,attr,valueStack; 1)requ ...
- 使用btrace来找出执行慢的方法
转载于:https://shaojun.name/2016/07/260 btrace script import static com.sun.btrace.BTraceUtils.name; im ...
- POJ 2676 Sudoku (DFS)
Sudoku Time Limit: 2000MS Memory Limit: 65536K Total Submissions: 11694 Accepted: 5812 Special ...