一文快速入門分庫分表中間件 Sharding-JDBC (必修課)

書接上文 《一文快速入門分庫分表(必修課)》,這篇拖了好長的時間,原本計劃在一週前就該寫完的,結果家庭內部忽然人事調整,領導層進行權利交接,隨之宣佈我正式當爹,緊接着家庭地位滑落至第三名,還給我分配了一個長期維護任務:帶娃。看看咱們的靚照,標準的小淑女一枚萌萌噠。javascript

千金小姐姐

做爲Sharding-JDBC 分庫分表實戰系列的開篇文章,咱們在前文中回顧了一下分庫分表的基礎知識,對分庫分表的拆分方式有了必定的瞭解,下邊咱們介紹一下 Sharding-JDBC框架和快速的搭建一個分庫分表案例,爲講解後續功能點準備好環境。java

1、Sharding-JDBC 簡介

Sharding-JDBC 最先是噹噹網內部使用的一款分庫分表框架,到2017年的時候纔開始對外開源,這幾年在大量社區貢獻者的不斷迭代下,功能也逐漸完善,現已改名爲 ShardingSphere,2020年4⽉16⽇正式成爲 Apache 軟件基⾦會的頂級項⽬。node

隨着版本的不斷更迭 ShardingSphere 的核心功能也變得多元化起來。從最開始 Sharding-JDBC 1.0 版本只有數據分片,到 Sharding-JDBC 2.0 版本開始支持數據庫治理(註冊中心、配置中心等等),再到 Sharding-JDBC 3.0版本又加分佈式事務 (支持 AtomikosNarayanaBitronixSeata),現在已經迭代到了 Sharding-JDBC 4.0 版本。mysql

在這裏插入圖片描述

如今的 ShardingSphere 不僅僅是指某個框架而是一個生態圈,這個生態圈 Sharding-JDBCSharding-ProxySharding-Sidecar 這三款開源的分佈式數據庫中間件解決方案所構成。git

ShardingSphere 的前身就是 Sharding-JDBC,因此它是整個框架中最爲經典、成熟的組件,咱們先從 Sharding-JDBC 框架入手學習分庫分表。程序員

2、核心概念

在開始 Sharding-JDBC分庫分表具體實戰以前,咱們有必要先了解分庫分表的一些核心概念。github

分片

通常咱們在提到分庫分表的時候,大可能是以水平切分模式(水平分庫、分表)爲基礎來講的,數據分片將本來一張數據量較大的表 t_order 拆分生成數個表結構徹底一致的小數據量表 t_order_0t_order_1、···、t_order_n,每張表只存儲原大表中的一部分數據,當執行一條SQL時會經過 分庫策略分片策略 將數據分散到不一樣的數據庫、表內。算法

在這裏插入圖片描述

數據節點

數據節點是分庫分表中一個不可再分的最小數據單元(表),它由數據源名稱和數據表組成,例如上圖中 order_db_1.t_order_0order_db_2.t_order_1 就表示一個數據節點。spring

邏輯表

邏輯表是指一組具備相同邏輯和數據結構表的總稱。好比咱們將訂單表t_order 拆分紅 t_order_0 ··· t_order_9 等 10張表。此時咱們會發現分庫分表之後數據庫中已不在有 t_order 這張表,取而代之的是 t_order_n,但咱們在代碼中寫 SQL 依然按 t_order 來寫。此時 t_order 就是這些拆分表的邏輯表sql

真實表

真實表也就是上邊提到的 t_order_n 數據庫中真實存在的物理表。

分片鍵

用於分片的數據庫字段。咱們將 t_order 表分片之後,當執行一條SQL時,經過對字段 order_id 取模的方式來決定,這條數據該在哪一個數據庫中的哪一個表中執行,此時 order_id 字段就是 t_order 表的分片健。

在這裏插入圖片描述

這樣以來同一個訂單的相關數據就會存在同一個數據庫表中,大幅提高數據檢索的性能,不只如此 sharding-jdbc 還支持根據多個字段做爲分片健進行分片。

分片算法

上邊咱們提到能夠用分片健取模的規則分片,但這只是比較簡單的一種,在實際開發中咱們還但願用 >=<=><BETWEENIN 等條件做爲分片規則,自定義分片邏輯,這時就須要用到分片策略與分片算法。

從執行 SQL 的角度來看,分庫分表能夠看做是一種路由機制,把 SQL 語句路由到咱們指望的數據庫或數據表中並獲取數據,分片算法能夠理解成一種路由規則。

