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层,增加 分片字段参数。
详细解决方案
Mycat 多租户方案 (1)
热度:9 发布时间:2023-12-21 10:54:45.0
相关解决方案
- 分布式数据库架构-MyCAT
- mycat的mycat-web安装与使用(mycat-eye和zookeeper)
- 搭建分布式数据库负载均衡运行环境的一些配置记录(2)mycat
- 分布式数据存储(Elasticsearch(搜索引擎解决方案)+Mycat)
- mycat-1、检测工具mycat-web的安装和使用
- mycat-3、使用mycat将mysql实现主从复制,读写分离
- mycat-2、window和linux安装配置mycat进行分表分库
- MyCAT 日志文件描述
- Mycat 水平拆分----分表
- Mycat 垂直拆分--分库
- Mycat 读写分离,主从切换
- Linux:部署keepalived为mycat实现高可用【keepalived+mycat】
- Mycat 多租户方案 (1)
- Mycat(一)概述
- MyCat——数据读写分离、分库分表以及HA搭建
- mycat 测试后的结果让我决定放弃mycat作为中间件
- mycat-web 安装
- mycat 中间件 mysql 主从读写分离
- 【MyCat】mycat实现mysql读写分离
- 【MyCat】mycat简介
- Mycat+MySQL实现分表分库实例
- MyCat & Nginx
- MyCat 增加分区
- Mycat---启动(三)
- 阿里集团中间件4面:J.U.C并发框架+RocketMQ +MyCat+锁机制+架构
- MySQL数据库集群项目实战(主从架构、mycat、haproxy负载均衡、PXC集群)
- mycat 垂直切分
- mycat 水平分库
- Mycat 结果集处理源码剖析
- 数据库中间件 MyCAT 读写分离实现