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,它只要求实现一个方法:

  1. /**
  2. * Determine the current lookup key. This will typically be
  3. * implemented to check a thread-bound transaction context.
  4. * <p>Allows for arbitrary keys. The returned key needs
  5. * to match the stored lookup key type, as resolved by the
  6. * {@link #resolveSpecifiedLookupKey} method.
  7. */
  8. protected abstract Object determineCurrentLookupKey();

你可以简单的理解它:spring把所有的数据源都存放在了一个map中,这个方法返回一个key告诉spring用这个key从map中去取。

它还有个targetDataSourcesdefaultTargetDataSource属性,网上的一堆做法是继承这个类,然后在声明bean的时候注入dataSource:

  1. <bean id="dynamicdatasource" class="......">
  2. <property name="targetDataSources">
  3. <map>
  4. <entry key="dataSource1" value-ref="dataSource1" />
  5. <entry key="dataSource2" value-ref="dataSource2" />
  6. <entry key="dataSource3" value-ref="dataSource3" />
  7. </map>
  8. </property>
  9. <property name="defaultTargetDataSource" ref="dataSource1" />
  10. </bean>

这样虽然简单,但是弊端也是显而易见的,除了使用了多个数据源之外没有我们想要的任何操作。但是如果不配置targetDataSources,spring在启动的时候就会抛出异常而无法运行。

其实我们完全可以在spring启动的时候,我们自己来解析数据源,然后把解析出并实例化的dataSource设置到targetDataSources。下面是解析的核心代码,数据源配置文件的格式可以看这里:数据源配置样例

  1. /**
  2. * 初始化数据源
  3. *
  4. * @param dataSourceList
  5. */
  6. public void initDataSources(List<Map<String, String>> dataSourceList) {
  7. LOG.info("开始初始化动态数据源");
  8. readDataSourceKeyList = new ArrayList<String>();
  9. writeDataSourceKeyList = new ArrayList<String>();
  10. Map<Object, Object> targetDataSource = new HashMap<Object, Object>();
  11. Object defaultTargetDataSource = null;
  12. for (Map<String, String> map : dataSourceList) {
  13. String dataSourceId = DynamicDataSourceUtils.getAndRemoveValue(map, ATTR_ID,
  14. UUIDUtils.getUUID8());
  15. String dataSourceClass = DynamicDataSourceUtils
  16. .getAndRemoveValue(map, ATTR_CLASS, null);
  17. String isDefaultDataSource = DynamicDataSourceUtils.getAndRemoveValue(map,
  18. ATTR_DEFAULT, "false");
  19. String weight = DynamicDataSourceUtils.getAndRemoveValue(map, DS_WEIGHT, "1");
  20. String mode = DynamicDataSourceUtils.getAndRemoveValue(map, DS_MODE, "rw");
  21. DataSource dataSource = (DataSource) ClassUtils.newInstance(dataSourceClass);
  22. DynamicDataSourceUtils.setDsProperties(map, dataSource);
  23. targetDataSource.put(dataSourceId, dataSource);
  24. if (Boolean.valueOf(isDefaultDataSource)) {
  25. defaultTargetDataSource = dataSource;
  26. }
  27. DynamicDataSourceUtils.addWeightDataSource(readDataSourceKeyList,
  28. writeDataSourceKeyList, dataSourceId, Integer.valueOf(weight), mode);
  29. LOG.info("dataSourceId={},dataSourceClass={},isDefaultDataSource={},weight={},mode={}",
  30. new Object[] { dataSourceId, dataSourceClass, isDefaultDataSource, weight, mode });
  31. }
  32. this.setTargetDataSources(targetDataSource);
  33. if (defaultTargetDataSource == null) {
  34. defaultTargetDataSource = (CollectionUtils.isEmpty(writeDataSourceKeyList) ? targetDataSource
  35. .get(readDataSourceKeyList.iterator().next()) : targetDataSource
  36. .get(writeDataSourceKeyList.iterator().next()));
  37. }
  38. this.setDefaultTargetDataSource(defaultTargetDataSource);
  39. super.afterPropertiesSet();
  40. LOG.info("初始化动态数据源完成");
  41. }

在解析出来之后,我们调用父类的this.setTargetDataSources(targetDataSource);this.setDefaultTargetDataSource(defaultTargetDataSource);方法将它们存入进去。而dataSource的key则根据读写和权重的不同,分别保存到readDataSourceKeyListwriteDataSourceKeyList