我們先捋一下它們之間的關係,分片策略只是抽象出的概念,它是由分片算法和分片健組合而成,分片算法作具體的數據分片邏輯。

分庫、分表的分片策略配置是相對獨立的,能夠各自使用不一樣的策略與算法,每種策略中能夠是多個分片算法的組合,每一個分片算法能夠對多個分片健作邏輯判斷。

分片算法和分片策略的關係

注意:sharding-jdbc 並無直接提供分片算法的實現,須要開發者根據業務自行實現。

sharding-jdbc 提供了4種分片算法:

一、精確分片算法

精確分片算法(PreciseShardingAlgorithm)用於單個字段做爲分片鍵,SQL中有 =IN 等條件的分片,須要在標準分片策略(StandardShardingStrategy )下使用。

二、範圍分片算法

範圍分片算法(RangeShardingAlgorithm)用於單個字段做爲分片鍵,SQL中有 BETWEEN AND><>=<= 等條件的分片,須要在標準分片策略(StandardShardingStrategy )下使用。

三、複合分片算法

複合分片算法(ComplexKeysShardingAlgorithm)用於多個字段做爲分片鍵的分片操做,同時獲取到多個分片健的值,根據多個字段處理業務邏輯。須要在複合分片策略(ComplexShardingStrategy )下使用。

四、Hint分片算法

Hint分片算法(HintShardingAlgorithm)稍有不一樣,上邊的算法中咱們都是解析SQL 語句提取分片鍵,並設置分片策略進行分片。但有些時候咱們並無使用任何的分片鍵和分片策略,可還想將 SQL 路由到目標數據庫和表,就須要經過手動干預指定SQL的目標數據庫和表信息,這也叫強制路由。

分片策略

上邊講分片算法的時候已經說過,分片策略是一種抽象的概念,實際分片操做的是由分片算法和分片健來完成的。

一、標準分片策略

標準分片策略適用於單分片鍵,此策略支持 PreciseShardingAlgorithmRangeShardingAlgorithm 兩個分片算法。

其中 PreciseShardingAlgorithm 是必選的,用於處理 =IN 的分片。RangeShardingAlgorithm 是可選的,用於處理BETWEEN AND><>=<= 條件分片,若是不配置RangeShardingAlgorithm,SQL中的條件等將按照全庫路由處理。

二、複合分片策略

複合分片策略,一樣支持對 SQL語句中的 =><>=<=INBETWEEN AND 的分片操做。不一樣的是它支持多分片鍵,具體分配片細節徹底由應用開發者實現。

三、行表達式分片策略

行表達式分片策略,支持對 SQL語句中的 =IN 的分片操做,但只支持單分片鍵。這種策略一般用於簡單的分片,不須要自定義分片算法,能夠直接在配置文件中接着寫規則。

t_order_$->{t_order_id % 4} 表明 t_order 對其字段 t_order_id取模,拆分紅4張表,而表名分別是t_order_0t_order_3

四、Hint分片策略

Hint分片策略,對應上邊的Hint分片算法,經過指定分片健而非從 SQL中提取分片健的方式進行分片的策略。

分佈式主鍵

數據分⽚後,不一樣數據節點⽣成全局惟⼀主鍵是⾮常棘⼿的問題,同⼀個邏輯表(t_order)內的不一樣真實表(t_order_n)之間的⾃增鍵因爲⽆法互相感知而產⽣重複主鍵。

儘管可經過設置⾃增主鍵 初始值步⻓ 的⽅式避免ID碰撞,但這樣會使維護成本加大,乏完整性和可擴展性。若是後去須要增長分片表的數量,要逐一修改分片表的步長,運維成本很是高,因此不建議這種方式。

實現分佈式主鍵⽣成器的方式不少,能夠參考我以前寫的《9種分佈式ID生成方式》。

爲了讓上手更加簡單,ApacheShardingSphere 內置了UUIDSNOWFLAKE 兩種分佈式主鍵⽣成器,默認使⽤雪花算法(snowflake)⽣成64bit的⻓整型數據。不只如此它還抽離出分佈式主鍵⽣成器的接口,⽅便咱們實現⾃定義的⾃增主鍵⽣成算法。

廣播表

廣播表:存在於全部的分片數據源中的表,表結構和表中的數據在每一個數據庫中均徹底一致。通常是爲字典表或者配置表 t_config,某個表一旦被配置爲廣播表,只要修改某個數據庫的廣播表,全部數據源中廣播表的數據都會跟着同步。

綁定表

