业务场景
公司需要开发一个SAAS平台,考虑到数据的安全性和隔离级别,打算采用Mycat做为中间件,使用Mycat的多租户方案,实现租户数据的独立性。
Mycat提供的两种多租户方案
基于Mycat注解的方式,动态切schema
优点:适用于传统的每个租户部署一套 web+db 的老系统升级为新的SAAS系统,这种方式改动较少,侵入性较小。
方案详解 [Mybatis拦截器+Mycat注解]
1.编写Mybatis拦截器
/**
* Mycat多租户拦截器
*
* @author jinliang 2018/11/29 11:26
*/
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
@Component
public class MycatTenantInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
//获取当前登录的用户信息
CustomUserDetails userDetails = DetailsHelper.getUserDetails();
if(userDetails == null){
//如果没有用户信息,则不进行操作 TODO
return invocation.proceed();
}
//获取租户ID
Long organizationId = userDetails.getOrganizationId();
//通过租户ID查询租户的schema TODO
String schema = "test_interface1";
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
BoundSql boundSql = statementHandler.getBoundSql();
//获取到原始sql语句
String sql = boundSql.getSql();
System.out.println("处理之前" + sql);
sql = "/*!mycat:schema=" + schema + " */" + sql;
//通过反射修改sql语句
System.out.println("处理之后" + sql);
Field field = boundSql.getClass().getDeclaredField("sql");
field.setAccessible(true);
field.set(boundSql, sql);
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
2.配置租户和Schema的映射关系
该方案,需要在用户登录时将用户信息放到一个线程的ThreadLocal变量中,然后通过用户信息去找到租户的信息,根据租户信息返回schema。将schema和mycat的注解拼接起来,mycat根据注解指定的schema动态的去切换,最终能达到多租户的效果。
注意:该方案一定要考虑多线程的问题,因为如果出现了多线程问题,A集团的数据发到了B集团中,这种风险是巨大的,所以一定要仔细。
3.测试
使用不同租户下的用户登录系统,进行CRUD操作,验证数据是否安装预期的结果分发到不同的库。
基于分片函数的方式,共用schema
优点:这种方式适用于新的SAAS平台的开发,要求在设计的时候租户相关的表需要设计tenant_id字段,后面数据分库的时候根据tenant_id切分,不同的数据写到不同的节点上。
1.确定系统中哪些表为全局表,哪些表为分片表
因为我们做的是一个接口平台,所以拆分相对简单。所有接口平台相关的表都配置为全局表,接口相关的表配置为分片表。
2.确定分片函数
- rule.xml
<?xml version="1.0" encoding="UTF-8"?>
<!-- - - Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License. - You
may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0
- - Unless required by applicable law or agreed to in writing, software -
distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the
License for the specific language governing permissions and - limitations
under the License. -->
<!DOCTYPE mycat:rule SYSTEM "rule.dtd">
<mycat:rule xmlns:mycat="http://io.mycat/">
<tableRule name="sharding-by-tenant">
<rule>
<columns>tenant_id</columns>
<algorithm>by-tenant</algorithm>
</rule>
</tableRule>
<function name="by-tenant" class="io.mycat.route.function.PartitionByFileMap">
<property name="mapFile">sharding-by-tenant.txt</property>
<property name="type">0</property>
<property name="defaultNode">0</property>
</function>
- sharding-by-tenant.txt
0=0
90102=1
90103=2
10020=3
配置规则为租户ID,配置了一个默认节点,如果根据分片规则没找到具体的库则把数据写入0这个节点,这种方式有种优势在于,有的小型租户,数据量不是特别大,并且对数据隔离性要求不高,就可以不用配置一个租户一个库,直接往默认的节点插入。
3.schema.xml
<?xml version="1.0"?>
<!DOCTYPE mycat:schema SYSTEM "schema.dtd">
<mycat:schema xmlns:mycat="http://io.mycat/">
<schema name="test_interface" checkSQLschema="true" >
<!--全局表-->
<table name="test_external_systems" primaryKey="external_system_id" type="global" dataNode="testDn$0-3" />
<!--分库表-->
<table name="test_asn_header_exp" rule="sharding-by-tenant" dataNode="testDn$0-3" />
<!--跨库查询的表-->
<table name="hpfm_tenant" primaryKey="tenant_id" autoIncrement="true" dataNode="test_platform" />
</schema>
<!--用于information_schema查询schema问题-->
<schema name="test_interface_default" checkSQLschema="false" sqlMaxLimit="100" dataNode="testDn0" >
</schema>
<dataNode name="testDn0" dataHost="mysql127" database="test_interface"/>
<dataNode name="testDn1" dataHost="mysql127" database="test_interface_90102"/>
<dataNode name="testDn2" dataHost="mysql127" database="test_interface_90103"/>
<dataNode name="testDn3" dataHost="mysql127" database="test_interface_10020"/>
<dataNode name="test_platform" dataHost="test_platform" database="test_platform" />
<dataNode name="test_supplier" dataHost="test_supplier" database="test_supplier" />
<dataNode name="test_order" dataHost="test_order" database="test_order" />
<dataNode name="test_mdm" dataHost="test_mdm" database="test_mdm" />
<dataHost balance="3" maxCon="1000" minCon="10" name="mysql127" writeType="0" switchType="1" dbType="mysql" dbDriver="native">
<heartbeat>select user()</heartbeat>
<writeHost host="192.168.56.127" url="192.168.56.127:3306" password="123456" user="root"/>
</dataHost>
<dataHost balance="3" maxCon="1000" minCon="10" name="test_platform" writeType="0" switchType="1" dbType="mysql" dbDriver="native">
<heartbeat>select user()</heartbeat>
<writeHost host="192.168.56.127" url="192.168.56.127:3306" password="123456" user="root"/>
</dataHost>
<dataHost balance="3" maxCon="1000" minCon="10" name="test_supplier" writeType="0" switchType="1" dbType="mysql" dbDriver="native">
<heartbeat>select user()</heartbeat>
<writeHost host="192.168.56.127" url="192.168.56.127:3306" password="123456" user="root"/>
</dataHost>
<dataHost balance="3" maxCon="1000" minCon="10" name="test_order" writeType="0" switchType="1" dbType="mysql" dbDriver="native">
<heartbeat>select user()</heartbeat>
<writeHost host="192.168.56.127" url="192.168.56.127:3306" password="123456" user="root"/>
</dataHost>
<dataHost balance="3" maxCon="1000" minCon="10" name="test_mdm" writeType="0" switchType="1" dbType="mysql" dbDriver="native">
<heartbeat>select user()</heartbeat>
<writeHost host="192.168.56.127" url="192.168.56.127:3306" password="123456" user="root"/>
</dataHost>
</mycat:schema>
4.测试
使用不同租户下的用户登录系统,进行CRUD操作,验证数据是否安装预期的结果分发到不同的库。
如果没有配置切分规则,该租户的数据将被分发到默认的节点上
5.注意
1.因为配置表没有tenant_id,所以配置为全局表
2.因为系统涉及到动态建表,需要去查询 information_schema 所以配置了一个默认的 schema,并且该schema指定到一个具体的物理库,在sql里通过 /*!mycat:schema=schema的名字 */ 注解去指定该sql该发往哪个具体的schema。[如果不指定,查询 information_schema 没有tenant_id字段,mycat会把sql随机发到物理节点,导致结果不一致]
总结
因为我们是新的SAAS平台,并且接口平台业务单一,所以采用切分函数的方案非常适合。