MyException - 我的异常网
当前位置:我的异常网» 数据库 » Mycat 多租户议案 (1)

Mycat 多租户议案 (1)

www.MyException.Cn  网友分享于:2013-10-08  浏览:0次
Mycat 多租户方案 (1)
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();
    }

}

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23

那控制器层代码的伪代码如下:
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。。。");
        }

    }

}

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32

统一处理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();  
        }   
    }   

}

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57

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

    }

}

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86

3)配置如下:

这里写图片描述

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.4、方案优缺点
优点:
实现简单,不需要增加额外的拦截器等。并且多库汇聚非常方便。
缺点:
在业务开发中,需要在方法参数列表中,特别是DAO层,增加 分片字段参数。

文章评论

当下全球最炙手可热的八位少年创业者
当下全球最炙手可热的八位少年创业者
代码女神横空出世
代码女神横空出世
团队中“技术大拿”并非越多越好
团队中“技术大拿”并非越多越好
老美怎么看待阿里赴美上市
老美怎么看待阿里赴美上市
老程序员的下场
老程序员的下场
亲爱的项目经理,我恨你
亲爱的项目经理,我恨你
我的丈夫是个程序员
我的丈夫是个程序员
2013年中国软件开发者薪资调查报告
2013年中国软件开发者薪资调查报告
我是如何打败拖延症的
我是如何打败拖延症的
程序员最害怕的5件事 你中招了吗?
程序员最害怕的5件事 你中招了吗?
鲜为人知的编程真相
鲜为人知的编程真相
聊聊HTTPS和SSL/TLS协议
聊聊HTTPS和SSL/TLS协议
程序猿的崛起——Growth Hacker
程序猿的崛起——Growth Hacker
5款最佳正则表达式编辑调试器
5款最佳正则表达式编辑调试器
什么才是优秀的用户界面设计
什么才是优秀的用户界面设计
写给自己也写给你 自己到底该何去何从
写给自己也写给你 自己到底该何去何从
程序员都该阅读的书
程序员都该阅读的书
Web开发人员为什么越来越懒了?
Web开发人员为什么越来越懒了?
要嫁就嫁程序猿—钱多话少死的早
要嫁就嫁程序猿—钱多话少死的早
程序员的一天:一寸光阴一寸金
程序员的一天:一寸光阴一寸金
中美印日四国程序员比较
中美印日四国程序员比较
看13位CEO、创始人和高管如何提高工作效率
看13位CEO、创始人和高管如何提高工作效率
如何成为一名黑客
如何成为一名黑客
每天工作4小时的程序员
每天工作4小时的程序员
我跳槽是因为他们的显示器更大
我跳槽是因为他们的显示器更大
 程序员的样子
程序员的样子
那些争议最大的编程观点
那些争议最大的编程观点
程序员的鄙视链
程序员的鄙视链
程序员必看的十大电影
程序员必看的十大电影
为什么程序员都是夜猫子
为什么程序员都是夜猫子
旅行,写作,编程
旅行,写作,编程
那些性感的让人尖叫的程序员
那些性感的让人尖叫的程序员
为啥Android手机总会越用越慢?
为啥Android手机总会越用越慢?
十大编程算法助程序员走上高手之路
十大编程算法助程序员走上高手之路
Java程序员必看电影
Java程序员必看电影
程序员和编码员之间的区别
程序员和编码员之间的区别
科技史上最臭名昭著的13大罪犯
科技史上最臭名昭著的13大罪犯
初级 vs 高级开发者 哪个性价比更高?
初级 vs 高级开发者 哪个性价比更高?
10个调试和排错的小建议
10个调试和排错的小建议
一个程序员的时间管理
一个程序员的时间管理
“懒”出效率是程序员的美德
“懒”出效率是程序员的美德
“肮脏的”IT工作排行榜
“肮脏的”IT工作排行榜
程序员周末都喜欢做什么?
程序员周末都喜欢做什么?
10个帮程序员减压放松的网站
10个帮程序员减压放松的网站
60个开发者不容错过的免费资源库
60个开发者不容错过的免费资源库
Google伦敦新总部 犹如星级庄园
Google伦敦新总部 犹如星级庄园
编程语言是女人
编程语言是女人
Java 与 .NET 的平台发展之争
Java 与 .NET 的平台发展之争
2013年美国开发者薪资调查报告
2013年美国开发者薪资调查报告
软件开发程序错误异常ExceptionCopyright © 2009-2015 MyException 版权所有