# 多数据源配置与切换 **本文引用的文件** - [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 文件和路由日志。