綁定表:那些分片規則一致的主表和子表。好比:t_order 訂單表和 t_order_item 訂單服務項目表,都是按 order_id 字段分片,所以兩張表互爲綁定表關係。

那綁定表存在的意義是啥呢?

一般在咱們的業務中都會使用 t_ordert_order_item 等表進行多表聯合查詢,但因爲分庫分表之後這些表被拆分紅N多個子表。若是不配置綁定表關係,會出現笛卡爾積關聯查詢,將產生以下四條SQL

SELECT * FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id 
SELECT * FROM t_order_0 o JOIN t_order_item_1 i ON o.order_id=i.order_id 
SELECT * FROM t_order_1 o JOIN t_order_item_0 i ON o.order_id=i.order_id 
SELECT * FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id

笛卡爾積查詢

而配置綁定表關係後再進行關聯查詢時,只要對應表分片規則一致產生的數據就會落到同一個庫中,那麼只需 t_order_0t_order_item_0 表關聯便可。

SELECT * FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id 
SELECT * FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id

綁定表關係

注意:在關聯查詢時 t_order 它做爲整個聯合查詢的主表。 全部相關的路由計算都只使用主表的策略,t_order_item 表的分片相關的計算也會使用 t_order 的條件,因此要保證綁定表之間的分片鍵要徹底相同。

3、和JDBC的貓膩

從名字上不難看出,Sharding-JDBCJDBC有很大關係,咱們知道 JDBC 是一種 Java 語言訪問關係型數據庫的規範,其設計初衷就是要提供一套用於各類數據庫的統一標準,不一樣廠家共同遵照這套標準,並提供各自的實現方案供應用程序調用。

在這裏插入圖片描述

但其實對於開發人員而言,咱們只關心如何調用 JDBC API 來訪問數據庫,只要正確使用 DataSourceConnectionStatementResultSet 等 API 接口,直接操做數據庫便可。因此若是想在 JDBC 層面實現數據分片就必須對現有的 API 進行功能拓展,而 Sharding-JDBC 正是基於這種思想,重寫了 JDBC 規範並徹底兼容了 JDBC 規範。

JDBC流程

對原有的 DataSourceConnection 等接口擴展成 ShardingDataSourceShardingConnection,而對外暴露的分片操做接口與 JDBC 規範中所提供的接口徹底一致,只要你熟悉 JDBC 就能夠輕鬆應用 Sharding-JDBC 來實現分庫分表。

在這裏插入圖片描述

所以它適用於任何基於 JDBCORM 框架,如:JPAHibernateMybatisSpring JDBC Template 或直接使用的 JDBC。完美兼容任何第三方的數據庫鏈接池,如:DBCPC3P0BoneCPDruidHikariCP 等,幾乎對主流關係型數據庫都支持。

Sharding-JDBC 又是如何拓展這些接口的呢?想知道答案咱們就的從源碼入手了,下邊咱們以 JDBC API 中的 DataSource 爲例看看它是如何被重寫擴展的。

數據源 DataSource 接口的核心做用就是獲取數據庫鏈接對象 Connection,咱們看其內部提供了兩個獲取數據庫鏈接的方法 ,而且繼承了 CommonDataSourceWrapper 兩個接口。

public interface DataSource  extends CommonDataSource, Wrapper {

  /**
   * <p>Attempts to establish a connection with the data source that
   * this {@code DataSource} object represents.
   * @return  a connection to the data source
   */
  Connection getConnection() throws SQLException;

  /**
   * <p>Attempts to establish a connection with the data source that
   * this {@code DataSource} object represents.
   * @param username the database user on whose behalf the connection is
   *  being made
   * @param password the user's password
   */
  Connection getConnection(String username, String password)
    throws SQLException;
}

其中 CommonDataSource 是定義數據源的根接口這很好理解,而 Wrapper 接口則是拓展 JDBC 分片功能的關鍵。

因爲數據庫廠商的不一樣,他們可能會各自提供一些超越標準 JDBC API 的擴展功能,但這些功能非 JDBC 標準並不能直接使用,而 Wrapper 接口的做用就是把一個由第三方供應商提供的、非 JDBC 標準的接口包裝成標準接口,也就是適配器模式

既然講到了適配器模式就多囉嗦幾句,也方便後邊的理解。