那么什么时候来运行这个解析的方法呢?有些同学可能一下就想到了spring声明bean时的init-method属性,但是这里不行。因为init-method是在bean初始化完成之后调用的,当spring在初始化DynamicDataSource时发现这两个属性是空的异常就抛出来了,根本就没有机会去运行init-method

所以我们要在bean的初始化过程中来解析并存入我们的数据源。要实现这个操作,我们可以实现spring的InitializingBean接口。由于AbstractRoutingDataSource已经实现了该接口,我们只需要重写该方法就行。也就是说DynamicDataSource要实现以下两个方法:

  1. @Override
  2. protected Object determineCurrentLookupKey() {
  3. ...
  4. }
  5. @Override
  6. public void afterPropertiesSet() {
  7. this.initDataSources();
  8. }

afterPropertiesSet方法中实现我们解析数据源的操作。但是这样还不够,因为spring容器并不知道你做了这些,所以最后的一行super.afterPropertiesSet();千万别忘了,用来通知spring容器。

到这里数据源的解析已经完成了,我们又怎么样来取数据源呢?

这个我们可以利用ThreadLocal来实现。编写DynamicDataSourceHolder类,核心代码:

  1. private static final ThreadLocal<DataSourceContext> DATASOURCE_LOCAL = new ThreadLocal<DataSourceContext>();
  2. /**
  3. * 设置数据源读写模式
  4. *
  5. * @param isWrite
  6. */
  7. public static void setIsWrite(boolean isWrite) {
  8. DataSourceContext dsContext = DATASOURCE_LOCAL.get();
  9. //已经持有且可写,直接返回
  10. if (dsContext != null && dsContext.getIsWrite()) {
  11. return;
  12. }
  13. if (dsContext == null || isWrite) {
  14. dsContext = new DataSourceContext();
  15. dsContext.setIsWrite(isWrite);
  16. DATASOURCE_LOCAL.set(dsContext);
  17. }
  18. }
  19. /**
  20. * 获取dsKey
  21. *
  22. * @return
  23. */
  24. public static DataSourceContext getDsContent() {
  25. return DATASOURCE_LOCAL.get();
  26. }

只有简单的设置读写模式和获取dataSource的key。

动态数据源”读已之所写”问题

在设置读写模式时需要注意,如果当前线程已经拥有数据源了且是可写的,则直接返回使用当前的数据源。这是一个简单的操作却会影响到整个项目。为什么要这样做呢?要是我方法中写操作后有读操作不是也用写数据源了?没错!

这涉及到一个多数据源主从同步时的读已之所写问题,这里简单的来讲解一下。

数据库主从同步时,事务一般分两种:

1、硬事务,当往数据库保存数据时,程序读到所有数据库的数据都是一致的,但相应的性能会变低,如果数据库操作时间较长,有可能会引起线程阻塞。

2、软事务,当往数据库保存数据时,程序读到的数据不一定是一致的,但最终是一致的。举个例子,当你往主库(写库)存入数据时,数据可能无法实时同步到从库(读库),这中间可能会有那么几秒钟的误差,如果这时候刚好读到这批数据,数据就是不一致的。

当数据库都要分主从和读写分离了,肯定是有性能压力了,所以大多数都会选择第二种(只是大部分不是绝对,银行等机构可能会第一种)。

这时候数据就会有一个实时展示的问题了。以当前较流行的微信朋友圈为例,我自己发表了一条朋友圈动态,肯定是希望能够马上看到,如果隔个三五秒才能显示我会怀疑是不是发布失败了?用户体验感也会直线下降。但对别人来说,就算时时关注着我也不会知道我这个时候发布了动态,迟个三五秒显示并无大碍,对整个系统也没有影响。

说到这里相信应该已经明白了吧,简单说就是自己写的数据要能够马上读到,这就是原因了。

