ApacheShardingSphereの実用ガイドヒント

SphereExミドルウェアR&DエンジニアであるApache ShardingSphereCommitterのChenChuxinは、現在、ApacheShardingSphereカーネルモジュールの研究開発に注力しています。

バックグラウンド

ユーザーの実際の使用シナリオに基づいて、Apache ShardingSphereは、データシャーディング、読み取り/書き込み分離など、ユーザー向けのさまざまな実用的な機能を作成しました。データシャーディング機能では、Apache ShardingSphereは、標準シャーディングや複合シャーディングなどのさまざまな実用的なシャーディング戦略を提供します。さまざまなシャーディング戦略では、ユーザーは関連するシャーディングアルゴリズムを構成して、データシャーディングの問題を解決できます。読み取り/書き込み分離機能では、ShardingSphereは、ユーザーの実際のニーズを満たすために、静的および動的、および豊富な負荷分散アルゴリズムの2種類の読み取り/書き込み分離をユーザーに提供します。

ShardingSphereのシャーディングおよび読み取り/書き込み分離機能はすでに非常に豊富であることがわかりますが、ユーザーの実際の使用シナリオは常に変化しています。マルチテナントシナリオを例にとると、ユーザーはログインアカウントが属するテナントに応じてシャーディングすることを期待しますが、テナント情報は各ビジネスSQLに存在しません。この場合、SQLからシャードフィールドを抽出するためのアルゴリズム動作しないでしょう。読み取りと書き込みの分離を例にとると、ほとんどのシナリオで、ユーザーはクエリ操作をスレーブデータベースにルーティングして実行できるようにしたいのですが、リアルタイム要件が高い一部のシナリオでは、SQLを強制的に実行用のマスターデータベース。現時点では、読み取りと書き込みの分離はビジネス要件を満たすことができません。

上記の問題点に基づいて、Apache ShardingSphereはユーザーにヒント機能を提供します。ユーザーはSQLの外部のロジックを使用して、実際のビジネスシナリオと組み合わせて強制ルーティングまたはシャーディングを実行できます。現在、ShardingSphereは2つのヒントメソッドをユーザーに提供しています。1つはJava APIを介した手動プログラミングであり、強制ルーティングとシャーディングにHintManagerを使用します。このメソッドは、JDBCプログラミングを使用するアプリケーションに非常に適しています。簡単に作成するには、少量のコードしか必要ありません。 SQLに依存しないシャーディングまたは強制ルーティングを実装します。もう1つの方法は、開発を知らないDBAにとってより使いやすい方法です。ShardingSphereは、分散SQLによって提供される使用法に基づいて、SQLHINTおよびDistSQLHINTを使用することにより、コーディングなしでシャーディングおよび強制ルーティング機能をユーザーに提供します。次に、これら2つの使用方法を見てみましょう。

HintManagerに基づく手動プログラミング

ShardingSphereは主に、HintManagerオブジェクトを介した強制ルーティングとシャーディングの機能を実装します。HintManagerを使用すると、ユーザーのシャーディングはSQLに依存しなくなります。これにより、ユーザーの使用シナリオが大幅に拡張され、ユーザーはデータをシャーディングしたり、ルーティングをより柔軟に適用したりできます。現在、ユーザーはHintManagerを介して、ShardingSphereの組み込みまたはカスタマイズされたHintアルゴリズムと連携してシャーディング機能を実現できます。また、指定されたデータソースを設定したり、メインライブラリに読み取りと書き込みを強制したりすることで、強制ルーティング機能を実現できます。HintManagerの使用法を学ぶ前に、その実装原理を簡単に理解しましょう。これは、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_id

props:
    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 也会直接在数据库 ds

0 上原封不动地执行,并返回执行结果。

/* 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 visit(final ParseContext parseContext) { ParseTreeVisitor visitor = SQLVisitorFactory.newInstance(databaseType, visitorType, SQLVisitorRule.valueOf(parseContext.getParseTree().getClass()), props); T result = parseContext.getParseTree().accept(visitor); appendSQLComments(parseContext, result); return result; }

    private void appendSQLComments(final ParseContext parseContext, final T visitResult) { if (!parseContext.getHiddenTokens().isEmpty() && visitResult instanceof AbstractSQLStatement) { Collection 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


@RequiredArgsConstructor
public 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:github.com/apache/shar…

中文社区:community.sphere-ex.com/

欢迎添加社区经理微信(ss_assistant_1)加入交流群,与众多 ShardingSphere 爱好者一同交流。

おすすめ

転載: juejin.im/post/7078973491554287647