mybatis整合mycat實現分庫

在這裏插入圖片描述

前言

工作中我們可能會遇到的一個問題,可能會出現多租戶場景,這種情況下,我們不得不對我們的系統分庫,對於每一個租戶來說都是一個數據庫,這個我們可能考慮到多數據源去解決,也是一個思路,這幾天調研了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數據庫。

  1. 解壓修改配置文件, 首先是conf目錄下的server.xml文件,修改mycat的用戶名及密碼。默認端口號是8066。
<user name="root">
		<property name="password">batman</property>
        <property name="schemas">t1,t2</property>
	</user>
  1. 修改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>

  1. 測試
啓動:
./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的配置和租戶信息數據庫配置好,利用反射寫回BoundSqlsql屬性。

@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

參考

發佈了97 篇原創文章 · 獲贊 100 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章