- 提供统一编译、逻辑表映射与单源/联邦自动路由 - 增加有界执行、查询生命周期、统计成本优化与执行分析 - 内置 MySQL 与 PostgreSQL JDBC 适配和统计采集
157 lines
8.7 KiB
Markdown
157 lines
8.7 KiB
Markdown
# Easy-Agents Federation SQL
|
||
|
||
基于 Apache Calcite 的 SQL 编译、方言转换、数据源绑定与流式 JDBC 查询底座。
|
||
|
||
## 模块
|
||
|
||
- `easy-agents-federation-sql-core`:公共 API、Calcite 编译、单源/联邦自动路由、计划缓存、数据源 Runtime、准入、指标与取消。
|
||
- `easy-agents-federation-sql-adapter-jdbc`:默认 JDBC Adapter,也是信创数据库 Adapter 的实现示例。
|
||
|
||
业务项目通常只需依赖 JDBC Adapter,它会传递依赖 Core:
|
||
|
||
```xml
|
||
<dependencyManagement>
|
||
<dependencies>
|
||
<dependency>
|
||
<groupId>com.easyagents</groupId>
|
||
<artifactId>easy-agents-bom</artifactId>
|
||
<version>1.2.0-RC</version>
|
||
<type>pom</type>
|
||
<scope>import</scope>
|
||
</dependency>
|
||
</dependencies>
|
||
</dependencyManagement>
|
||
|
||
<dependency>
|
||
<groupId>com.easyagents</groupId>
|
||
<artifactId>easy-agents-federation-sql-adapter-jdbc</artifactId>
|
||
</dependency>
|
||
```
|
||
|
||
## 公共入口
|
||
|
||
- `FederationSqlEngines.builder()`:组装 Engine、Resolver、策略与准入控制器。
|
||
- `engine.sources()`:探测、绑定、预热、更新或移除数据源 Definition。
|
||
- `engine.compile()` / `engine.execute()`:高级的节点本地计划模式。
|
||
- `engine.query()`:推荐入口,在接收请求的节点完成编译或缓存命中并立即执行。
|
||
- `engine.explain()`:显式返回 Calcite 计划;`PHYSICAL` 级别还会请求各数据库的非 `ANALYZE` Explain。
|
||
- `engine.cancel(queryId)`:取消准入等待、JDBC 执行或游标消费中的节点本地查询;编译阶段收到取消后不会继续执行。
|
||
|
||
`FederationSqlPlan` 是 Engine 签发的只读接口,只能交回签发它的 Engine 执行。
|
||
|
||
`FederationSourceDefinition` 始终描述一个物理数据源。一次查询可见的单源或虚拟联邦范围由调用方使用 `FederationQueryScopeDefinition` 声明;Core 不持久化虚拟数据源,也不保存凭据。
|
||
|
||
## 最小使用示例
|
||
|
||
```java
|
||
SourceId sourceId = new SourceId("main");
|
||
FederationSourceDefinition definition = new FederationSourceDefinition(
|
||
sourceId,
|
||
1,
|
||
JdbcFederationSqlAdapterProvider.ADAPTER_ID,
|
||
List.of(new JdbcSchemaDefinition("APP", null, "public")),
|
||
Map.of()
|
||
);
|
||
|
||
try (FederationSqlEngine engine = FederationSqlEngines.builder()
|
||
.dataSourceResolver(current -> {
|
||
HikariDataSource pool = createPool(current.sourceId());
|
||
RuntimeFingerprint fingerprint = detectFingerprint(pool);
|
||
return FederationDataSourceHandles.owned(pool, fingerprint, pool::close);
|
||
})
|
||
.maximumPlanCacheEntries(1024)
|
||
.maximumPlanCacheWeightBytes(64L * 1024L * 1024L)
|
||
.planCacheTimeToLive(Duration.ofMinutes(30))
|
||
.build()) {
|
||
engine.sources().apply(definition, SourceApplyOptions.prewarmNow());
|
||
|
||
SqlQueryCommand command = SqlQueryCommand.of(
|
||
"SELECT NAME FROM APP.PERSON WHERE ID = ?",
|
||
sourceId,
|
||
1,
|
||
List.of(new SqlParameter(Types.INTEGER, 1))
|
||
);
|
||
try (FederationResultCursor cursor = engine.query(command)) {
|
||
while (cursor.next()) {
|
||
System.out.println(cursor.row());
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
`createPool`、凭据存储和 `detectFingerprint` 由调用方实现。Core 管理 Handle/Runtime 生命周期;连接复用、超时、泄漏检测和预热连接数由 HikariCP 等连接池负责。
|
||
|
||
## 虚拟联邦查询
|
||
|
||
调用方先分别登记 MySQL 与 PostgreSQL 的物理 `FederationSourceDefinition`,再为一次查询组装逻辑 Binding:
|
||
|
||
```java
|
||
FederationQueryScopeDefinition scope = FederationQueryScopeDefinition.virtual(
|
||
"sales-analysis",
|
||
7,
|
||
Map.of(
|
||
"SALES", FederationSourceBindingDefinition.of(
|
||
new SourceId("mysql-sales"), 12, Map.of("APP", "APP")
|
||
),
|
||
"CRM", FederationSourceBindingDefinition.of(
|
||
new SourceId("pg-crm"), 5, Map.of("APP", "APP")
|
||
)
|
||
),
|
||
"SALES",
|
||
FederationExecutionPolicy.basic()
|
||
);
|
||
|
||
String sql = """
|
||
SELECT c.ID, SUM(o.AMOUNT) AS TOTAL
|
||
FROM CRM.APP.CUSTOMER c
|
||
JOIN SALES.APP.ORDER_ITEM o ON c.ID = o.CUSTOMER_ID
|
||
GROUP BY c.ID
|
||
ORDER BY TOTAL DESC
|
||
""";
|
||
|
||
try (FederationResultCursor cursor = engine.query(
|
||
SqlQueryCommand.of(sql, scope, List.of())
|
||
)) {
|
||
while (cursor.next()) {
|
||
System.out.println(cursor.row());
|
||
}
|
||
FederationQueryMetricsSnapshot metrics = cursor.metrics();
|
||
}
|
||
```
|
||
|
||
查询模式按 Calcite 校验后实际引用的物理 `SourceId` 数量决定。多 Binding Scope 中只引用一个源的 SQL 仍完整下推;引用多个源时,Core 生成目标方言 Fragment,并使用有界 Calcite 本地算子汇总。
|
||
|
||
调用方已有表列统计快照时,可以通过 `tableStatisticsProvider(...)` 注入行数、行宽、列基数、空值率和唯一键。Provider 的 `snapshot()` 必须一次性返回同时冻结版本、数据和有效期的 `FederationStatisticsSnapshot`,编译阶段不得主动执行 `COUNT(*)`;版本变化会隔离旧计划缓存,计划缓存期限也不会超过统计快照的最早失效时间。统计完整且未过期时,等值 `INNER JOIN` 会把估算搬运量较小的一侧作为本地 Hash Table 构建端;统计缺失、不完整或过期时保持稳定的保守顺序。逻辑 Explain 的每个 Fragment 会返回估算是否可用、扫描/输出行数、行宽、搬运字节、统计来源/采集时间和下推算子。
|
||
|
||
首批联邦算子覆盖等值 `INNER JOIN`、`LEFT JOIN`、`UNION ALL`、`COUNT/SUM/MIN/MAX/AVG`、普通 `GROUP BY`、CTE、排序和分页。非等值 Join、联邦本地字符比较/排序/分组/`MIN/MAX`、`UNION DISTINCT`、窗口函数、磁盘 Spill 与跨库事务快照会明确拒绝。字符型本地算子需要调用方先统一排序规则,后续再由 Adapter 提供可验证的 Collation 能力。Calcite 本地时间表示只保证毫秒精度;映射精度超过 3 位或运行时检测到亚毫秒值时会明确拒绝。驱动以 `ANY/OTHER` 返回的标准 JDBC 时区标量会保留纳秒并统一为 UTC Offset。
|
||
|
||
结果采用标准流式 Cursor 语义:Fragment 或本地算子可能在调用方已读取若干行后失败,已交付的行无法撤回。调用方只能在 `next()` 正常返回 `false` 后将本次结果视为完整成功;需要不可逆副作用时应先完整消费并自行提交,或提供补偿机制。
|
||
|
||
## Explain 与指标
|
||
|
||
普通 `query` 不会访问数据库 Optimizer。只有显式调用物理 Explain 才会产生额外数据库往返:
|
||
|
||
```java
|
||
SqlCompileRequest compile = SqlCompileRequest.of(sql, scope);
|
||
SqlExplainResult logical = engine.explain(
|
||
new SqlExplainRequest(compile, SqlExplainLevel.LOGICAL)
|
||
);
|
||
SqlExplainResult physical = engine.explain(new SqlExplainRequest(compile));
|
||
```
|
||
|
||
`physical.fragments()` 为每个 Fragment 返回目标方言 SQL、参数映射和数据库原生计划。MySQL/PostgreSQL Adapter 尽力归一化扫描方式、候选索引、选中索引、估算行数与过滤条件;数据库没有返回的字段保持空值。为避免原生计划回显敏感常量,Explain 不接受实际参数值,只按 `SqlCompileRequest` 声明的 JDBC 类型绑定 `NULL`,因此索引选择可能与真实参数计划不同。
|
||
|
||
`FederationResultCursor.metrics()` 可在消费过程中读取,并在耗尽或关闭后定稿,包含模式、计划缓存命中、编译、准入等待、连接获取、数据库执行、本地算子、首行与完整消费耗时,以及最终行/字节、中间搬运行/字节、截断、超时、错误分类和各 Fragment 统计。Adapter 无法安全估算字节时对应字段为 `-1`,不会用 `0` 冒充已测量值。
|
||
|
||
查询总时限取 Engine、Query Scope 和请求 JDBC timeout 中的最小值。硬时限会覆盖连接池等待后的 JDBC 执行和游标消费,并尝试同时 `cancel`、关闭全部活动 Statement/Cursor;连接池自身仍需配置有限的 connection timeout,以约束 Statement 创建前的连接获取阶段。
|
||
|
||
## Adapter 扩展
|
||
|
||
实现 `FederationSqlAdapterProvider` 并通过 Java `ServiceLoader` 注册。Adapter 直接提供 Calcite `Schema`、`SqlDialect`、类型系统、运算符表、Planner Rule 和参数 `SqlDataTypeSpec`,无需额外中间态。重复 `adapterId` 会在启动时拒绝。
|
||
|
||
## 分布式边界
|
||
|
||
Definition、revision 和墓碑可以由调用方存入 Redis 等共享状态系统,并通过 `FederationSourceStateProvider` 下发。连接池、Calcite Schema、计划与活动查询均为节点本地对象,不应序列化或跨节点共享。负载均衡请求应携带 `minimumRevision`,落后节点会先同步或返回明确的未就绪错误。
|
||
|
||
当前联邦路径以最多两个实际物理源和内存内有界汇总为基线。各源使用独立只读连接,不提供跨数据库全局快照一致性;应通过 `FederationExecutionPolicy` 为中间行数、字节数、Fragment 数和总时限设置硬上限。
|