分庫分表神器 Sharding-JDBC,幾千萬的資料你不搞一下?

java架構師小芷發表於2020-11-19

今天我們來介紹一下 Sharding-JDBC框架和快速地搭建一個分庫分表案例,為講解後續功能點準備好環境。

一、Sharding-JDBC 簡介

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

隨著版本的不斷更迭 ShardingSphere 的核心功能也變得多元化起來。從最開始 Sharding-JDBC 1.0 版本只有資料分片,到 Sharding-JDBC 2.0 版本開始支援資料庫治理(註冊中心、配置中心等等),再到 Sharding-JDBC 3.0版本又加分散式事務 (支援 Atomikos、Narayana、Bitronix、Seata),如今已經迭代到了 Sharding-JDBC 4.0 版本。

 

現在的 ShardingSphere 不單單是指某個框架而是一個生態圈,這個生態圈 Sharding-JDBC、Sharding-Proxy 和 Sharding-Sidecar 這三款開源的分散式資料庫中介軟體解決方案所構成。

ShardingSphere 的前身就是 Sharding-JDBC,所以它是整個框架中最為經典、成熟的元件,我們先從 Sharding-JDBC 框架入手學習分庫分表。

二、核心概念

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

分片

一般我們在提到分庫分表的時候,大多是以水平切分模式(水平分庫、分表)為基礎來說的,資料分片將原本一張資料量較大的表 t_order 拆分生成數個表結構完全一致的小資料量表 t_order_0、t_order_1、···、t_order_n,每張表只儲存原大表中的一部分資料,當執行一條SQL時會通過 分庫策略、分片策略 將資料分散到不同的資料庫、表內。

 

資料節點

資料節點是分庫分表中一個不可再分的最小資料單元(表),它由資料來源名稱和資料表組成,例如上圖中 order_db_1.t_order_0、order_db_2.t_order_1 就表示一個資料節點。

邏輯表

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

真實表

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

分片鍵

用於分片的資料庫欄位。我們將 t_order 表分片以後,當執行一條SQL時,通過對欄位 order_id 取模的方式來決定,這條資料該在哪個資料庫中的哪個表中執行,此時 order_id 欄位就是 t_order 表的分片健。

 

這樣以來同一個訂單的相關資料就會存在同一個資料庫表中,大幅提升資料檢索的效能,不僅如此 sharding-jdbc 還支援根據多個欄位作為分片健進行分片。

分片演算法

上邊我們提到可以用分片健取模的規則分片,但這只是比較簡單的一種,在實際開發中我們還希望用 >=、<=、>、<、BETWEEN 和 IN 等條件作為分片規則,自定義分片邏輯,這時就需要用到分片策略與分片演算法。

從執行 SQL 的角度來看,分庫分表可以看作是一種路由機制,把 SQL 語句路由到我們期望的資料庫或資料表中並獲取資料,分片演算法可以理解成一種路由規則。

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

分庫、分表的分片策略配置是相對獨立的,可以各自使用不同的策略與演算法,每種策略中可以是多個分片演算法的組合,每個分片演算法可以對多個分片健做邏輯判斷。

 

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

 

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

1、精確分片演算法

精確分片演算法(PreciseShardingAlgorithm)用於單個欄位作為分片鍵,SQL中有 = 與 IN 等條件的分片,需要在標準分片策略(StandardShardingStrategy )下使用。

2、範圍分片演算法

範圍分片演算法(RangeShardingAlgorithm)用於單個欄位作為分片鍵,SQL中有 BETWEEN AND、>、<、>=、<= 等條件的分片,需要在標準分片策略(StandardShardingStrategy )下使用。

3、複合分片演算法

複合分片演算法(ComplexKeysShardingAlgorithm)用於多個欄位作為分片鍵的分片操作,同時獲取到多個分片健的值,根據多個欄位處理業務邏輯。需要在複合分片策略(ComplexShardingStrategy )下使用。

4、Hint分片演算法

Hint分片演算法(HintShardingAlgorithm)稍有不同,上邊的演算法中我們都是解析SQL 語句提取分片鍵,並設定分片策略進行分片。但有些時候我們並沒有使用任何的分片鍵和分片策略,可還想將 SQL 路由到目標資料庫和表,就需要通過手動干預指定SQL的目標資料庫和表資訊,這也叫強制路由。

分片策略

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

1、標準分片策略

標準分片策略適用於單分片鍵,此策略支援 PreciseShardingAlgorithm 和 RangeShardingAlgorithm 兩個分片演算法。

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

2、複合分片策略

複合分片策略,同樣支援對 SQL語句中的 =,>, <, >=, <=,IN和 BETWEEN AND 的分片操作。不同的是它支援多分片鍵,具體分配片細節完全由應用開發者實現。

3、行表示式分片策略

行表示式分片策略,支援對 SQL語句中的 = 和 IN 的分片操作,但只支援單分片鍵。這種策略通常用於簡單的分片,不需要自定義分片演算法,可以直接在配置檔案中接著寫規則。

t_order_$->{t_order_id % 4} 代表 t_order 對其欄位 t_order_id取模,拆分成4張表,而表名分別是t_order_0 到 t_order_3。

4、Hint分片策略

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

分散式主鍵

資料分⽚後,不同資料節點⽣成全域性唯⼀主鍵是⾮常棘⼿的問題,同⼀個邏輯表(t_order)內的不同真實表(t_order_n)之間的⾃增鍵由於⽆法互相感知而產⽣重複主鍵。

