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
<?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平臺,並且接口平臺業務單一,所以採用切分函數的方案非常適合。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章