­

多租户多数据源切换

  • 2019 年 10 月 3 日
  • 筆記

在很多系统中,都存在着租户的概念。更具需求的不同,系统可以分为3种类型

  • 方式一:每个租户有独立的服务和独立的数据库
  • 方式二:每个租户有共享的服务和独立的数据库
  • 方式三:每个租户有共享的服务和共享的数据库

方式1和方式3和我们日常的应用并无不同。但方式二的实现就需要做些改动了

这里我参考了一个主从分离的例子,根据租户的身份特征选择相对应的数据源。同时,还应做到动态的添加租户和数据源

参考了读写分离的配置,总共分为4步

1.继承AbstractRoutingDataSource

public class DynamicDataSource extends AbstractRoutingDataSource {    }

2.添加数据源
每一个数据源都会有一个标识key,数据源和标识key保存在map,通过标识key找到该数据源

DynamicDataSource dynamicDataSource = new DynamicDataSource();          DataSource master = masterDataSource();          DataSource slave = slaveDataSource();          //设置默认数据源          dynamicDataSource.setDefaultTargetDataSource(master);//默认从库          //配置多数据源          Map<Object, Object> map = new HashMap<>();          map.put(DataSourceType.Master.getName(), master);    //key需要跟ThreadLocal中的值对应          map.put(DataSourceType.Slave.getName(), slave);          dynamicDataSource.setTargetDataSources(map);

3.选择数据源
重写AbstractRoutingDataSource的determineCurrentLookupKey,即每次想切换数据的时候修改CurrentLookupKey,这样就能找到该key对应的数据源。

@Override      protected Object determineCurrentLookupKey() {          logger.info("数据源为{}", JdbcContextHolder.getDataSource());          return JdbcContextHolder.getDataSource();      }

4.切面判断选择key


数据源选择

有了上面的基础,现在我们要实现根据租户选择数据源也是非常简单

我们想让以下方法自动选择数据源

我们制定好了任何需要切换数据源的方法首个参数必须是cusId的规则
(注意:项目可以直接从登录租户或者其他方法拿到cusId)

public List<Custom> getList(String cusId) {                  return  customService.list();          }

依然还是4步

前三步都一样,最后一步我们需要拿到方法的首位参数,代码如下

1.定义注解

@Retention(RetentionPolicy.RUNTIME)  @Target(ElementType.METHOD)  public @interface AutoDataSource {      DataSourceType value() default DataSourceType.Master;  }

2.切面逻辑

@Before("aspect()")      private void before(JoinPoint point) {              Object target = point.getTarget();              String method = point.getSignature().getName();              Class<?> classz = target.getClass();              Class<?>[] parameterTypes = ((MethodSignature) point.getSignature()).getMethod().getParameterTypes();              try {                      Method m = classz.getMethod(method, parameterTypes);                      if (m != null && m.isAnnotationPresent(AutoDataSource.class)) {  //              AutoDataSource data = m.getAnnotation(AutoDataSource.class);                              Object[] args = point.getArgs();                              Object sourceKey = args[0];                              JdbcContextHolder.putDataSource(sourceKey + "");                              logger.info("{}-当前数据源:{}", method, sourceKey);                      }              } catch (Exception e) {                      e.printStackTrace();              }      }

JdbcContextHolder内部是个ThreadLocal

public class JdbcContextHolder {      private final static ThreadLocal<String> local = new ThreadLocal<>();        public static void putDataSource(String name) {          local.set(name);      }        public static String getDataSource() {          return local.get();      }

3.添加注解即可

@AutoDataSource          public List<Custom> getList(String cusId) {                  return  customService.list();          }

动态添加数据源

下面要实现的是在不停机的情况下,动态添加数据源。通过上文我们知道,数据源的添加是通过

dynamicDataSource.setTargetDataSources(map)

这行代码实现的。实际上也就是把我们的数据源信息保存在了AbstractRoutingDataSource的一个map集合中。但是这个map是私有类型的,而且也没有提供get方法。我们无法直接获取到map里的数据

@Nullable  private Map<Object, Object> targetDataSources;

有两种办法

  • 通过反射拿到AbstractRoutingDataSource的targetDataSources
  • 自己维护一个map,相当于加了一层代理

这里选择第二种方法

还是在DynamicDataSource类中创建一个map,并重写setTargetDataSources方法

private Map<Object, Object> dynamicTargetDataSources = new HashMap<>();    @Override  public void setTargetDataSources(Map<Object, Object> targetDataSources) {          super.setTargetDataSources(targetDataSources);          this.dynamicTargetDataSources = targetDataSources;  }

在拿到map数据之后我们再添加一个新增方法

/**   * 新增数据源   * @param key 数据源标识   * @param dataSource 数据源   */  public void addTargetDataSources(Object key, Object dataSource) {          dynamicTargetDataSources.put(key, dataSource);          super.setTargetDataSources(dynamicTargetDataSources);  }

这里好像就有点问题了,数据是加载到TargetDataSources了,但是项目只会在启动的时候去解析map中的数据。AbstractRoutingDataSource实现了InitializingBean接口,实现了afterPropertiesSet方法,所以我们需要手动触发下

public void afterPropertiesSet() {      if (this.targetDataSources == null) {          throw new IllegalArgumentException("Property 'targetDataSources' is required");      }      this.resolvedDataSources = new HashMap<>(this.targetDataSources.size());      this.targetDataSources.forEach((key, value) -> {          Object lookupKey = resolveSpecifiedLookupKey(key);          //解析数据源          DataSource dataSource = resolveSpecifiedDataSource(value);          this.resolvedDataSources.put(lookupKey, dataSource);      });      if (this.defaultTargetDataSource != null) {          this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);      }  }

修改下新增的方法,每次都要调用一次afterPropertiesSet

/**   * 新增数据源   * @param key 数据源标识   * @param dataSource 数据源   */  public void addTargetDataSources(Object key, Object dataSource) {          dynamicTargetDataSources.put(key, dataSource);          super.setTargetDataSources(dynamicTargetDataSources);          super.afterPropertiesSet();  }

接下来就很简单了,对外暴露一个接口用于新增数据源,数据源的key便是租户的身份特征编号。这些代码就不再描述!