適配器模式個種比較經常使用的設計模式,它的做用是將某個類的接口轉換成客戶端指望的另外一個接口,使本來因接口不匹配(或者不兼容)而沒法在一塊兒工做的兩個類可以在一塊兒工做。
好比用耳機聽音樂,我有個圓頭的耳機,可手機插孔倒是扁口的,若是我想要使用耳機聽音樂就必須藉助一個轉接頭才能夠,這個轉接頭就起到了適配做用。
舉個栗子:假如咱們 Target 接口中有 hello()word() 兩個方法。

public interface Target {

    void hello();

    void world();
}

可因爲接口版本迭代Target 接口的 word() 方法可能會被廢棄掉或不被支持,Adaptee 類的 greet()方法將代替hello() 方法。

public class Adaptee {

    public void greet(){

    }
    public void world(){

    }
}

但此時舊版本仍然有大量 word() 方法被使用中,解決此事最好的辦法就是建立一個適配器Adapter,這樣就適配了 Target 類,解決了接口升級帶來的兼容性問題。

public class Adapter extends Adaptee implements Target {

    @Override
    public void world() {

    }

    @Override
    public void hello() {
        super.greet();
    }

    @Override
    public void greet() {

    }
}

Sharding-JDBC 提供的正是非 JDBC 標準的接口,因此它也提供了相似的實現方案,也使用到了 Wrapper 接口作數據分片功能的適配。除了 DataSource 以外,Connection、Statement、ResultSet 等核心對象也都繼承了這個接口。

下面咱們經過 ShardingDataSource 類源碼簡單看下實現過程,下圖是繼承關係流程圖。

ShardingDataSource實現流程

ShardingDataSource 類它在原 DataSource 基礎上作了功能拓展,初始化時註冊了分片SQL路由包裝器、SQL重寫上下文和結果集處理引擎,還對數據源類型作了校驗,由於它要同時支持多個不一樣類型的數據源。到這好像也沒看出如何適配,那接着向上看 ShardingDataSource 的繼承類 AbstractDataSourceAdapter

@Getter
public class ShardingDataSource extends AbstractDataSourceAdapter {

    private final ShardingRuntimeContext runtimeContext;

    /**
     * 註冊路由、SQl重寫上下文、結果集處理引擎
     */
    static {
        NewInstanceServiceLoader.register(RouteDecorator.class);
        NewInstanceServiceLoader.register(SQLRewriteContextDecorator.class);
        NewInstanceServiceLoader.register(ResultProcessEngine.class);
    }

    /**
     * 初始化時校驗數據源類型 並根據數據源 map、分片規則、數據庫類型獲得一個分片上下文,用來獲取數據庫鏈接
     */
    public ShardingDataSource(final Map<String, DataSource> dataSourceMap, final ShardingRule shardingRule, final Properties props) throws SQLException {
        super(dataSourceMap);
        checkDataSourceType(dataSourceMap);
        runtimeContext = new ShardingRuntimeContext(dataSourceMap, shardingRule, props, getDatabaseType());
    }

    private void checkDataSourceType(final Map<String, DataSource> dataSourceMap) {
        for (DataSource each : dataSourceMap.values()) {
            Preconditions.checkArgument(!(each instanceof MasterSlaveDataSource), "Initialized data sources can not be master-slave data sources.");
        }
    }

    /**
     * 數據庫鏈接
     */
    @Override
    public final ShardingConnection getConnection() {
        return new ShardingConnection(getDataSourceMap(), runtimeContext, TransactionTypeHolder.get());
    }
}

AbstractDataSourceAdapter 抽象類內部主要獲取不一樣類型的數據源對應的數據庫鏈接對象,實現 AutoCloseable 接口是爲在使用完資源後能夠自動將這些資源關閉(調用 close方法),那再看看繼承類 AbstractUnsupportedOperationDataSource

@Getter
public abstract class AbstractDataSourceAdapter extends AbstractUnsupportedOperationDataSource implements AutoCloseable {

    private final Map<String, DataSource> dataSourceMap;

    private final DatabaseType databaseType;

    public AbstractDataSourceAdapter(final Map<String, DataSource> dataSourceMap) throws SQLException {
        this.dataSourceMap = dataSourceMap;
        databaseType = createDatabaseType();
    }

    public AbstractDataSourceAdapter(final DataSource dataSource) throws SQLException {
        dataSourceMap = new HashMap<>(1, 1);
        dataSourceMap.put("unique", dataSource);
        databaseType = createDatabaseType();
    }

    private DatabaseType createDatabaseType() throws SQLException {
        DatabaseType result = null;
        for (DataSource each : dataSourceMap.values()) {
            DatabaseType databaseType = createDatabaseType(each);
            Preconditions.checkState(null == result || result == databaseType, String.format("Database type inconsistent with '%s' and '%s'", result, databaseType));
            result = databaseType;
        }
        return result;
    }