指定了读写模式,接下来就是获取数据源了。代码:

  1. @Override
  2. protected Object determineCurrentLookupKey() {
  3. DataSourceContext dsContent = DynamicDataSourceHolder.getDsContent();
  4. //已设置过数据源,直接返回
  5. if (StringUtils.isNotBlank(dsContent.getDsKey())) {
  6. return dsContent.getDsKey();
  7. }
  8. if (dsContent.getIsWrite()) {
  9. String dsKey = writeDataSourceKeyList.get(RandomUtils.nextInt(writeDataSourceKeyList
  10. .size()));
  11. dsContent.setDsKey(dsKey);
  12. } else {
  13. String dsKey = readDataSourceKeyList.get(RandomUtils.nextInt(readDataSourceKeyList
  14. .size()));
  15. dsContent.setDsKey(dsKey);
  16. }
  17. if (LOG.isDebugEnabled()) {
  18. LOG.debug("当前操作使用数据源:{}", dsContent.getDsKey());
  19. }
  20. return dsContent.getDsKey();
  21. }

这里同样注意如果已经设置过数据源了,直接返回,这样就能保证当前线程用的始终是同一个数据源(读改写时会变化一次)。

如果未设置过数据源则根据读写模式,随机的从key列表中取一个使用。为什么要随机呢?这就牵扯到具体的权重实现了。

动态数据源权重实现

这里的权重实现十分简单,也是当前很多组件的权重实现方式。假设一个读dataSource的权重是5,则相应的往readDataSourceKeyList中存入5个key,写dataSource也一样,读写则两边都存。这样根据权重的不同key列表中存入的数量也就不尽相同,取时生成一个小于列表大小的随机数随机取一个就行了。

使用拦截器设置读写模式

各个组件的功能都实现了,只差东风了,什么时候来设置读写模式呢?

这个简单,使用一个拦截器就能搞定。因为是基于Spring JdbcTemplate,所以只要拦截相应的方法即可。JdbcTemplate的方法命名还是十分规范的,开发人员改动的可能性也几乎为零,这里我们拦截接口:

  1. /**
  2. * 动态数据源拦截器
  3. *
  4. * Created by liyd on 2015-11-2.
  5. */
  6. @Aspect
  7. @Component
  8. public class DynamicDsInterceptor {
  9. @Pointcut("execution(* org.springframework.jdbc.core.JdbcOperations.*(..))")
  10. public void executeMethod() {
  11. }
  12. @Around("executeMethod()")
  13. public Object methodAspect(ProceedingJoinPoint pjp) throws Throwable {
  14. String methodName = pjp.getSignature().getName();
  15. if (StringUtils.startsWith(methodName, "query")) {
  16. DynamicDataSourceHolder.setIsWrite(false);
  17. } else {
  18. DynamicDataSourceHolder.setIsWrite(true);
  19. }
  20. return pjp.proceed();
  21. }
  22. }

动态修改数据源

到这里我们的动态数据源就实现的差不多了,有的同学可能会问,那我怎么动态的去修改它呢?

其实看到上面的initDataSources方法答案就已经有了,它的参数是 List<Map<String,
String>> dataSourceList
,只需要将数据源的参数封装成map的list传入调用该方法就能实现动态修改了,这也是我为什么把super.afterPropertiesSet();这一行放到这里面而不是重写方法本身的原因。以下是一个简单的候示例:

  1. List<Map<String, String>> dsList = new ArrayList<Map<String, String>>();
  2. Map<String, String> map = new HashMap<String, String>();
  3. map.put("id", "dataSource4");
  4. map.put("class", "org.apache.commons.dbcp.BasicDataSource");
  5. map.put("default", "true");
  6. map.put("weight", "10");
  7. map.put("mode", "rw");
  8. map.put("driverClassName", "com.mysql.jdbc.Driver");
  9. map.put("url",
  10. "jdbc:mysql://localhost:3306/db1?useUnicode=true&amp;characterEncoding=utf-8");
  11. map.put("username", "root");
  12. map.put("password", "");
  13. dsList.add(map);
  14. dynamicDataSource.initDataSources(dsList);

在实际的场景中,根据项目使用技术的不同,你可以使用监听器、socket、配置中心等来实现该数据源动态修改的功能,只要保存调用initDataSources方法时传入的数据源信息是正确的就可以了。

动态数据源的实现就到这里了,我希望更多的是提供了一种思维,可以根据这个思维做些改变将它应用到具体的场景中,而不仅仅限于JdbcTemplate和Spring,只是做了一个抛砖引玉而已。

所有的源码都可以在上方供下载的dexcoder-assistant工具包中找到,欢迎各位讨论,留下自己的意见和想法。

时间: 2024-10-29 19:05:32

Spring实现动态数据源,支持动态添加、删除和设置权重及读写分离的相关文章

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

