赞
踩
本文主要来介绍多租户方案。
环境说明:
用户在用用户名登陆时首先需要根据用户名,查询到该用户所在的逻辑库,然后登陆成功后,将和会话信息存放在一起,方便在访问其他业务的时候,能够很方便的得到该逻辑库。与此同时,利用Mybatis 提供的 SQL拦截器机制与Mycat提供的注解,改写SQL语句为 sql = “/*!mycat:schema=” + tenant + " */" + sql; 这样Mycat在解析时,会自动路由到tenat逻辑库上执行SQL语句。
Mycat 模拟配置如下:
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缓存等。该处可以扩展。
通过成功登录系统后,就能得到 逻辑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】
业务方法无需改变;但是要利用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 } }
配置如下:
该方案代码:请关注如下代码:
相关代码我已上传到:https://github.com/dingwpmz/Mycat-Demo
每个分片对应一个集团,每个业务表中增加一个分片字段 db_pos,类型为int型,比如制定如下字段:
0 h_xsgizx
20 h_xsyz
40 m_fhzx
60 m_mzzx
Mycat 提供一个逻辑库,其中每个分片代表一个集团,,由于集团数量是固定的,故可以采用 分片枚举 进行分片。
这种方案,不是传统意义上的多租户,而是用mycat枚举分片规则。配置分片就好。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。