Mycat 多租户方案 注解、切分函数

业务场景

公司需要开发一个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



<mycat:rule xmlns:mycat="http://io.mycat/">
    <tableRule name="sharding-by-tenant">
      <rule>
         <columns>tenant_idcolumns>
         <algorithm>by-tenantalgorithm>
      rule>
    tableRule>
    <function name="by-tenant" class="io.mycat.route.function.PartitionByFileMap">
        <property name="mapFile">sharding-by-tenant.txtproperty>
        <property name="type">0property>
        <property name="defaultNode">0property>
    function>

  • sharding-by-tenant.txt
0=0
90102=1
90103=2
10020=3

配置规则为租户ID,配置了一个默认节点,如果根据分片规则没找到具体的库则把数据写入0这个节点,这种方式有种优势在于,有的小型租户,数据量不是特别大,并且对数据隔离性要求不高,就可以不用配置一个租户一个库,直接往默认的节点插入。

3.schema.xml



<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>

<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平台,并且接口平台业务单一,所以采用切分函数的方案非常适合。

你可能感兴趣的:(MyCat)