当项目慢慢变大,訪问量也慢慢变大的时候.就难免的要使用多个数据源和设置读写分离了. 在开题之前先说明下,由于项目多是使用Spring,因此下面说到某些操作可能会依赖于Spring. 在我经历过的项目中,见过比較多的读写分离处理方式,主要分为两步: 1.对于开发者,要求serivce类的方法名必须遵守规范,读操作以query.get等开头,写操作以update.delete开头. 2.配置一个拦截器,根据方法名推断是读操作还是写操作,设置对应的数据源. 以上做法能实现最简单的读写分离.但对应的也会

Spring整合多数据源实现动态切换

在实际项目中时常需要连接多个数据库,而且不同的业务需求在实现过程当中往往需要访问不同的数据库. jdbc.properties配置文件,配置多个dataSource ##########################MySQL##################################### hibernate.dialect=org.hibernate.dialect.MySQLInnoDBDialect connection.driver_class=com.mysql.jdbc.

用JQuery动态为选中元素添加/删除类

在做一些tab页功能时,我们经常会见到如下样式: 即当选中一个元素时,在此元素下会添加相应的类,以示区别.今天就研究了一下如何用JQuery实现此效果. 1. HTML代码 <a id="med_specialist_1" name="med-specialist" class="med-active">专家门诊1</a> <a id="med_specialist_2" name="m

搞定SpringBoot多数据源(2):动态数据源

目录 1. 引言 2. 动态数据源流程说明 3. 实现动态数据源 3.1 说明及数据源配置 3.1.1 包结构说明 3.1.2 数据库连接信息配置 3.1.3 数据源配置 3.2 动态数据源设置 3.2.1 动态数据源配置 3.2.2 动态选择数据源 3.2.3 动态数据源使用 3.3 使用 AOP 选择数据源 3.3.1 定义数据源注解 3.3.2 定义数据源切面 3.3.3 使用 AOP 进行数据源切换 4. 再思考一下 5. 总结 参考资料 往期文章 一句话概括:使用动态数据源对多个数据库

Spring+MyBatis实现数据库读写分离方案

方案1通过MyBatis配置文件创建读写分离两个DataSource,每个SqlSessionFactoryBean对象的mapperLocations属性制定两个读写数据源的配置文件.将所有读的操作配置在读文件中,所有写的操作配置在写文件中. 优点:实现简单缺点:维护麻烦,需要对原有的xml文件进行重新修改,不支持多读,不易扩展实现方式 <bean id="abstractDataSource" abstract="true" class="com

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

[视频&交流平台] àSpringBoot视频 http://study.163.com/course/introduction.htm?courseId=1004329008&utm_campaign=commission&utm_source=400000000155061&utm_medium=share à SpringCloud视频 http://study.163.com/course/introduction.htm?courseId=1004638001&a

Spring Boot:实现MyBatis动态数据源

综合概述 在很多具体应用场景中,我们需要用到动态数据源的情况,比如多租户的场景,系统登录时需要根据用户信息切换到用户对应的数据库.又比如业务A要访问A数据库,业务B要访问B数据库等,都可以使用动态数据源方案进行解决.接下来,我们就来讲解如何实现动态数据源,以及在过程中剖析动态数据源背后的实现原理. 实现案例 本教程案例基于 Spring Boot + Mybatis + MySQL 实现. 生成项目模板 为方便我们初始化项目,Spring Boot给我们提供一个项目模板生成网站. 1.  打开浏

Spring主从数据库的配置和动态数据源切换原理

原文:https://www.liaoxuefeng.com/article/00151054582348974482c20f7d8431ead5bc32b30354705000 在大型应用程序中,配置主从数据库并使用读写分离是常见的设计模式.在Spring应用程序中,要实现读写分离,最好不要对现有代码进行改动,而是在底层透明地支持. Spring内置了一个AbstractRoutingDataSource,它可以把多个数据源配置成一个Map,然后,根据不同的key返回不同的数据源.因为Abst

spring 动态数据源

1.动态数据源:  在一个项目中,有时候需要用到多个数据库,比如读写分离,数据库的分布式存储等等,这时我们要在项目中配置多个数据库. 2.原理:   (1).spring 单数据源获取数据连接过程: DataSource --> SessionFactory --> Session  DataSouce   实现javax.sql.DateSource接口的数据源,  DataSource  注入SessionFactory, 从sessionFactory 获取 Session,实现数据库的