    /**
     * 不一樣數據源類型獲取數據庫鏈接
     */
    private DatabaseType createDatabaseType(final DataSource dataSource) throws SQLException {
        if (dataSource instanceof AbstractDataSourceAdapter) {
            return ((AbstractDataSourceAdapter) dataSource).databaseType;
        }
        try (Connection connection = dataSource.getConnection()) {
            return DatabaseTypes.getDatabaseTypeByURL(connection.getMetaData().getURL());
        }
    }

    @Override
    public final Connection getConnection(final String username, final String password) throws SQLException {
        return getConnection();
    }

    @Override
    public final void close() throws Exception {
        close(dataSourceMap.keySet());
    }
}

AbstractUnsupportedOperationDataSource 實現DataSource 接口並繼承了 WrapperAdapter 類,它內部並無什麼具體方法只起到橋接的做用,但看着是否是和咱們前邊講適配器模式的例子方式有點類似。

public abstract class AbstractUnsupportedOperationDataSource extends WrapperAdapter implements DataSource {

    @Override
    public final int getLoginTimeout() throws SQLException {
        throw new SQLFeatureNotSupportedException("unsupported getLoginTimeout()");
    }

    @Override
    public final void setLoginTimeout(final int seconds) throws SQLException {
        throw new SQLFeatureNotSupportedException("unsupported setLoginTimeout(int seconds)");
    }
}

WrapperAdapter 是一個包裝器的適配類,實現了 JDBC 中的 Wrapper 接口,其中有兩個核心方法 recordMethodInvocation 用於添加須要執行的方法和參數,而 replayMethodsInvocation 則將添加的這些方法和參數經過反射執行。仔細看不難發現兩個方法中都用到了 JdbcMethodInvocation類。

public abstract class WrapperAdapter implements Wrapper {

    private final Collection<JdbcMethodInvocation> jdbcMethodInvocations = new ArrayList<>();

    /**
     * 添加要執行的方法
     */
    @SneakyThrows
    public final void recordMethodInvocation(final Class<?> targetClass, final String methodName, final Class<?>[] argumentTypes, final Object[] arguments) {
        jdbcMethodInvocations.add(new JdbcMethodInvocation(targetClass.getMethod(methodName, argumentTypes), arguments));
    }

    /**
     * 經過反射執行 上邊添加的方法
     */
    public final void replayMethodsInvocation(final Object target) {
        for (JdbcMethodInvocation each : jdbcMethodInvocations) {
            each.invoke(target);
        }
    }
}

JdbcMethodInvocation 類主要應用反射經過傳入的 method 方法和 arguments 參數執行對應的方法,這樣就能夠經過 JDBC API 調用非 JDBC 方法了。

@RequiredArgsConstructor
public class JdbcMethodInvocation {

    @Getter
    private final Method method;

    @Getter
    private final Object[] arguments;

    /**
     * Invoke JDBC method.
     * 
     * @param target target object
     */
    @SneakyThrows
    public void invoke(final Object target) {
        method.invoke(target, arguments);
    }
}

Sharding-JDBC 拓展 JDBC API 接口後,在新增的分片功能裏又作了哪些事情呢?

一張表通過分庫分表後被拆分紅多個子表,並分散到不一樣的數據庫中,在不修改原業務 SQL 的前提下,Sharding-JDBC 就必須對 SQL進行一些改造才能正常執行。

大體的執行流程:SQL 解析 -> 執⾏器優化 -> SQL 路由 -> SQL 改寫 -> SQL 執⾏ -> 結果歸併 六步組成,一塊兒瞅瞅每一個步驟作了點什麼。

在這裏插入圖片描述

SQL 解析

SQL解析過程分爲詞法解析和語法解析兩步,好比下邊這條查詢用戶訂單的SQL,先用詞法解析將SQL拆解成不可再分的原子單元。在根據不一樣數據庫方言所提供的字典,將這些單元歸類爲關鍵字,表達式,變量或者操做符等類型。

SELECT order_no,price FROM t_order_ where user_id = 10086 and order_status > 0

接着語法解析會將拆分後的SQL轉換爲抽象語法樹,經過對抽象語法樹遍歷,提煉出分片所需的上下文,上下文包含查詢字段信息(Field)、表信息(Table)、查詢條件(Condition)、排序信息(Order By)、分組信息(Group By)以及分頁信息(Limit)等,並標記出 SQL中有可能須要改寫的位置。

