当前位置: 首页>编程语言>正文

Spring动态从nacos加载数据库配置 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实现动态数据源

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在启动的时候就会抛出异常而无法运行。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则根据读写和权重的不同,分别保存到readDataSourceKeyListwriteDataSourceKeyListinit-method属性,但是这里不行。因为init-method是在bean初始化完成之后调用的,当spring在初始化DynamicDataSource时发现这两个属性是空的异常就抛出来了,根本就没有机会去运行init-methodInitializingBean接口。由于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列表中取一个使用。为什么要随机呢?这就牵扯到具体的权重实现了。

动态数据源权重实现

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&characterEncoding=utf-8");
11. map.put("username", "root");
12. map.put("password", "");
13. dsList.add(map);
14. dynamicDataSource.initDataSources(dsList);

initDataSources方法时传入的数据源信息是正确的就可以了。

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


https://www.xamrdz.com/lan/5h21937175.html

相关文章: