Mycat 多租户方案

本文主要来介绍多租户方案。

1、需求

1.1 需求图

这里写图片描述

1.2 环境说明

环境说明:
这里写图片描述

2、每租户一逻辑库方案

2.1实现思想

用户在用用户名登陆时首先需要根据用户名,查询到该用户所在的逻辑库,然后登陆成功后,将和会话信息存放在一起,方便在访问其他业务的时候,能够很方便的得到该逻辑库。与此同时,利用Mybatis 提供的 SQL拦截器机制与Mycat提供的注解,改写SQL语句为 sql = “/*!mycat:schema=” + tenant + " */" + sql; 这样Mycat在解析时,会自动路由到tenat逻辑库上执行SQL语句。

2.2 具体实现关键点

Mycat 模拟配置如下:
这里写图片描述

2.2.1、登陆接口申明

public Map login( String account, String password );
根据用户名与密码登陆。
返回值说明如下:

      {
          "code" : 0,
          "data": {
             "userId" : 1,
             "tenant" : "h_xsgjzx"
          }
      }

接口中的返回 tenant 参数,作为其他业务接口的第一参数。

现在有个关键点,就是根据用户名 account 怎么知道用户存在哪个逻辑库呢?我给出用思路是,提供一个表来记录所有数据库中表的结合,global_user,字段基本如下:
     ID account db_pos
然后提供一个接口,根据用户名查询出db_pos的值。

然后再去实现该接口

实现1:查询刚才global_user表,获取tenent;也可以用redis缓存等。该处可以扩展。

2.2.2 控制层方法

通过成功登录系统后,就能得到 逻辑scheme : tenant。业务action的声明如下:
public Map findDepts( String tenant, 其他业务参数 ) ;

为了避免 tenant 参数污染业务层,DAO层的方法声明,,故在控制器层(Control)将 tenant 参数存入到 ThreadLocal 变量中。现在提供 Tenant工具类,申明如下:

package persistent.prestige.modules.common.tenant;
public class TenantContextHolder {
	
	private static ThreadLocal<String> tenanThreadLocal = new ThreadLocal<String>();
	
	public static final void setTenant(String scheme) {
		tenanThreadLocal.set(scheme);
	}
	
	public static final String getTenant() {
		String scheme = tenanThreadLocal.get();
		if (scheme == null) {
			scheme = "";
		}
		return scheme;
	}
	
	public static final void remove() {
		tenanThreadLocal.remove();
	}

}

那控制器层代码的伪代码如下:

  public Map  findDepts( String tenant, String businessP1  ) {
     Map result = new HashMap();
     try {
         TenantContextHolder.setTenant(tenant);
         //调用service层代码
     } catch(Throw e) {
         e.printStackTrace();
		 result.put("msg", "系统异常");
		 result.put("code", 1);
     } finally {
		TenantContextHolder.remove();
        System.out.println("控制器层面,,移除tenant。。。");
    }
  
  }

如果每个控制器层代码,都需要用上面的模板来做,未免有点。。。所以为了统一处理 Tenant ,目前提供一个给予Spring AOP 的拦截器。代码如下:

package persistent.prestige.modules.common.tenant;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.beans.factory.annotation.Autowired;
import persistent.prestige.modules.edu.service.UserSchemeService;
public class TenantControlInteceper implements MethodInterceptor {
	@Autowired
	private UserSchemeService userScemeService;
	@Override
	public Object invoke(MethodInvocation invocation) throws Throwable {
		try {
			if("login".equals(invocation.getMethod().getName())) {
				return invocation.proceed();
			}
			
			System.out.println("控制器层面,,计算 tenant。。。");
			Object[] args = invocation.getArguments();
			String tenant = "";
			if( args != null && args.length > 0) {
				tenant = (String)args[0];
			}
			TenantContextHolder.setTenant(tenant);
			return invocation.proceed();
		}finally {
			TenantContextHolder.remove();
			System.out.println("控制器层面,,移除tenant。。。");
		}
		
	}
}

统一处理Tenant 的设置为移除;此处与代码中的有点差别,是因为,,根据用户登录名获取tenant的逻辑放在了上面登录接口中。

只要遵循这样一种编码规范,action方法的第一个参数的值为 tenant 就好。配置一下拦截器【基于Spring AOP】
这里写图片描述

2.2.3、业务承载方法

业务方法无需改变;但是要利用Mybatis 拦截器改写SQL。代码和配置如下:
1、工具类

package persistent.prestige.platform.mybatis.Interceptor;

import java.lang.reflect.Field;

import org.apache.commons.lang.reflect.FieldUtils;

public class ReflectHelper {

	public static Object getFieldValue(Object obj , String fieldName ){  
        
        if(obj == null){  
            return null ;  
        }  
          
        Field targetField = getTargetField(obj.getClass(), fieldName);  
          
        try {  
            return FieldUtils.readField(targetField, obj, true ) ;  
        } catch (IllegalAccessException e) {  
            e.printStackTrace();  
        }   
        return null ;  
    }  
      
    public static Field getTargetField(Class<?> targetClass, String fieldName) {  
        Field field = null;  
  
        try {  
            if (targetClass == null) {  
                return field;  
            }  
  
            if (Object.class.equals(targetClass)) {  
                return field;  
            }  
  
            field = FieldUtils.getDeclaredField(targetClass, fieldName, true);  
            if (field == null) {  
                field = getTargetField(targetClass.getSuperclass(), fieldName);  
            }  
        } catch (Exception e) {  
        }  
  
        return field;  
    }  
      
    public static void setFieldValue(Object obj , String fieldName , Object value ){  
        if(null == obj){return;}  
        Field targetField = getTargetField(obj.getClass(), fieldName);    
        try {  
             FieldUtils.writeField(targetField, obj, value) ;  
        } catch (IllegalAccessException e) {  
            e.printStackTrace();  
        }   
    }   

}

SQL拦截类

package persistent.prestige.platform.mybatis.Interceptor;

import java.sql.Connection;
import java.util.Properties;

import org.apache.ibatis.executor.statement.RoutingStatementHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.kahadb.page.Page;
import org.springframework.beans.factory.annotation.Autowired;

import persistent.prestige.modules.common.tenant.TenantContextHolder;
import persistent.prestige.modules.edu.dao.TeacherUserDao;

@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class }) })
public class TenantInterceptor implements Interceptor {

	@Override
	public Object intercept(Invocation invocation) throws Throwable {
		
		String tenant = TenantContextHolder.getTenant();
		
		if(tenant == null || tenant == "") {
			System.out.println("tenant 为空,不需要改写sql语句");
			return invocation.proceed();
		}

		if (invocation.getTarget() instanceof RoutingStatementHandler) {

			System.out.println("aaaaaaa");
			RoutingStatementHandler statementHandler = (RoutingStatementHandler) invocation
					.getTarget();
			StatementHandler delegate = (StatementHandler) ReflectHelper
					.getFieldValue(statementHandler, "delegate");
			BoundSql boundSql = delegate.getBoundSql();
			Object obj = boundSql.getParameterObject();
			
			
			// 通过反射获取delegate父类BaseStatementHandler的mappedStatement属性
			MappedStatement mappedStatement = (MappedStatement) ReflectHelper
					.getFieldValue(delegate, "mappedStatement");
			// 拦截到的prepare方法参数是一个Connection对象
			Connection connection = (Connection) invocation.getArgs()[0];
			// 获取当前要执行的Sql语句,也就是我们直接在Mapper映射语句中写的Sql语句
			String sql = boundSql.getSql();
			// 给当前的page参数对象设置总记录数
			System.out.println("处理之前" + sql);
			//对 sql 增加 mycat 注解
			
			
			sql = "/*!mycat:schema=" + tenant + " */" + sql;
			
			System.out.println("加入处理后:" + sql);
			
			
			
			ReflectHelper.setFieldValue(boundSql, "sql", sql);
			
		}
		return invocation.proceed();
	}

	@Override
	public Object plugin(Object target) {
		// TODO Auto-generated method stub
		if (target instanceof StatementHandler) {
			return Plugin.wrap(target, this);
		} else {
			return target;
		}
	}

	@Override
	public void setProperties(Properties properties) {
		// TODO Auto-generated method stub

	}

}

配置如下:

这里写图片描述

2.2.4 方案优缺点

  • 优点:
    对业务代码侵入少,开发人员无需关注数据在哪个逻辑库上,隔离性好。
  • 缺点
    如果需要对所有租户的数据进行汇聚的话,需要业务上去实现。

该方案代码:请关注如下代码:

  • 控制层
    persistent.prestige.modules.edu.action. EduControl 里面有login的模拟实现,业务方法的实现。
  • 拦截器
    • persistent.prestige.modules.common.tenant. TenantControlInteceper
    • persistent.prestige.platform.mybatis.Interceptor. TenantInterceptor

相关代码我已上传到:https://github.com/dingwpmz/Mycat-Demo

3、多租户同一逻辑库方案

3.1实现思想

每个分片对应一个集团,每个业务表中增加一个分片字段 db_pos,类型为int型,比如制定如下字段:
     0 h_xsgizx
     20 h_xsyz
     40 m_fhzx
     60 m_mzzx

Mycat 提供一个逻辑库,其中每个分片代表一个集团,,由于集团数量是固定的,故可以采用 分片枚举 进行分片。

这种方案,不是传统意义上的多租户,而是用mycat枚举分片规则。配置分片就好。

3.2 方案优缺点

  • 优点:
    实现简单,不需要增加额外的拦截器等。并且多库汇聚非常方便。
  • 缺点:
    在业务开发中,需要在方法参数列表中,特别是DAO层,增加 分片字段参数。
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

中间件兴趣圈

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值