抽象語法樹

執⾏器優化

執⾏器優化對SQL分片條件進行優化,處理像關鍵字 OR這種影響性能的壞味道。

SQL 路由

SQL 路由經過解析分片上下文,匹配到用戶配置的分片策略,並生成路由路徑。簡單點理解就是能夠根據咱們配置的分片策略計算出 SQL該在哪一個庫的哪一個表中執行,而SQL路由又根據有無分片健區分出 分片路由廣播路由

官方路由圖譜

有分⽚鍵的路由叫分片路由,細分爲直接路由、標準路由和笛卡爾積路由這3種類型。

標準路由

標準路由是最推薦也是最爲常⽤的分⽚⽅式,它的適⽤範圍是不包含關聯查詢或僅包含綁定表之間關聯查詢的SQL。

當 SQL分片健的運算符爲 = 時,路由結果將落⼊單庫(表),當分⽚運算符是BETWEENIN 等範圍時,路由結果則不⼀定落⼊惟⼀的庫(表),所以⼀條邏輯SQL最終可能被拆分爲多條⽤於執⾏的真實SQL。

SELECT * FROM t_order  where t_order_id in (1,2)

SQL路由處理後

SELECT * FROM t_order_0  where t_order_id in (1,2)
SELECT * FROM t_order_1  where t_order_id in (1,2)

直接路由

直接路由是經過使用 HintAPI 直接將 SQL路由到指定⾄庫表的一種分⽚方式,並且直接路由能夠⽤於分⽚鍵不在SQL中的場景,還能夠執⾏包括⼦查詢、⾃定義函數等複雜狀況的任意SQL。

好比根據 t_order_id 字段爲條件查詢訂單,此時但願在不修改SQL的前提下,加上 user_id做爲分片條件就可使用直接路由。

笛卡爾積路由

笛卡爾路由是由⾮綁定表之間的關聯查詢產生的,查詢性能較低儘可能避免走此路由模式。


無分⽚鍵的路由又叫作廣播路由,能夠劃分爲全庫表路由、全庫路由、 全實例路由、單播路由和阻斷路由這 5種類型。

全庫表路由

全庫表路由針對的是數據庫 DQLDML,以及 DDL等操做,當咱們執行一條邏輯表 t_order SQL時,在全部分片庫中對應的真實表 t_order_0 ··· t_order_n 內逐一執行。

全庫路由

全庫路由主要是對數據庫層面的操做,好比數據庫 SET 類型的數據庫管理命令,以及 TCL 這樣的事務控制語句。

對邏輯庫設置 autocommit 屬性後,全部對應的真實庫中都執行該命令。

SET autocommit=0;

全實例路由

全實例路由是針對數據庫實例的 DCL 操做(設置或更改數據庫用戶或角色權限),好比:建立一個用戶 order ,這個命令將在全部的真實庫實例中執行,以此確保 order 用戶能夠正常訪問每個數據庫實例。

CREATE USER order@127.0.0.1 identified BY '程序員內點事';

單播路由

單播路由用來獲取某一真實表信息,好比得到表的描述信息:

DESCRIBE t_order;

t_order 的真實表是 t_order_0 ···· t_order_n,他們的描述結構相徹底同,咱們只需在任意的真實表執行一次就能夠。

阻斷路由

⽤來屏蔽SQL對數據庫的操做,例如:

USE order_db;

這個命令不會在真實數據庫中執⾏,由於 ShardingSphere 採⽤的是邏輯 Schema(數據庫的組織和結構) ⽅式,因此無需將切換數據庫的命令發送⾄真實數據庫中。

SQL 改寫

將基於邏輯表開發的SQL改寫成能夠在真實數據庫中能夠正確執行的語句。好比查詢 t_order 訂單表,咱們實際開發中 SQL是按邏輯表 t_order 寫的。

SELECT * FROM t_order

但分庫分表之後真實數據庫中 t_order 表就不存在了,而是被拆分紅多個子表 t_order_n 分散在不一樣的數據庫內,還按原SQL執行顯然是行不通的,這時須要將分表配置中的邏輯表名稱改寫爲路由以後所獲取的真實表名稱。

SELECT * FROM t_order_n

SQL執⾏

將路由和改寫後的真實 SQL 安全且高效發送到底層數據源執行。但這個過程並非簡單的將 SQL 經過JDBC 直接發送至數據源執行,而是平衡數據源鏈接建立以及內存佔用所產生的消耗,它會自動化的平衡資源控制與執行效率。

結果歸併

