zoukankan      html  css  js  c++  java
  • 多租户多数据源切换

    在很多系统中,都存在着租户的概念。更具需求的不同,系统可以分为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便是租户的身份特征编号。这些代码就不再描述!

  • 相关阅读:
    java中获取服务器的IP和端口
    springboot项目 配置https
    vue+element+upload实现头像上传
    js指定日期时间加一天 ,判断指定时间是否为周末
    在内网中 vue项目添加ECharts图表插件
    vue+element树组件 实现树懒加载
    iview 表格随着更改刷新
    vue设置input不可编辑切换
    .Net程序员学用Oracle系列(3):数据库编程规范
    .Net程序员学用Oracle系列(2):准备测试环境
  • 原文地址:https://www.cnblogs.com/xmzJava/p/11608145.html
Copyright © 2011-2022 走看看