前言
工作中我們可能會遇到的一個問題,可能會出現多租戶場景,這種情況下,我們不得不對我們的系統分庫,對於每一個租戶來說都是一個數據庫,這個我們可能考慮到多數據源去解決,也是一個思路,這幾天調研了mycat
做分庫,下面慢慢分享這一個過程。
如何獲取當前線程的租戶
我們首先要解決這個問題,今天首先要出場的是ThreadLocal
,對於這個類我的解釋是:
- 保存線程上下文信息,在任意需要的地方可以獲取!!!
- 線程安全的,避免某些情況需要考慮線程安全必須同步帶來的性能損失!!!
之前看過阿里規範有這條:
實現
首先來一個接口BatmanTenant
,這個接口主要封裝兩個方法。
public interface BatmanTenant {
void setBatmanTenantId(String var1);
String getBatmanTenantId();
}
接着創建一個實現類TenantStore
去實現BatmanTenant
,一個靜態變量CONTEXT
存取上下文的租戶信息。
public class TenantStore implements BatmanTenant {
private static final ThreadLocal<String> CONTEXT = new TransmittableThreadLocal<>();
private static boolean isApplicationTenant = false;
private static String applicationTenantId;
private static final String TENANT_DEFAULT_ID = "t0";
public static void setTenantId(String tenantId) {
CONTEXT.set(tenantId);
}
public static String getTenantId() {
if (isApplicationTenant) {
return applicationTenantId;
}
String tenantId = CONTEXT.get();
if (tenantId == null || "".equals(tenantId)) {
tenantId = TENANT_DEFAULT_ID;
}
return tenantId;
}
public static void clear() {
CONTEXT.remove();
}
public static boolean isApplicationTenant() {
return isApplicationTenant;
}
public static void setApplicationTenant(boolean applicationTenant) {
isApplicationTenant = applicationTenant;
}
public static String getApplicationTenantId() {
return applicationTenantId;
}
public static void setApplicationTenantId(String applicationTenantId) {
TenantStore.applicationTenantId = applicationTenantId;
}
@Override
public void setBatmanTenantId(String s) {
setTenantId(s);
}
@Override
public String getBatmanTenantId() {
return getTenantId();
}
}
Mycat 服務搭建
mycat是一個數據庫中間件,也可以理解爲是數據庫代理。在架構體系中是位於數據庫和應用層之間的一個組件,並且對於應用層是透明的,即數據庫 感受不到mycat的存在,認爲是直接連接的mysql數據庫。
- 下載鏈接 http://dl.mycat.io/1.6.7.4/,現在對應的版本
- 解壓修改配置文件, 首先是
conf
目錄下的server.xml文件
,修改mycat的用戶名及密碼。默認端口號是8066。
<user name="root">
<property name="password">batman</property>
<property name="schemas">t1,t2</property>
</user>
- 修改
conf
目錄下的schema.xml
,將下面配置拷貝過去即可。
<?xml version="1.0"?>
<!DOCTYPE mycat:schema SYSTEM "schema.dtd">
<mycat:schema xmlns:mycat="http://io.mycat/">
<!-- 設置dataNode 對應的數據庫,及 mycat 連接的地址dataHost -->
<schema name="t1" checkSQLschema="false" sqlMaxLimit="100" dataNode="dn_t1" />
<schema name="t2" checkSQLschema="false" sqlMaxLimit="100" dataNode="dn_t2" />
<dataNode name="dn_t1" dataHost="dh" database="t1"/>
<dataNode name="dn_t2" dataHost="dh" database="t2"/>
<!-- mycat 邏輯主機dataHost對應的物理主機.其中也設置對應的mysql登陸信息 -->
<dataHost name="dh" maxCon="1000" minCon="10" balance="0" writeType="0"
dbType="mysql" dbDriver="native" switchType="2" slaveThreshold="100">
<!--<heartbeat>select user()</heartbeat>-->
<heartbeat>select user()</heartbeat>
<writeHost host="tenant_db" url="localhost:3306" user="root" password="root">
<readHost host="tenant_db" url="localhost:3306" user="root" password="root"/>
</writeHost>
</dataHost>
</mycat:schema>
- 測試
啓動:
./mycat start
查看啓動狀態:
./mycat status
停止:
./mycat stop
重啓(改變上面的 xml 配置不用重啓,管理端可以重新載入):
./mycat restart
查看 logs/ 下的 wrapper.log 和 mycat.log 可以查看運行時問題和異常。
mycat 啓動日誌:
cat ./logs/wrapper.log
mycat 應用日誌:
cat ./logs/mycat.log
添加mybatis
的攔截器
創建TenantInterceptor
這個文件,從StatementHandler
獲取到BoundSql
對象,這樣就獲取到要執行的sql
,把mycat的配置和租戶信息數據庫配置好,利用反射寫回BoundSql
的sql
屬性。
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class TenantInterceptor implements Interceptor {
private static final Logger LOGGER = LoggerFactory.getLogger(TenantInterceptor.class);
private static final String SCHEMA_START = "/*mycat:schema=";
private static final String SCHEMA_END = "*/";
@Override
public Object intercept(Invocation invocation) throws Throwable {
String tenant = TenantStore.getTenantId();
if (tenant == null || "".equals(tenant)) {
return invocation.proceed();
}
StatementHandler statementHandler = realTarget(invocation.getTarget());
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
String sql = boundSql.getSql();
//LOGGER.debug("TenantInterceptor before sql:" + sql);
//add sql mycat hits for sql route
//sql = "/*!mycat:schema=" + tenant + "*/" + sql;
if (!sql.startsWith(SCHEMA_START)) {
StringBuilder stringBuilder = new StringBuilder(sql.length() + 30);
stringBuilder.append(SCHEMA_START);
stringBuilder.append(tenant);
stringBuilder.append(SCHEMA_END);
stringBuilder.append(sql);
sql = stringBuilder.toString();
}
LOGGER.debug("TenantInterceptor after sql:" + sql);
ReflectHelper.setFieldValue(boundSql, "sql", sql);
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
if (target instanceof StatementHandler) {
return Plugin.wrap(target, this);
} else {
return target;
}
}
@Override
public void setProperties(Properties properties) {
}
/**
* <p>
* 獲得真正的處理對象,可能多層代理.
* </p>
*/
@SuppressWarnings("unchecked")
private static <T> T realTarget(Object target) {
if (Proxy.isProxyClass(target.getClass())) {
MetaObject metaObject = SystemMetaObject.forObject(target);
return realTarget(metaObject.getValue("h.target"));
}
return (T) target;
}
}
配置攔截器,創建MultiTenantMyBatisConfiguration
,給每個SqlSessionFactory
對象添加攔截。
@Configuration
@ConditionalOnClass({SqlSessionFactory.class, Interceptor.class})
public class MultiTenantMyBatisConfiguration {
@Autowired
private List<SqlSessionFactory> sqlSessionFactoryList;
@PostConstruct
public void addPageInterceptor() {
for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
sqlSessionFactory.getConfiguration().addInterceptor(new TenantInterceptor());
}
}
}
測試
創建兩個數據庫分別爲t1,t2。兩個數據庫有都有demo這張表。測試下面接口,會出現不同的結果。
github地址:https://github.com/fafeidou/fast-cloud-nacos/tree/master/fast-common-examples/fast-common-tenant-example