Apache ShardingSphere HINT 實用指南

SphereEx發表於2022-03-25

陳出新,SphereEx 中介軟體研發工程師,Apache ShardingSphere Committer,目前專注於 Apache ShardingSphere 核心模組的研發工作。

背景

Apache ShardingSphere 基於使用者的實際使用場景,為使用者打造了多種實用功能,包括資料分片、讀寫分離等。在資料分片功能中,Apache ShardingSphere 提供了標準分片、複合分片等多種實用的分片策略,在各種分片策略中,使用者又可以配置相關分片演算法,從而解決資料分片的問題。在讀寫分離功能中,ShardingSphere 為使用者提供了靜態和動態的兩種讀寫分離型別以及豐富的負載均衡演算法以滿足使用者實際需求。

可以看到 ShardingSphere 的分片和讀寫分離功能已經非常豐富,不過使用者的真實使用場景是千變萬化的。以多租戶場景為例,使用者期望按照登入賬號所屬租戶進行分片,但是租戶資訊卻並不是存在於每條業務 SQL 中,這時從 SQL 中提取分片欄位的演算法將無法發揮作用。再以讀寫分離為例,大部分場景下使用者都希望能夠將查詢操作路由到從庫上執行,但是在某些實時性要求很高的場景下,使用者希望將 SQL 強制路由到主庫執行,這時讀寫分離就無法滿足業務要求。

基於以上痛點 Apache ShardingSphere 為使用者提供了 Hint 功能,使用者可以結合實際業務場景,利用 SQL 外部的邏輯進行強制路由或者分片。目前 ShardingSphere 為使用者提供了兩種 Hint 方式,一種透過 Java API 手動程式設計,利用 HintManager 進行強制路由和分片,這種方式對採用 JDBC 程式設計的應用非常友好,只需要少量的程式碼編寫,就能夠輕鬆實現不依賴 SQL 的分片或者強制路由功能。另外一種方式對於不懂開發的 DBA 而言更加友好,ShardingSphere 基於分散式 SQL 提供的使用方式,利用 SQL HINT 和 DistSQL HINT,為使用者提供了無需編碼就能實現的分片和強制路由功能。接下來,讓我們一起了解下這兩種使用方式。

基於 HintManager 的手動程式設計

ShardingSphere 主要透過 HintManager 物件來實現強制路由和分片的功能。利用 HintManager,使用者的分片將不用再依賴 SQL。它可以極大地擴充套件使用者的使用場景,讓使用者可以更加靈活地進行資料分片或者強制路由。目前透過 HintManager,使用者可以配合 ShardingSphere 內建的或者自定義的 Hint 演算法實現分片功能,還可以透過設定指定資料來源或者強制主庫讀寫,實現強制路由功能。在學習 HintManager 的使用之前,讓我們先來簡單地瞭解一下它的實現原理,這有助於我們更好地使用它。

HintManager 實現原理

其實透過檢視 HintManager 程式碼,我們可以快速地瞭解它的原理。

@NoArgsConstructor(access = AccessLevel.PRIVATE)public final class HintManager implements AutoCloseable {
    private static final ThreadLocal<HintManager> HINT_MANAGER_HOLDER = new ThreadLocal<>();
}

正如你所看到的,ShardingSphere 透過 ThreadLocal 來實現 HintManager 的功能,只要在同一個執行緒中,使用者的分片設定都會得以保留。因此,只要使用者在執行 SQL 之前呼叫 HintManager 相關功能,ShardingSphere 就能在當前執行緒中獲取使用者設定的分片或強制路由條件,從而進行分片或者路由操作。瞭解了 HintManager 的原理之後,讓我們一起來學習一下它的使用。

HintManager 的使用

使用 Hint 分片

Hint 分片演算法需要使用者實現   org.apache.shardingsphere.sharding.api.sharding.hint.HintShardingAlgorithm介面。Apache ShardingSphere 在進行路由時,將會從 HintManager 中獲取分片值進行路由操作。

參考配置如下:

rules:- !SHARDING  tables:    t_order:      actualDataNodes: demo_ds_${0..1}.t_order_${0..1}      databaseStrategy:        hint:          algorithmClassName: xxx.xxx.xxx.HintXXXAlgorithm      tableStrategy:        hint:          algorithmClassName: xxx.xxx.xxx.HintXXXAlgorithm  defaultTableStrategy:    none:  defaultKeyGenerateStrategy:    type: SNOWFLAKE    column: order_idprops:    sql-show: true

獲取 HintManager 例項

HintManager hintManager = HintManager.getInstance();

新增分片鍵

  • 使用   hintManager.addDatabaseShardingValue來新增資料來源分片鍵值。

  • 使用   hintManager.addTableShardingValue  來新增表分片鍵值。

注:分庫不分表情況下,強制路由至某一個分庫時,可使用 hintManager.setDatabaseShardingValue 方式新增分片。

清除分片鍵值

分片鍵值儲存在 ThreadLocal 中,所以需要在操作結束時呼叫   hintManager.close()  來清除 ThreadLocal 中的內容。

完整程式碼示例

String sql = "SELECT * FROM t_order";try (HintManager hintManager = HintManager.getInstance();
     Connection conn = dataSource.getConnection();
     PreparedStatement preparedStatement = conn.prepareStatement(sql)) {
    hintManager.addDatabaseShardingValue("t_order", 1);
    hintManager.addTableShardingValue("t_order", 2);
    try (ResultSet rs = preparedStatement.executeQuery()) {
        while (rs.next()) {
            // ...        }
    }
}String sql = "SELECT * FROM t_order";try (HintManager hintManager = HintManager.getInstance();
     Connection conn = dataSource.getConnection();
     PreparedStatement preparedStatement = conn.prepareStatement(sql)) {
    hintManager.setDatabaseShardingValue(3);
    try (ResultSet rs = preparedStatement.executeQuery()) {
        while (rs.next()) {
            // ...        }
    }
}

使用 Hint 強制主庫路由

獲取 HintManager

與基於 Hint 的資料分片相同。

設定主庫路由

使用   hintManager.setWriteRouteOnly  設定主庫路由。

清除分片鍵值

與基於 Hint 的資料分片相同。

完整程式碼示例

String sql = "SELECT * FROM t_order";try (HintManager hintManager = HintManager.getInstance();
     Connection conn = dataSource.getConnection();
     PreparedStatement preparedStatement = conn.prepareStatement(sql)) {
    hintManager.setWriteRouteOnly();
    try (ResultSet rs = preparedStatement.executeQuery()) {
        while (rs.next()) {
            // ...        }
    }
}

使用 Hint 路由至指定資料庫

獲取 HintManager

與基於 Hint 的資料分片相同。

設定路由至指定資料庫

使用   hintManager.setWriteRouteOnly  設定資料庫名稱。

完整程式碼示例

String sql = "SELECT * FROM t_order";try (HintManager hintManager = HintManager.getInstance();
     Connection conn = dataSource.getConnection();
     PreparedStatement preparedStatement = conn.prepareStatement(sql)) {
    hintManager.setDataSourceName("ds_0");
    try (ResultSet rs = preparedStatement.executeQuery()) {
        while (rs.next()) {
            // ...        }
    }
}

清除強制路由值

與基於 Hint 的資料分片相同。

在瞭解了基於 HintManager 的手動程式設計方式之後,讓我們一起來了解 ShardingSphere 基於分散式 SQL 提供的另一種 Hint 的解決方案。

基於分散式 SQL 的 Hint

Apache ShardingSphere 的分散式 SQL HINT 主要由兩種功能組成,一種叫做 SQL HINT,即基於 SQL 註釋的方式提供的功能,另外一種是透過 DistSQL 實現的作用於 HintManager 的功能。

SQL HINT

SQL HINT 就是透過在 SQL 語句上增加註釋,從而實現強制路由的一種 Hint 方式。它降低了使用者改造程式碼的成本,同時完全脫離了 Java API 的限制,不僅可以在 ShardingSphere-JDBC 中使用,也可以直接在 ShardingSphere-Proxy 上使用。

以下面 SQL 為例,即使使用者配置了針對 t order 的相關分片演算法,該 SQL 也會直接在資料庫 ds0 上原封不動地執行,並返回執行結果。

/* ShardingSphere hint: dataSourceName=ds_0 */SELECT * FROM t_order;

透過註釋的方式我們可以方便地將 SQL 直接送達指定資料庫執行而無視其它分片邏輯。以多租戶場景為例,使用者不用再配置複雜的分庫邏輯,也無需改造業務邏輯,只需要將指定庫新增到註釋資訊中即可。在瞭解了 SQL HINT 的基本使用之後,讓我們一起來了解一下 SQL HINT 的實現原理。

SQL HINT 的實現原理

其實瞭解 Apache ShardingSphere 的讀者朋友們一定對 SQL 解析引擎不會感到陌生。SQL HINT 實現的第一步就是提取 SQL 中的註釋資訊。利用 antlr4 的通道功能,可以將 SQL 中的註釋資訊單獨送至特定的隱藏通道,ShardingSphere 也正是利用該功能,在生成解析結果的同時,將隱藏通道中的註釋資訊一併提取出來了。具體實現如下方程式碼所示。

  • 將 SQL 中的註釋送入隱藏通道:
lexer grammar Comments;import Symbol;
BLOCK_COMMENT:  '/*' .*? '*/' -> channel(HIDDEN);
INLINE_COMMENT: (('-- ' | '#') ~[\r\n]* ('\r'? '\n' | EOF) | '--' ('\r'? '\n' | EOF)) -> channel(HIDDEN);
  • 訪問語法樹後增加對於註釋資訊的提取:
public <T> T visit(final ParseContext parseContext) {
    ParseTreeVisitor<T> visitor = SQLVisitorFactory.newInstance(databaseType, visitorType, SQLVisitorRule.valueOf(parseContext.getParseTree().getClass()), props);
    T result = parseContext.getParseTree().accept(visitor);
    appendSQLComments(parseContext, result);
    return result;
}private <T> void appendSQLComments(final ParseContext parseContext, final T visitResult) {
    if (!parseContext.getHiddenTokens().isEmpty() && visitResult instanceof AbstractSQLStatement) {
        Collection<CommentSegment> commentSegments = parseContext.getHiddenTokens().stream().map(each -> new CommentSegment(each.getText(), each.getStartIndex(), each.getStopIndex()))
                .collect(Collectors.toList());
        ((AbstractSQLStatement) visitResult).getCommentSegments().addAll(commentSegments);
    }
}

提取出使用者 SQL 中的註釋資訊之後,我們就需要根據註釋資訊來進行相關強制路由了。既然是路由,那麼自然就需要使用 Apache ShardingSphere 的路由引擎,我們在路由引擎上做了一些針對 HINT 的改造。

public RouteContext route(final LogicSQL logicSQL, final ShardingSphereMetaData metaData) {
    RouteContext result = new RouteContext();
    Optional<String> dataSourceName = findDataSourceByHint(logicSQL.getSqlStatementContext(), metaData.getResource().getDataSources());
    if (dataSourceName.isPresent()) {
        result.getRouteUnits().add(new RouteUnit(new RouteMapper(dataSourceName.get(), dataSourceName.get()), Collections.emptyList()));
        return result;
    }
    for (Entry<ShardingSphereRule, SQLRouter> entry : routers.entrySet()) {
        if (result.getRouteUnits().isEmpty()) {
            result = entry.getValue().createRouteContext(logicSQL, metaData, entry.getKey(), props);
        } else {
            entry.getValue().decorateRouteContext(result, logicSQL, metaData, entry.getKey(), props);
        }
    }
    if (result.getRouteUnits().isEmpty() && 1 == metaData.getResource().getDataSources().size()) {
        String singleDataSourceName = metaData.getResource().getDataSources().keySet().iterator().next();
        result.getRouteUnits().add(new RouteUnit(new RouteMapper(singleDataSourceName, singleDataSourceName), Collections.emptyList()));
    }
    return result;
}

ShardingSphere 首先發現了符合定義的 SQL 註釋,再經過基本的校驗之後,就會直接返回使用者指定的路由結果,從而實現強制路由功能。在瞭解了 SQL HINT 的基本原理之後,讓我們一起學習如何使用 SQL HINT。

如何使用 SQL HINT

SQL HINT 的使用非常簡單,無論是 ShardingSphere-JDBC 還是 ShardingSphere-Porxy,都可以使用。

第一步開啟註釋解析開關。將   sqlCommentParseEnabled  設定為 true。

第二步在 SQL 上增加註釋即可。目前 SQL HINT 支援指定資料來源路由和主庫路由。

  • 指定資料來源路由:目前只支援路由至一個資料來源。註釋格式暫時只支援   /* */,內容需要以   ShardingSphere hint:  開始,屬性名為   dataSourceName

  • /* ShardingSphere hint: dataSourceName=ds_0 */SELECT * FROM t_order;
  • 主庫路由:註釋格式暫時只支援   /* */,內容需要以   ShardingSphere hint:  開始,屬性名為   writeRouteOnly

/* ShardingSphere hint: writeRouteOnly=true */SELECT * FROM t_order;

DistSQL HINT

Apache ShardingSphere 的 DistSQL 也提供了 Hint 相關功能,讓使用者可以透過 ShardingSphere-Proxy 來實現分片和強制路由功能。

DistSQL HINT 的實現原理

同前文一致,在學習使用 DistSQL HINT 功能之前,讓我們一起來了解一下 DistSQL Hint 的實現原理。DistSQL HINT 的實現原理非常簡單,其實就是透過操作 HintManager 實現的 HINT 功能。以讀寫分離 Hint 為例,當使用者透過 ShardingSphere-Proxy 執行以下 SQL 時,其實 ShardingSphere 內部對 SQL 做了如下方程式碼所示的操作。

-- 強制主庫讀寫set readwrite_splitting hint source = write
@RequiredArgsConstructorpublic final class SetReadwriteSplittingHintExecutor extends AbstractHintUpdateExecutor<SetReadwriteSplittingHintStatement> {
    private final SetReadwriteSplittingHintStatement sqlStatement;
    @Override    public ResponseHeader execute() {
        HintSourceType sourceType = HintSourceType.typeOf(sqlStatement.getSource());
        switch (sourceType) {
            case AUTO:
                HintManagerHolder.get().setReadwriteSplittingAuto();
                break;
            case WRITE:
                HintManagerHolder.get().setWriteRouteOnly();
                break;
            default:
                break;
        }
        return new UpdateResponseHeader(new EmptyStatement());
    }
}
@NoArgsConstructor(access = AccessLevel.PRIVATE)public final class HintManagerHolder {
    private static final ThreadLocal<HintManager> HINT_MANAGER_HOLDER = new ThreadLocal<>();
    /**
     * Get an instance for {@code HintManager} from {@code ThreadLocal},if not exist,then create new one.
     *
     * @return hint manager
     */    public static HintManager get() {
        if (HINT_MANAGER_HOLDER.get() == null) {
            HINT_MANAGER_HOLDER.set(HintManager.getInstance());
        }
        return HINT_MANAGER_HOLDER.get();
    }
    /**
     * remove {@code HintManager} from {@code ThreadLocal}.
     */    public static void remove() {
        HINT_MANAGER_HOLDER.remove();
    }
}

使用者執行 SQL 之後,DistSQL 解析引擎會首先識別出該 SQL 是讀寫分離 Hint 的 SQL,同時會提取出使用者想要自動路由或者強制到主庫的欄位。之後它會採用 SetReadwriteSplittingHintExecutor 執行器去執行 SQL,從而將正確操作設定到 HintManager 中,進而實現強制路由主庫的功能。

DistSQL HINT 的使用

下表為大家展示了 DistSQL Hint 的相關語法。

本文詳細介紹了 Hint 使用的兩種方式以及基本原理,相信透過本文,讀者朋友們對 Hint 都有了一些基本瞭解了,大家可以根據自己的需求來選擇使用合適的方式。如果在使用過程中遇到任何問題,或者有任何建議想法,都歡迎來社群反饋。

GitHub:

中文社群:

 
歡迎新增社群經理微信(ss_assistant_1)加入交流群,與眾多 ShardingSphere 愛好者一同交流。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70001955/viewspace-2884116/,如需轉載,請註明出處,否則將追究法律責任。

相關文章