# 多数据源配置与切换
**本文引用的文件**
- [DataSourceConfig.java](file://ai-server/src/main/java/com/zsjz/ai/common/config/DataSourceConfig.java)
- [CaseRoutingDataSource.java](file://ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseRoutingDataSource.java)
- [CaseDataSourceRegistry.java](file://ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseDataSourceRegistry.java)
- [DuckdbUnpooledDataSource.java](file://ai-server/src/main/java/com/zsjz/ai/common/config/DuckdbUnpooledDataSource.java)
- [application.yaml](file://ai-server/src/main/resources/application.yaml)
- [application-dev.yaml](file://ai-server/src/main/resources/application-dev.yaml)
- [application-pro.yaml](file://ai-server/src/main/resources/application-pro.yaml)
- [pom.xml(ai-server)](file://ai-server/pom.xml)
- [RagVectorMapper.java](file://ai-server/src/main/java/com/zsjz/ai/module/agent/mapper/RagVectorMapper.java)
- [AiCleanTemplateVectorIndex.java](file://ai-server/src/main/java/com/zsjz/ai/module/aiclean/index/AiCleanTemplateVectorIndex.java)
- [case_table_1.sql](file://sql/case_table_1.sql)
## 目录
1. [引言](#引言)
2. [项目结构定位](#项目结构定位)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖关系分析](#依赖关系分析)
7. [pgvector 向量表约束与性能注意事项](#pgvector-向量表约束与性能注意事项)
8. [典型用法示例](#典型用法示例)
9. [常见问题排查](#常见问题排查)
10. [结论](#结论)
## 引言
本文面向后端开发者,说明本项目中 MyBatis-Plus + Dynamic Datasource 的多数据源设计:PostgreSQL/pgvector 作为主业务数据源,DuckDB 作为每个案件的临时/本地计算数据源。文档重点包括:
- 主数据源与案件 DuckDB 的用途差异;
- 如何在 Spring 配置中声明多个数据源;
- Service 层按场景切换数据源的机制;
- pgvector 向量表的约束、索引策略和性能注意事项;
- 常见错误及排查建议。
## 项目结构定位
本项目的多数据源能力集中在 `ai-server` 模块中:
- 数据源装配在 `common/config` 下;
- 动态路由与案件数据源生命周期管理在 `common/datasource` 下;
- PostgreSQL 连接信息放在 `application-dev.yaml` / `application-pro.yaml`;
- DuckDB 驱动由 `ai-server/pom.xml` 引入;
- pgvector 相关 SQL 和 Java 访问入口位于 `module/agent/mapper` 与 `module/aiclean/index`;
- 案件 DuckDB 的初始建表脚本位于 `sql/case_table_1.sql`。
```mermaid
graph TB
A["Spring Boot 应用"] --> B["MyBatis-Plus"]
B --> C["Dynamic Datasource
DynamicRoutingDataSource"]
C --> D["CaseRoutingDataSource
自定义路由"]
D --> E["master
PostgreSQL/pgvector"]
D --> F["case{caseId}
DuckDB 案件库"]
G["Service/Mapper"] --> C
H["模板向量索引"] --> E
I["清洗/临时分析"] --> F
```
**图表来源**
- [DataSourceConfig.java:1-38](file://ai-server/src/main/java/com/zsjz/ai/common/config/DataSourceConfig.java#L1-L38)
- [CaseRoutingDataSource.java:1-68](file://ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseRoutingDataSource.java#L1-L68)
- [CaseDataSourceRegistry.java:1-200](file://ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseDataSourceRegistry.java#L1-L200)
**章节来源**
- [DataSourceConfig.java:1-38](file://ai-server/src/main/java/com/zsjz/ai/common/config/DataSourceConfig.java#L1-L38)
- [application.yaml:1-193](file://ai-server/src/main/resources/application.yaml#L1-L193)
- [application-dev.yaml:1-87](file://ai-server/src/main/resources/application-dev.yaml#L1-L87)
- [application-pro.yaml:1-87](file://ai-server/src/main/resources/application-pro.yaml#L1-L87)
## 核心组件
本节列出与多数据源直接相关的核心类及其职责。
| 组件 | 职责 | 关键行为 |
|---|---|---|
| `DataSourceConfig` | 替换 Dynamic Datasource 自动装配的数据源 Bean | 构造 `CaseRoutingDataSource`,并把 `spring.datasource.dynamic.*` 中的 primary、strict、strategy、p6spy、seata、graceDestroy 等属性复制到自定义路由数据源上 |
| `CaseRoutingDataSource` | 把逻辑 key `slave` 映射到当前案件的物理 key `case{caseId}` | 未打开案件时返回 400;已打开但物理 key 不存在时也返回 400;其他 key 走默认路由 |
| `CaseDataSourceRegistry` | 管理已打开案件的 DuckDB 数据源生命周期 | 开案时校验文件、分配资源预算、注册 `case{caseId}`、补建 `file_ai_profile`;关案时移除并关闭 |
| `DuckdbUnpooledDataSource` | DuckDB 单实例数据源包装 | 通过 `duplicate()` 创建独立连接;设置全局内存和线程数;支持按进程分摊的资源预算参数 |
| `RagVectorMapper` | 对 pgvector 向量表执行原生 SQL | 建表、插入、删除、cosine 相似度检索、计数、判断表是否存在 |
| `AiCleanTemplateVectorIndex` | 模板元数据的 pgvector 向量索引服务 | 增量索引、全量重建、fail-open 降级 |
**章节来源**
- [DataSourceConfig.java:1-38](file://ai-server/src/main/java/com/zsjz/ai/common/config/DataSourceConfig.java#L1-L38)
- [CaseRoutingDataSource.java:1-68](file://ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseRoutingDataSource.java#L1-L68)
- [CaseDataSourceRegistry.java:1-200](file://ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseDataSourceRegistry.java#L1-L200)
- [DuckdbUnpooledDataSource.java:1-200](file://ai-server/src/main/java/com/zsjz/ai/common/config/DuckdbUnpooledDataSource.java#L1-L200)
- [RagVectorMapper.java:1-113](file://ai-server/src/main/java/com/zsjz/ai/module/agent/mapper/RagVectorMapper.java#L1-L113)
- [AiCleanTemplateVectorIndex.java:1-200](file://ai-server/src/main/java/com/zsjz/ai/module/aiclean/index/AiCleanTemplateVectorIndex.java#L1-L200)
## 架构总览
系统使用两个不同性质的数据库:
| 数据源 | 角色 | 存储位置 | 典型用途 |
|---|---|---|---|
| PostgreSQL | 主业务数据源 | 外部 PostgreSQL 服务器 | 用户、权限、模板、元数据、向量索引表、系统配置 |
| DuckDB | 临时/本地计算数据源 | 每个案件一个独立文件 | 导入原始表格、清洗中间结果、临时分析、快照导出 |
```mermaid
flowchart TD
Start["请求进入 Service 层"] --> CheckCase{"是否操作案件数据?"}
CheckCase -->|否| Master["写入/查询 master
PostgreSQL/pgvector"]
CheckCase -->|是| OpenCase{"案件 DuckDB 是否已打开?"}
OpenCase -->|否| ErrorNoCase["抛出 400:请先打开案件"]
OpenCase -->|是| Route["CaseRoutingDataSource
slave → case{caseId}"]
Route --> DuckDB["写入/查询 DuckDB 案件库"]
Master --> End["返回结果"]
DuckDB --> End
ErrorNoCase --> End
```
**图表来源**
- [CaseRoutingDataSource.java:20-68](file://ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseRoutingDataSource.java#L20-L68)
- [CaseDataSourceRegistry.java:40-120](file://ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseDataSourceRegistry.java#L40-L120)
该设计的关键是:
- 主数据源 `master` 始终指向 PostgreSQL;
- 所有需要读案件数据的 Mapper 都标注逻辑 key `slave`;
- `CaseRoutingDataSource` 在运行时把 `slave` 转换为 `case{caseId}`;
- DuckDB 不是全局共享数据源,而是“每案一库”。
**章节来源**
- [application-dev.yaml:8-26](file://ai-server/src/main/resources/application-dev.yaml#L8-L26)
- [application-pro.yaml:8-26](file://ai-server/src/main/resources/application-pro.yaml#L8-L26)
- [CaseRoutingDataSource.java:12-68](file://ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseRoutingDataSource.java#L12-L68)
## 详细组件分析
### 数据源装配与替换
`DataSourceConfig` 没有重新定义连接池,而是接管了 Dynamic Datasource 的装配过程:它创建一个 `CaseRoutingDataSource`,并将 `DynamicDataSourceProperties` 中的主要开关复制过去。这样既保留了 dynamic-datasource 的自动发现能力,又让路由逻辑由项目自己控制。
需要注意:
- `primary` 仍为 `master`;
- `strict=true` 能防止未来新增 Mapper 误写 `@DS` 后静默回退主库;
- `CaseRoutingDataSource` 对 `slave` 分支做额外校验,不受 strict 的通用容错影响。
```mermaid
classDiagram
class DataSourceConfig {
+caseRoutingDataSource(providers, properties) CaseRoutingDataSource
}
class CaseRoutingDataSource {
-routingDataSource : DynamicRoutingDataSource
+getDataSource(ds) DataSource
}
class CaseDataSourceRegistry {
+open(caseId, dbPath, ownerUserId) void
+close(caseId) void
+physicalKey(caseId) String
}
class DuckdbUnpooledDataSource {
+getConnection() Connection
+close() void
}
DataSourceConfig --> CaseRoutingDataSource : "创建并注入"
CaseDataSourceRegistry --> CaseRoutingDataSource : "注册/移除 case{caseId}"
CaseDataSourceRegistry --> DuckdbUnpooledDataSource : "构造 DuckDB 实例"
```
**图表来源**
- [DataSourceConfig.java:1-38](file://ai-server/src/main/java/com/zsjz/ai/common/config/DataSourceConfig.java#L1-L38)
- [CaseRoutingDataSource.java:1-68](file://ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseRoutingDataSource.java#L1-L68)
- [CaseDataSourceRegistry.java:1-120](file://ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseDataSourceRegistry.java#L1-L120)
- [DuckdbUnpooledDataSource.java:1-120](file://ai-server/src/main/java/com/zsjz/ai/common/config/DuckdbUnpooledDataSource.java#L1-L120)
**章节来源**
- [DataSourceConfig.java:1-38](file://ai-server/src/main/java/com/zsjz/ai/common/config/DataSourceConfig.java#L1-L38)
### 案件数据源路由
`CaseRoutingDataSource` 继承自 `DynamicRoutingDataSource`,只覆盖 `getDataSource(String)`:
- 非 `slave` 的 key 直接交给父类处理;
- `slave` 会读取当前线程上下文中的 `caseId`;
- 若 `caseId` 为空,返回 400,表示“用户没开案”;
- 根据 `caseId` 生成物理 key `case{caseId}`,并在已注册数据源中查找;
- 找不到时返回 400,表示“案件数据源未打开或已释放”。
物理 key 命名要求严格:不能使用下划线。因为 dynamic-datasource 会把下划线前的部分当作组名,可能导致负载均衡随机串库。
```mermaid
flowchart TD
Enter["getDataSource(key)"] --> IsSlave{"key == slave ?"}
IsSlave -->|否| Super["super.getDataSource(key)"]
IsSlave -->|是| ReadCase["读取当前 caseId"]
ReadCase --> HasCase{"caseId 是否存在?"}
HasCase -->|否| ErrNoCase["返回 400:请先打开案件"]
HasCase -->|是| BuildKey["构建 case{caseId}"]
BuildKey --> FindDs{"是否已注册?"}
FindDs -->|否| ErrNotOpen["返回 400:案件数据源未打开"]
FindDs -->|是| ReturnDs["返回 DuckDB 数据源"]
```
**图表来源**
- [CaseRoutingDataSource.java:20-68](file://ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseRoutingDataSource.java#L20-L68)
**章节来源**
- [CaseRoutingDataSource.java:12-68](file://ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseRoutingDataSource.java#L12-L68)
### 案件数据源生命周期
`CaseDataSourceRegistry` 是 DuckDB 案件库的真正管理者。它维护一个从 `caseId` 到 `OpenedCase` 的并发映射,记录数据源、归属用户、归属 token 和打开时间。
开案流程要点:
1. 校验是否已有相同 `caseId` 被打开;
2. 限制同时打开的案件数量;
3. 校验 DuckDB 文件路径合法且文件大小大于 0;
4. 根据 `DuckdbBudgetProperties` 计算内存和线程预算;
5. 创建 `DuckdbUnpooledDataSource`;
6. 向 `CaseRoutingDataSource` 注册 `case{caseId}`;
7. 幂等补建 `file_ai_profile` 表;
8. 写入内存中的已打开案件列表。
关案流程要点:
- 从路由数据源移除物理 key;
- 关闭 DuckDB 数据源;
- 清理内存中的 `caseId` 条目。
```mermaid
sequenceDiagram
participant Client as "调用方"
participant Registry as "CaseDataSourceRegistry"
participant Routing as "CaseRoutingDataSource"
participant Duck as "DuckdbUnpooledDataSource"
participant File as "DuckDB 文件"
Client->>Registry: open(caseId, dbPath, ownerUserId)
Registry->>Registry: 校验 caseId 是否已打开
Registry->>Registry: 检查最大打开案件数
Registry->>File: 检查文件存在且大小大于 0
Registry->>Duck: 构造数据源(内存/线程预算)
Registry->>Routing: addDataSource(case{caseId}, duck)
Registry->>Registry: 补建 file_ai_profile
Registry-->>Client: 开案成功
Client->>Registry: close(caseId)
Registry->>Routing: removeDataSource(case{caseId})
Registry->>Duck: close()
Registry-->>Client: 关闭完成
```
**图表来源**
- [CaseDataSourceRegistry.java:40-120](file://ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseDataSourceRegistry.java#L40-L120)
- [CaseDataSourceRegistry.java:200-280](file://ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseDataSourceRegistry.java#L200-L280)
- [DuckdbUnpooledDataSource.java:60-120](file://ai-server/src/main/java/com/zsjz/ai/common/config/DuckdbUnpooledDataSource.java#L60-L120)
**章节来源**
- [CaseDataSourceRegistry.java:1-120](file://ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseDataSourceRegistry.java#L1-L120)
- [CaseDataSourceRegistry.java:200-280](file://ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseDataSourceRegistry.java#L200-L280)
### DuckDB 数据源实现
`DuckdbUnpooledDataSource` 不是传统连接池,而是一个单实例包装器:
- 内部持有唯一的 `DuckDBConnection`;
- 每次 `getConnection()` 调用都会通过 `duplicate()` 获取独立连接;
- 可配置 `memoryLimitInMiB` 和 `threads`;
- 连接级网络超时复用守护线程执行器,避免连接数增长导致线程泄漏。
资源预算来自 `zsjz.duckdb.memory-ratio` 和 `zsjz.duckdb.threads-per-instance`。由于 `SET GLOBAL` 作用域是 DuckDB 的 database instance,而不是单个连接,因此必须按“每案实例”分摊预算,否则多人同时开案会导致内存和 CPU 成倍叠加。
**章节来源**
- [DuckdbUnpooledDataSource.java:1-120](file://ai-server/src/main/java/com/zsjz/ai/common/config/DuckdbUnpooledDataSource.java#L1-L120)
- [DuckdbUnpooledDataSource.java:120-200](file://ai-server/src/main/java/com/zsjz/ai/common/config/DuckdbUnpooledDataSource.java#L120-L200)
- [application.yaml:54-80](file://ai-server/src/main/resources/application.yaml#L54-L80)
## 依赖关系分析
项目通过 Maven 引入以下关键依赖:
- PostgreSQL JDBC 驱动;
- pgvector Java SDK;
- DuckDB JDBC 驱动;
- MyBatis-Plus Spring Boot 3 Starter;
- Dynamic Datasource Spring Boot 3 Starter。
```mermaid
graph LR
App["ai-server 应用"] --> MP["MyBatis-Plus"]
App --> DDS["dynamic-datasource-spring-boot3-starter"]
App --> PG["postgresql"]
App --> PGV["com.pgvector:pgvector"]
App --> DDB["duckdb_jdbc"]
MP --> PG
MP --> DDB
DDS --> App
```
**图表来源**
- [pom.xml(ai-server):1-200](file://ai-server/pom.xml#L1-L200)
**章节来源**
- [pom.xml(ai-server):1-200](file://ai-server/pom.xml#L1-L200)
## pgvector 向量表约束与性能注意事项
### 用途区分
- PostgreSQL 上的 pgvector 表用于存储**全局向量索引**,例如模板元数据向量、RAG 向量表;
- DuckDB 不用于承载主业务向量索引;
- 向量索引的事实来源是模板元数据,而不是案件 DuckDB。
### 向量表结构与字段
`RagVectorMapper` 定义的向量表包含:
- `doc_id`:文档唯一标识;
- `content`:文本内容;
- `payload`:结构化 JSONB;
- `embedding`:`vector(${dims})` 列,维度由运行期决定。
对于 AI 模板向量索引,表名形如 `rag_tpl{tag}_d{dims}`,其中 `dims` 是嵌入模型输出维度。
### 约束
- `doc_id` 是主键;
- 向量列类型必须是 `vector`,维度必须与嵌入模型一致;
- 写入时必须把 embedding 转为 `::vector`;
- payload 通常用 `::jsonb` 转换;
- 向量表名称由服务层拼接,不能直接使用普通 MyBatis-Plus 实体注解;
- 如果嵌入模型维度变化,应重建对应向量表,而不是混用旧表。
### 索引策略
代码中未显式创建额外的 pgvector 索引(例如 IVFFlat/HNSW),而是使用 pgvector 默认的 cosine 距离 `<=>` 进行比较检索:
- 检索公式为 `1 - (embedding <=> #{embedding}::vector)`;
- 通过 `threshold` 过滤低分结果;
- 通过 `limit` 控制 top-k;
- 对模板索引采用“按需建表 + ON CONFLICT 覆盖写”,避免重复建表冲突。
这意味着在大规模向量表中,cosine 搜索可能退化为全表扫描。后续如需优化,应在生产环境评估是否添加 pgvector 索引类型,并验证召回质量与写入成本。
### 性能注意事项
1. **向量维度必须固定**:同一张向量表内所有行的 embedding 维度必须一致。
2. **不要频繁重建整表**:模板索引使用 `ON CONFLICT` 增量更新,避免 DROP 再 CREATE 造成的锁竞争。
3. **fail-open 降级**:向量索引不可用时不应阻断主业务链路;AI 模板识别仍可通过指纹、打分和 LLM 继续工作。
4. **嵌入模型加载成本高**:本地 ONNX 模型首次加载可能较慢,应考虑预热或懒加载策略。
5. **向量检索是辅助召回**:召回结果只是候选集,最终匹配逻辑不应只依赖相似度。
**章节来源**
- [RagVectorMapper.java:1-113](file://ai-server/src/main/java/com/zsjz/ai/module/agent/mapper/RagVectorMapper.java#L1-L113)
- [AiCleanTemplateVectorIndex.java:1-200](file://ai-server/src/main/java/com/zsjz/ai/module/aiclean/index/AiCleanTemplateVectorIndex.java#L1-L200)
- [application.yaml:80-160](file://ai-server/src/main/resources/application.yaml#L80-L160)
## 典型用法示例
### 普通 CRUD 使用默认数据源
大多数业务 Mapper 不需要显式指定数据源,它们会走 MyBatis-Plus 默认装配的主数据源,即 PostgreSQL。这类操作包括:
- 用户、权限、系统配置;
- 模板元数据;
- 向量索引表读写;
- 系统日志、审计信息等。
推荐做法:
- 不要给这些 Mapper 加 `@DS`;
- 如果需要跨库写,不要在同一个事务里混写 PostgreSQL 和 DuckDB;
- 如果确实需要明确主数据源,优先保持默认行为,而不是随意加 `@DS("master")`。
### 案件数据查询切换到 DuckDB
当业务需要读取某个案件导入的表格数据时,应先确保案件已打开,然后使用标注 `@DS("slave")` 的 Mapper。框架会自动把 `slave` 路由到 `case{caseId}`。
推荐流程:
1. Controller 或上层服务先调用 `CaseDataSourceRegistry.open(...)`;
2. 在 `CaseContextHolder` 中设置当前 `caseId`;
3. 调用标注 `@DS("slave")` 的 Mapper 读取案件表;
4. 业务完成后调用 `CaseDataSourceRegistry.close(caseId)`。
注意:
- 如果当前没有 `caseId`,`CaseRoutingDataSource` 会返回 400;
- 如果 `caseId` 已关闭,也会返回 400;
- 不要把案件 SQL 误写成 `@DS("master")`,否则会查 PostgreSQL。
### 批量清洗或临时分析切换到 DuckDB
批量清洗、临时分析、预览、快照导出等属于“计算型任务”,适合在 DuckDB 中进行:
- 清洗中间结果可以写进案件 DuckDB;
- 临时分析 SQL 可在 DuckDB 中执行;
- 导出快照时,由 `CaseDataSourceRegistry.exportSnapshot` 导出独立 DuckDB 文件;
- 子进程只读打开快照文件,避免与主进程写锁冲突。
```mermaid
sequenceDiagram
participant User as "用户"
participant Service as "清洗/分析 Service"
participant Registry as "CaseDataSourceRegistry"
participant DS as "CaseRoutingDataSource"
participant Duck as "DuckDB"
User->>Service: 提交批量清洗任务
Service->>Registry: open(caseId, dbPath)
Service->>Service: 准备清洗 SQL / 数据
Service->>DS: @DS("slave") 写入 DuckDB
DS->>Duck: 写入临时表 / 清洗结果
Service->>Registry: exportSnapshot(caseId, target)
Registry->>Duck: 导出快照文件
Service->>Registry: close(caseId)
```
**图表来源**
- [CaseDataSourceRegistry.java:40-120](file://ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseDataSourceRegistry.java#L40-L120)
- [CaseDataSourceRegistry.java:200-280](file://ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseDataSourceRegistry.java#L200-L280)
**章节来源**
- [CaseRoutingDataSource.java:20-68](file://ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseRoutingDataSource.java#L20-L68)
- [CaseDataSourceRegistry.java:40-120](file://ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseDataSourceRegistry.java#L40-L120)
- [CaseDataSourceRegistry.java:200-280](file://ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseDataSourceRegistry.java#L200-L280)
## 常见问题排查
### 数据源选择错误
**现象:** 本该查 DuckDB 的案件数据却查到 PostgreSQL,或者反过来。
**可能原因:**
- Mapper 误加 `@DS("master")`;
- 忘记标注 `@DS("slave")`;
- 当前线程没有 `caseId`;
- 物理 key 使用了下划线,导致 group 解析异常;
- 案件已关闭,但业务仍尝试访问。
**排查建议:**
- 开启 debug 日志查看 `slave -> case{caseId}` 的路由信息;
- 确认 `CaseContextHolder.currentCaseId()` 是否非空;
- 确认 `CaseDataSourceRegistry.isOpen(caseId)`;
- 检查 `@DS` 值是否为 `slave` 或空。
**章节来源**
- [CaseRoutingDataSource.java:20-68](file://ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseRoutingDataSource.java#L20-L68)
### pgvector 扩展未安装
**现象:** 向量表创建失败、向量类型无法识别、向量检索报错。
**可能原因:**
- PostgreSQL 未安装 `pgvector` 扩展;
- 向量表所在 schema 不可写;
- 向量维度与实际嵌入模型不一致;
- 向量表名拼写错误或被误删。
**排查建议:**
- 确认 PostgreSQL 已安装 pgvector;
- 检查 `RagVectorMapper` 生成的向量表是否存在;
- 检查向量列类型是否为 `vector` 且维度正确;
- 检查模板向量索引是否处于 fail-open 降级状态。
**章节来源**
- [RagVectorMapper.java:1-113](file://ai-server/src/main/java/com/zsjz/ai/module/agent/mapper/RagVectorMapper.java#L1-L113)
- [AiCleanTemplateVectorIndex.java:1-200](file://ai-server/src/main/java/com/zsjz/ai/module/aiclean/index/AiCleanTemplateVectorIndex.java#L1-L200)
### DuckDB JDBC 连接失败
**现象:** 开案失败、DuckDB 连接异常、文件被占用。
**可能原因:**
- DuckDB JDBC 驱动未引入;
- DuckDB 文件路径非法;
- DuckDB 文件不存在或大小为 0;
- 目标目录不可写;
- Windows 下另一个进程占用了 DuckDB 文件;
- 同时打开的案件数超过上限。
**排查建议:**
- 确认 `ai-server/pom.xml` 中引入了 `duckdb_jdbc`;
- 检查 `zsjz.duckdb.max-open-cases`;
- 检查 DuckDB 文件绝对路径是否可读且非空;
- 避免主进程与子进程同时写入同一个 DuckDB 文件;
- 使用 `exportSnapshot` 导出快照后再让子进程只读打开。
**章节来源**
- [pom.xml(ai-server):1-200](file://ai-server/pom.xml#L1-L200)
- [CaseDataSourceRegistry.java:40-120](file://ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseDataSourceRegistry.java#L40-L120)
- [CaseDataSourceRegistry.java:200-280](file://ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseDataSourceRegistry.java#L200-L280)
### 事务跨数据源限制
**重要限制:** PostgreSQL 和 DuckDB 是两个独立的数据库实例,不能在同一个数据库事务中跨数据源提交。
**建议做法:**
- 不要把 PostgreSQL 写和 DuckDB 写放在同一个事务中;
- 如果需要一致性,应在业务层协调两阶段提交,例如先写主库,再写 DuckDB,失败时记录补偿日志;
- 对于清洗、分析等临时数据,允许最终一致性;
- 不要把 DuckDB 当成 PostgreSQL 的事务后备库。
**章节来源**
- [application-dev.yaml:8-26](file://ai-server/src/main/resources/application-dev.yaml#L8-L26)
- [application-pro.yaml:8-26](file://ai-server/src/main/resources/application-pro.yaml#L8-L26)
- [CaseRoutingDataSource.java:20-68](file://ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseRoutingDataSource.java#L20-L68)
### 案件数据源未打开或已释放
**现象:** 调用案件数据接口时报“请先打开案件”或“案件数据源未打开”。
**可能原因:**
- 前端未成功进入案件页面;
- 用户退出案件后未及时刷新;
- 用户被顶下线或会话过期;
- 开案时 DuckDB 文件无效;
- 达到同时打开案件上限。
**排查建议:**
- 检查是否调用了 `CaseDataSourceRegistry.open`;
- 检查 `CaseContextHolder.currentCaseId()`;
- 检查 `openedCount()` 是否达到上限;
- 检查 DuckDB 文件路径和大小;
- 检查用户会话状态。
**章节来源**
- [CaseRoutingDataSource.java:20-68](file://ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseRoutingDataSource.java#L20-L68)
- [CaseDataSourceRegistry.java:40-120](file://ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseDataSourceRegistry.java#L40-L120)
## 结论
本项目的多数据源方案以 PostgreSQL 为主业务数据源,以 DuckDB 为每案独立的临时计算数据源。通过 `DataSourceConfig` 替换默认路由、`CaseRoutingDataSource` 将 `slave` 映射到 `case{caseId}`、`CaseDataSourceRegistry` 管理 DuckDB 生命周期,系统在保持 MyBatis-Plus 和 Dynamic Datasource 兼容性的同时,实现了“按案件隔离”的计算数据源。
实践中应遵守以下原则:
- 主业务数据走 PostgreSQL;
- 案件数据查询和临时计算走 DuckDB;
- 不要跨数据源使用单一事务;
- 向量索引存储在 PostgreSQL/pgvector,不污染 DuckDB;
- 开案和关案要成对调用,避免内存和文件句柄泄漏;
- 出现串库或空数据问题时,优先检查 `@DS`、`caseId`、DuckDB 文件和路由日志。