業務場景
公司需要開發一個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平臺,並且接口平臺業務單一,所以採用切分函數的方案非常適合。