儘管可通過設定⾃增主鍵 初始值 和 步⻓ 的⽅式避免ID碰撞,但這樣會使維護成本加大,乏完整性和可擴充套件性。如果後去需要增加分片表的數量,要逐一修改分片表的步長,運維成本非常高,所以不建議這種方式。

實現分散式主鍵⽣成器的方式很多,具體可以百度,網上有很多

為了讓上手更加簡單,ApacheShardingSphere 內建了UUID、SNOWFLAKE 兩種分散式主鍵⽣成器,預設使⽤雪花演算法(snowflake)⽣成64bit的⻓整型資料。不僅如此它還抽離出分散式主鍵⽣成器的介面,⽅便我們實現⾃定義的⾃增主鍵⽣成演算法。

廣播表

廣播表:存在於所有的分片資料來源中的表,表結構和表中的資料在每個資料庫中均完全一致。一般是為字典表或者配置表 t_config,某個表一旦被配置為廣播表,只要修改某個資料庫的廣播表,所有資料來源中廣播表的資料都會跟著同步。

繫結表

繫結表:那些分片規則一致的主表和子表。比如:t_order 訂單表和 t_order_item 訂單服務專案表,都是按 order_id 欄位分片,因此兩張表互為繫結表關係。

那繫結表存在的意義是啥呢?

通常在我們的業務中都會使用 t_order 和 t_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_0 和 t_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 的條件,所以要保證繫結表之間的分片鍵要完全相同。

三、和JDBC的貓膩

從名字上不難看出,Sharding-JDBC 和 JDBC有很大關係,我們知道 JDBC 是一種 Java 語言訪問關係型資料庫的規範,其設計初衷就是要提供一套用於各種資料庫的統一標準,不同廠家共同遵守這套標準,並提供各自的實現方案供應用程式呼叫。

 

但其實對於開發人員而言,我們只關心如何呼叫 JDBC API 來訪問資料庫,只要正確使用 DataSource、Connection、Statement 、ResultSet 等 API 介面,直接運算元據庫即可。所以如果想在 JDBC 層面實現資料分片就必須對現有的 API 進行功能擴充,而 Sharding-JDBC 正是基於這種思想,重寫了 JDBC 規範並完全相容了 JDBC 規範。

 

對原有的 DataSource、Connection 等介面擴充套件成 ShardingDataSource、ShardingConnection,而對外暴露的分片操作介面與 JDBC 規範中所提供的介面完全一致,只要你熟悉 JDBC 就可以輕鬆應用 Sharding-JDBC 來實現分庫分表。

 

因此它適用於任何基於 JDBC 的 ORM 框架,如:JPA, Hibernate,Mybatis,Spring JDBC Template 或直接使用的 JDBC。完美相容任何第三方的資料庫連線池,如:DBCP, C3P0, BoneCP,Druid, HikariCP 等,幾乎對主流關係型資料庫都支援。

那 Sharding-JDBC 又是如何擴充這些介面的呢?想知道答案我們就的從原始碼入手了,下邊我們以 JDBC API 中的 DataSource 為例看看它是如何被重寫擴充套件的。

資料來源 DataSource 介面的核心作用就是獲取資料庫連線物件 Connection,我們看其內部提供了兩個獲取資料庫連線的方法 ,並且繼承了 CommonDataSource 和 Wrapper 兩個介面。

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 類它在原 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分片健的運算子為 = 時,路由結果將落⼊單庫(表),當分⽚運算子是BETWEEN 或IN 等範圍時,路由結果則不⼀定落⼊唯⼀的庫(表),因此⼀條邏輯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種型別。

全庫表路由

全庫表路由針對的是資料庫 DQL和 DML,以及 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中的排序、分組、分頁和聚合等語法,均是在歸併後的結果集上進行操作的。

四、快速實踐

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

1、準備工作

先做準備工作,建立兩個資料庫 ds-0、ds-1,兩個庫中分別建表 t_order_0、t_order_1、t_order_2 、t_order_item_0、t_order_item_1、t_order_item_2,t_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_0 與 t_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-starter 和 sharding-core-common 包,版本統一用的 4.0.0-RC1。

2、分片配置

<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-0、ds-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-$->{0..1}.t_order_$->{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-$->{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.column 和 key-generator.type 設定自增主鍵以及指定自增主鍵的生成方案,目前內建了SNOWFLAKE 和 UUID 兩種方式,還能自定義的主鍵生成演算法類,後續會詳細地講解。

# 繫結表關係
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
複製程式碼

3、驗證分片

分片配置完以後我們無需再修改業務程式碼了,直接執行業務邏輯的增、刪、改、查即可,接下來驗證一下分片的效果。

我們同時向 t_order、t_order_item 表插入 5條訂單記錄,並不給定主鍵 order_id ,item_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_order 、t_order_item 表設為繫結表,直接聯表查詢執行一下。

 

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

 

那如果不互為繫結表又會是什麼情況呢?去掉 spring.shardingsphere.sharding.binding-tables試一下。

發現控制檯解析出了 3條真實表SQL,而去掉 order_id 作為查詢條件再次執行後,結果解析出了 9條SQL,進行了笛卡爾積查詢。所以相比之下繫結表的優點就不言而喻了。

 

五、總結

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

相關文章