將從各個數據節點獲取的多數據結果集,合併成一個大的結果集並正確的返回至請求客戶端,稱爲結果歸併。而咱們SQL中的排序、分組、分頁和聚合等語法,均是在歸併後的結果集上進行操做的。

4、快速實踐

下面咱們結合 Springboot + mybatisplus 快速搭建一個分庫分表案例。

一、準備工做

先作準備工做,建立兩個數據庫 ds-0ds-1,兩個庫中分別建表 t_order_0t_order_1t_order_2t_order_item_0t_order_item_1t_order_item_2t_config,方便後邊驗證廣播表、綁定表的場景。

建立分片庫表
表結構以下:

t_order_0 訂單表

CREATE TABLE `t_order_0` (
  `order_id` bigint(200) NOT NULL,
  `order_no` varchar(100) DEFAULT NULL,
  `create_name` varchar(50) DEFAULT NULL,
  `price` decimal(10,2) DEFAULT NULL,
  PRIMARY KEY (`order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

t_order_0t_order_item_0 互爲關聯表

CREATE TABLE `t_order_item_0` (
  `item_id` bigint(100) NOT NULL,
  `order_no` varchar(200) NOT NULL,
  `item_name` varchar(50) DEFAULT NULL,
  `price` decimal(10,2) DEFAULT NULL,
  PRIMARY KEY (`item_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

廣播表 t_config

`id` bigint(30) NOT NULL,
  `remark` varchar(50) CHARACTER SET utf8 DEFAULT NULL,
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `last_modify_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

ShardingSphere 提供了4種分片配置方式:

  • Java 代碼配置

  • Yaml 、properties 配置

  • Spring 命名空間配置

  • Spring Boot配置

爲讓代碼看上去更簡潔和直觀,後邊統一使用 properties 配置的方式,引入 shardingsphere 對應的 sharding-jdbc-spring-boot-startersharding-core-common 包,版本統一用的 4.0.0-RC1。

二、分片配置

<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
    <version>4.0.0-RC1</version>
</dependency>

<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>sharding-core-common</artifactId>
    <version>4.0.0-RC1</version>
</dependency>

準備工做作完( mybatis 搭建就不贅述了),接下來咱們逐一解讀分片配置信息。

咱們首先定義兩個數據源 ds-0ds-1,並分別加上數據源的基礎信息。

# 定義兩個全局數據源
spring.shardingsphere.datasource.names=ds-0,ds-1

# 配置數據源 ds-0
spring.shardingsphere.datasource.ds-0.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.ds-0.driverClassName=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds-0.url=jdbc:mysql://127.0.0.1:3306/ds-0?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT
spring.shardingsphere.datasource.ds-0.username=root
spring.shardingsphere.datasource.ds-0.password=root

# 配置數據源 ds-1
spring.shardingsphere.datasource.ds-1.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.ds-1.driverClassName=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds-1.url=jdbc:mysql://127.0.0.1:3306/ds-1?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT
spring.shardingsphere.datasource.ds-1.username=root
spring.shardingsphere.datasource.ds-1.password=root

配置完數據源接下來爲表添加分庫和分表策略,使用 sharding-jdbc 作分庫分表須要咱們爲每個表單獨設置分片規則。

# 配置分片表 t_order
# 指定真實數據節點
spring.shardingsphere.sharding.tables.t_order.actual-data-nodes=ds-$->{0..1}.t_order_$->{0..2}

actual-data-nodes 屬性指定分片的真實數據節點,$是一個佔位符,{0..1}表示實際拆分的數據庫表數量。

ds-$-&gt;{0..1}.t_order_$-&gt;{0..2} 表達式至關於 6個數據節點

  • ds-0.t_order_0
  • ds-0.t_order_1
  • ds-0.t_order_2
  • ds-1.t_order_0
  • ds-1.t_order_1
  • ds-1.t_order_2
### 分庫策略
# 分庫分片健
spring.shardingsphere.sharding.tables.t_order.database-strategy.inline.sharding-column=order_id
# 分庫分片算法
spring.shardingsphere.sharding.tables.t_order.database-strategy.inline.algorithm-expression=ds-$->{order_id % 2}

爲表設置分庫策略,上邊講了 sharding-jdbc 它提供了四種分片策略,爲快速搭建咱們先以最簡單的行內表達式分片策略來實現,在下一篇會介紹四種分片策略的詳細用法和使用場景。

database-strategy.inline.sharding-column 屬性中 database-strategy 爲分庫策略,inline 爲具體的分片策略,sharding-column 表明分片健。

database-strategy.inline.algorithm-expression 是當前策略下具體的分片算法,ds-$-&gt;{order_id % 2} 表達式意思是 對 order_id字段進行取模分庫,2 表明分片庫的個數,不一樣的策略對應不一樣的算法,這裏也能夠是咱們自定義的分片算法類。

# 分表策略
# 分表分片健
spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.sharding-column=order_id
# 分表算法
spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.algorithm-expression=t_order_$->{order_id % 3}
# 自增主鍵字段
spring.shardingsphere.sharding.tables.t_order.key-generator.column=order_id
# 自增主鍵ID 生成方案
spring.shardingsphere.sharding.tables.t_order.key-generator.type=SNOWFLAKE

分表策略 和 分庫策略 的配置比較類似,不一樣的是分表能夠經過 key-generator.columnkey-generator.type 設置自增主鍵以及指定自增主鍵的生成方案,目前內置了SNOWFLAKEUUID 兩種方式,還能自定義的主鍵生成算法類,後續會詳細的講解。

# 綁定表關係
spring.shardingsphere.sharding.binding-tables= t_order,t_order_item

必須按相同分片健進行分片的表才能互爲成綁定表,在聯合查詢時就能避免出現笛卡爾積查詢。

# 配置廣播表
spring.shardingsphere.sharding.broadcast-tables=t_config

廣播表,開啓 SQL解析日誌,能清晰的看到 SQL分片解析的過程

# 是否開啓 SQL解析日誌
spring.shardingsphere.props.sql.show=true

三、驗證分片

分片配置完之後咱們無需在修改業務代碼了,直接執行業務邏輯的增、刪、改、查便可,接下來驗證一下分片的效果。

咱們同時向 t_ordert_order_item 表插入 5條訂單記錄,並不給定主鍵 order_iditem_id 字段值。

public String insertOrder() {

   for (int i = 0; i < 4; i++) {
       TOrder order = new TOrder();
       order.setOrderNo("A000" + i);
       order.setCreateName("訂單 " + i);
       order.setPrice(new BigDecimal("" + i));
       orderRepository.insert(order);

       TOrderItem orderItem = new TOrderItem();
       orderItem.setOrderId(order.getOrderId());
       orderItem.setOrderNo("A000" + i);
       orderItem.setItemName("服務項目" + i);
       orderItem.setPrice(new BigDecimal("" + i));
       orderItemRepository.insert(orderItem);
   }
   return "success";
}

看到訂單記錄被成功分散到了不一樣的庫表中, order_id 字段也自動生成了主鍵ID,基礎的分片功能就完成了。

基礎分片

那向廣播表 t_config 中插入一條數據會是什麼效果呢?

public String config() {

    TConfig tConfig = new TConfig();
    tConfig.setRemark("我是廣播表");
    tConfig.setCreateTime(new Date());
    tConfig.setLastModifyTime(new Date());
    configRepository.insert(tConfig);
    return "success";
}

發現全部庫中 t_config 表都執行了這條SQL,廣播表和 MQ廣播訂閱的模式很類似,全部訂閱的客戶端都會收到同一條消息。

廣播表

簡單SQL操做驗證沒問通,接下來在試試複雜一點的聯合查詢,前邊咱們已經把 t_ordert_order_item 表設爲綁定表,直接聯表查詢執行一下。

關聯查詢

經過控制檯日誌發現,邏輯表SQL 通過解析之後,只對 t_order_0t_order_item_0 表進行了關聯產生一條SQL。

綁定表SQL

那若是不互爲綁定表又會是什麼狀況呢?去掉 spring.shardingsphere.sharding.binding-tables試一下。

發現控制檯解析出了 3條真實表SQL,而去掉 order_id 做爲查詢條件再次執行後,結果解析出了 9條SQL,進行了笛卡爾積查詢。因此相比之下綁定表的優勢就不言而喻了。

笛卡爾積查詢

5、總結

以上對分庫分表中間件 sharding-jdbc 的基礎概念作了簡單梳理,快速的搭建了一個分庫分表案例,但這只是實踐分庫分表的第一步,下一篇咱們會詳細的介紹四種分片策略的具體用法和使用場景(必知必會),後邊將陸續講解自定義分佈式主鍵、分佈式數據庫事務、分佈式服務治理,數據脫敏等。

案例 GitHub 地址:https://github.com/chengxy-nds/Springboot-Notebook/tree/master/springboot-sharding-jdbc

相關文章
相關標籤/搜索