第 3 章 从单体项目到 Spring Cloud 微服务

本章目标

前两章我们已经完成了两件事。

第一,理解 KnowHub 为什么不是一个普通聊天机器人,而是一个企业级 AI 知识库 / RAG 平台。

第二,梳理项目需要的技术栈和环境,包括 Java 17、Spring Cloud、Spring AI、MySQL、pgvector、Redis、RabbitMQ、MinIO、Nacos 和 Vue 前端。

从这一章开始,我们进入后端工程结构。

很多读者第一次看到本书项目时,会发现目录里有两套后端项目:在这里插入代码片

D:\rag\rag-demo-monolith`在这里插入代码片`
D:\rag\rag-platform

一个是单体版,一个是微服务版。

这不是重复建设,而是一条非常重要的学习路径:先用单体项目跑通业务闭环,再把边界清晰的能力拆成 Spring Cloud 多服务。

本章要解决的问题是:

  1. 为什么一开始不直接写微服务。
  2. 单体版 rag-demo-monolith 解决了什么问题。
  3. 单体项目继续扩展会遇到哪些边界。
  4. 微服务版 rag-platform 为什么拆成 Gateway、Auth、Knowledge、Task 和 Common。
  5. 请求在单体版和微服务版中分别怎么流转。

学完本章,你应该能看懂整个项目的后端结构,而不是只知道“这里有很多服务”。


3.1 为什么先做单体版

初学者做企业级项目时,很容易想一步到位:

Spring Cloud
Gateway
Nacos
Feign
RabbitMQ
Redis
MinIO
pgvector
Vue 管理端

这些技术看起来很完整,也很像企业项目。但如果一开始就全部上,问题会很快出现:你可能还没理解 RAG 是什么,就已经卡在 Nacos 注册失败、Gateway 路由不通、Feign 调用超时、Docker 端口冲突上。

这就是为什么 KnowHub 先有 rag-demo-monolith

单体版的目标不是最终交付,而是验证核心业务闭环。

RAG 主链路本身已经包含很多内容:

如果这条链路还没跑通,就急着拆服务,系统复杂度会迅速上升。

所以单体版的作用可以概括成一句话:

用最少的工程复杂度,先证明业务链路能跑通。

这和真实企业开发也很像。很多复杂系统并不是一开始就是微服务,而是先把核心业务跑通,再随着团队规模、业务边界和性能要求逐步拆分。


3.2 rag-demo-monolith 承担的职责

rag-demo-monolith 是一个 Spring Boot 单体项目。

在这个项目里,知识库、文档、任务、向量检索、问答都放在同一个应用中。

它大致包含这些模块:

common        通用返回、异常、配置、枚举
kb            知识库管理
document      文档上传、解析、切片、存储
task          索引任务状态和本地异步执行
vector        pgvector 写入和检索
qa            RAG 问答和问答日志
ai            Chat / Embedding 测试接口
rag           简单 RAG 示例接口

对于学习来说,这种结构有几个优点。

第一,调试简单。所有代码都在一个进程里,打断点、看日志、查数据库都比较直接。

第二,链路短。Controller 调 Service,Service 调 Mapper 或向量服务,不需要先理解网关、注册中心和服务间调用。

第三,适合验证 RAG 思路。你可以专注于文档怎么切片、向量怎么写入、检索结果怎么拼 Prompt,而不是被微服务基础设施干扰。

第四,适合沉淀公共能力。比如 TextChunkerDocumentParserPgVectorUtilsKnowledgeChatPromptBuilder 这些能力,在单体版里先验证,再迁移到微服务版更稳。

但是单体版也有明显边界。


3.3 单体项目的边界

单体项目不是不好。对于小项目、内部工具、快速验证,它非常高效。

但 Knowhub 的目标不是只做一个 Demo,而是做一个接近企业实践的 AI 知识库平台。随着功能增加,单体项目会遇到几个问题。

3.3.1 职责边界变模糊

在单体项目里,认证、知识库、文档、索引任务、问答都在一个应用中。

一开始这没问题,但功能越来越多后,代码边界容易变模糊。比如:

  • 用户登录逻辑和知识库逻辑放在同一个应用中。
  • 文档上传和索引任务执行耦合在一起。
  • 问答接口和向量写入共用大量服务。
  • 管理端接口和用户端接口混在同一个项目里。

当你想改某个模块时,容易影响其他模块。

3.3.2 用户身份不够工程化

单体版可以通过请求参数传 userId 来模拟用户隔离。

比如:

GET /kb/list?userId=1

这适合学习阶段,但不适合真实系统。

真实系统应该由登录产生 Token,由 Gateway 统一校验,再把用户身份透传给下游服务。前端不能随便传一个 userId 就访问数据,否则很容易越权。

这也是微服务版必须引入 Auth 和 Gateway 的原因。

3.3.3 耗时任务需要独立治理

文档索引是典型耗时任务。

解析 PDF、文本切片、调用 Embedding、写入向量库,这些步骤都可能失败,也可能耗时很长。

如果这些逻辑一直和知识库接口放在一个应用里,后续很难独立扩展、独立排查和独立重试。

企业级系统更合理的做法是:

knowledge-service 负责上传和创建任务
task-service 负责执行索引任务
RabbitMQ 负责异步消息传递

这样上传接口不会被索引耗时拖住,任务执行失败也可以单独追踪。

3.3.4 文件存储需要适配多服务部署

单体项目里,文件保存在本地目录通常可以正常运行。

但拆成微服务后,情况变了。

如果 knowledge-service 在一台机器上保存文件,而 task-service 在另一台机器上执行索引,task-service 就无法直接读取 knowledge-service 的本地路径。

所以完整架构中需要 MinIO。

MinIO 把文件变成对象存储资源,服务之间只传对象路径,而不是依赖某台机器上的本地目录。


3.4 为什么演进到 Spring Cloud

从单体版演进到 Spring Cloud,不是为了炫技,而是为了让系统边界更清晰。

rag-platform 将后端拆成几个核心模块:

rag-common
rag-gateway-service
rag-auth-service
rag-knowledge-service
rag-task-service

每个模块都有明确职责。

3.4.1 rag-common:公共能力

rag-common 存放多个服务都会用到的内容。

比如:

  • ApiResponse:统一返回结构。
  • BusinessException:业务异常。
  • ErrorCode:错误码。
  • BaseEntity:基础实体字段。
  • JwtTokenService:JWT 生成和解析。
  • UserClaims:Token 中的用户信息。
  • UserRoleUSER / ADMIN 角色。
  • 文档状态、任务状态、知识库状态枚举。

这些代码如果每个服务都复制一份,会很难维护。放到 common 后,所有服务都可以复用同一套语义。

3.4.2 rag-gateway-service:统一入口

Gateway 是所有后端请求的入口。

它负责:

请求路由
JWT 鉴权
白名单放行
用户信息透传
管理员路径校验
CORS 处理

比如用户请求知识库接口时,不直接访问 knowledge-service,而是访问 Gateway:

http://localhost:9000/kb/list

Gateway 校验 Token 后,把请求转发给 knowledge-service,并加上用户信息请求头。

这样下游服务不用重复解析 Token,只需要信任 Gateway 透传的用户上下文。

3.4.3 rag-auth-service:认证服务

Auth 服务专门处理用户身份。

它负责:

  • 注册。
  • 登录。
  • BCrypt 密码加密。
  • JWT 签发。
  • 当前用户查询。
  • 用户状态。
  • 用户角色。

认证服务独立出来后,其他服务不需要关心密码怎么校验、Token 怎么签发,只需要根据 Gateway 透传的身份做业务判断。

3.4.4 rag-knowledge-service:RAG 主业务服务

Knowledge 服务是 KnowHub 的核心业务服务。

它负责:

  • 知识库管理。
  • 文档上传入口。
  • 文档元数据。
  • 文档切片查询。
  • 向量检索。
  • RAG 问答。
  • qa_log 和 qa_reference。
  • Redis owner 缓存。
  • Sentinel 限流。
  • AI 调用降级。

它不应该承担所有后台耗时任务。文档索引执行应该交给 task-service,这样职责更清楚。

3.4.5 rag-task-service:异步任务服务

Task 服务负责索引任务治理。

在完整链路中,它通过 RabbitMQ 接收索引消息,执行解析、切片、向量化和入库。

它关注的问题不是“用户怎么提问”,而是:

  • 任务从 WAITING 到 RUNNING 是否正确。
  • 执行成功后是否更新 SUCCESS。
  • 失败后是否记录原因。
  • 是否允许手动重试。
  • RUNNING 超时是否能标记 TIMEOUT。
  • RabbitMQ 重复投递时是否能幂等处理。

这就是服务拆分的价值:每个服务只专注自己的核心职责。


3.5 单体链路与微服务链路对比

为了更直观地理解拆分,我们以“上传文档”为例。

3.5.1 单体版上传链路

rag-demo-monolith 中,链路大致是:
在这里插入图片描述

所有步骤都在一个 Spring Boot 应用内完成。

好处是简单,坏处是边界不够清晰。

3.5.2 微服务版上传链路

rag-platform 中,完整链路会变成:

`在这里插入图片描述

这条链路看起来更长,但职责更清楚。

  • Gateway 负责入口和鉴权。
  • knowledge-service 负责文档入口和业务校验。
  • MinIO 负责文件存储。
  • RabbitMQ 负责异步解耦。
  • task-service 负责索引执行。
  • Redis 负责幂等和缓存。
  • MySQL 和 pgvector 分别负责业务数据和向量数据。

3.5.3 问答链路对比

单体版问答链路:
在这里插入图片描述

微服务版问答链路:
在这里插入图片描述

微服务版多了 Gateway 和用户隔离,这正是企业系统必须补齐的部分。


3.6 服务之间如何协作

拆成多个服务后,一个关键问题是:服务之间怎么找到彼此,怎么调用彼此?

Knowhub 中主要有三种协作方式。

3.6.1 Gateway 路由

前端只需要记住 Gateway 地址。

比如:

http://localhost:9000

用户访问:

/auth/login
/kb/list
/kb/{kbId}/chat
/admin/index-tasks

Gateway 根据路径把请求转发到对应服务。

这种方式的好处是前端不需要知道每个后端服务的端口,也方便统一鉴权。

3.6.2 Nacos 服务注册与发现

每个服务启动后,会把自己注册到 Nacos。

比如:

rag-auth-service
rag-knowledge-service
rag-task-service
rag-gateway-service

Gateway 或 Feign 调用下游服务时,可以通过服务名找到真实实例。

这比写死 IP 和端口更灵活。以后服务部署到不同机器,或者一个服务启动多个实例,只要注册到 Nacos,调用方就能发现它。

3.6.3 OpenFeign 同步调用

同步调用适合“当前请求需要立即拿到结果”的场景。

比如 Knowledge-Service 需要查询 Task-Service 中某个文档的最新索引任务,就可以通过 OpenFeign 调用。

同步调用的特点是:

调用方等待结果
下游失败会影响当前请求
适合查询类、状态类、轻量操作

3.6.4 RabbitMQ 异步消息

异步消息适合耗时任务。

比如文档索引不需要在上传接口里同步完成。knowledge-service 只需要创建任务并发送消息,task-service 后台消费即可。

异步调用的特点是:

调用方不等待任务完成
任务可以后台执行
失败可以重试
削峰能力更好
需要处理重复消费和消息可靠性

所以,OpenFeign 和 RabbitMQ 不是谁替代谁,而是分工不同。

一句话总结:

需要立即返回结果的,用 Feign;耗时、可后台处理的,用 RabbitMQ。


3.7 Maven 多模块结构

rag-platform 是 Maven 多模块工程。

父工程 pom.xml 负责管理子模块和公共版本。

结构类似:

rag-platform
├── pom.xml
├── rag-common
├── rag-gateway-service
├── rag-auth-service
├── rag-knowledge-service
└── rag-task-service

父工程本身通常不写业务代码,它负责:

  • 统一 Java 版本。
  • 统一 Spring Boot / Spring Cloud 版本。
  • 声明子模块。
  • 管理依赖版本。

子模块负责具体业务。

初学者打开项目时,要从父工程打开,而不是只打开某个子模块。否则 IDEA 可能识别不到模块之间的依赖关系。


3.8 拆分后的收益和代价

微服务不是只有好处,也有代价。

3.8.1 收益

第一,职责更清晰。

认证、网关、知识库、任务分别拆开后,每个服务关注的问题更聚焦。

第二,权限边界更明确。

gateway 统一鉴权,业务服务做资源归属校验,比前端传 userId 更可靠。

第三,任务更容易治理。

文档索引变成后台任务后,可以记录状态、失败原因、重试次数、执行耗时,也可以通过管理端查看。

第四,部署和扩展更灵活。

如果将来问答请求多,可以扩展 knowledge service;如果索引任务多,可以扩展 task service 消费者。

第五,更接近企业项目。

对学习和简历来说,多服务架构能体现更多后端工程能力,比如服务注册、网关、鉴权、异步任务、缓存、降级和运维排查。

3.8.2 代价

第一,启动更复杂。

单体项目启动一个应用就够,微服务至少要启动 Gateway、auth、knowledge、task,还要启动 Nacos、MySQL、Redis、RabbitMQ、MinIO、PostgreSQL。

第二,配置更多。

每个服务都有自己的 application.yml,数据库、Redis、Nacos、模型 API、端口都要配置正确。

第三,排查链路更长。

一个请求失败,可能是 Gateway 拦截、Token 过期、Nacos 未注册、Feign 调用失败、数据库异常、模型接口超时。

第四,数据一致性更难。

文档上传、任务创建、消息投递、索引执行分布在多个组件中,必须通过状态机、日志和重试机制保证最终可追踪。

所以,微服务不是为了简单,而是为了在复杂业务下保持边界清晰。


3.9 常见问题排查

3.9.1 服务没有注册到 Nacos

现象:Gateway 找不到下游服务,Feign 调用失败。

排查顺序:

  1. Nacos 是否启动。
  2. 服务配置中的 Nacos 地址是否正确。
  3. 服务名是否和 Gateway 路由配置一致。
  4. 服务启动日志中是否出现注册成功信息。

3.9.2 Gateway 路由不生效

现象:访问 Gateway 返回 404 或 503。

排查顺序:

  1. 请求路径是否匹配路由规则。
  2. 下游服务是否注册。
  3. Gateway 是否引入负载均衡依赖。
  4. 是否被鉴权过滤器提前拦截。

3.9.3 Token 透传失败

现象:下游服务拿不到用户 ID,或者提示未登录。

排查顺序:

  1. 前端是否携带 Authorization: Bearer xxx
  2. Gateway 是否正确解析 Token。
  3. Gateway 是否写入 X-User-IdX-UsernameX-User-Role
  4. 下游服务拦截器是否读取这些请求头。

3.9.4 Feign 调用失败

现象:knowledge-service 调用 task-service 报错。

排查顺序:

  1. task-service 是否启动。
  2. task-service 是否注册到 Nacos。
  3. FeignClient 的服务名是否正确。
  4. 接口路径、请求方法、参数是否一致。
  5. 超时时间是否太短。

3.9.5 RabbitMQ 消息没有被消费

现象:上传文档后任务一直 WAITING。

排查顺序:

  1. RabbitMQ 是否启动。
  2. exchange、queue、routing key 是否一致。
  3. task-service 消费者是否启动。
  4. 消息是否堆积在队列中。
  5. 消费者是否因为异常一直重试或被关闭。

3.9.6 跨服务文件访问失败

现象:文档上传成功,但索引任务找不到文件。

排查顺序:

  1. 文件是否上传到 MinIO。
  2. 数据库中的 storage_path 是否正确。
  3. task-service 是否能访问 MinIO。
  4. bucket 是否存在。
  5. accessKey、secretKey 是否正确。

如果系统仍依赖本地路径,要特别注意服务是否部署在同一台机器。不同机器之间不能直接读取对方本地磁盘。


本章小结

这一章我们完成了从单体项目到 Spring Cloud 微服务的整体理解。

rag-demo-monolith 的作用是降低复杂度,先跑通 RAG 核心业务链路。它适合学习文档上传、解析、切片、向量检索和问答闭环。

rag-platform 的作用是把已经验证过的核心能力拆成更清晰的企业级结构。Gateway 负责统一入口,auth-service 负责认证,knowledge-service 负责 RAG 主业务,task-service 负责异步索引任务,rag-common 负责公共能力。

微服务带来了职责清晰、权限边界明确、任务治理能力增强等收益,也带来了启动复杂、配置更多、排查链路更长等代价。

从下一章开始,我们将进入认证和权限部分,先讲 JWT 登录认证与 Gateway 统一鉴权。因为在企业知识库中,只有先知道“谁在访问”,后面才能谈“他能访问哪些知识库”。

思考题

  1. 为什么 KnowHub 不建议一开始就直接写 Spring Cloud 多服务?
  2. rag-demo-monolith 在整个学习路线中承担什么作用?
  3. 单体项目中通过请求参数传 userId 有什么风险?
  4. Gateway 为什么适合作为统一鉴权入口?
  5. OpenFeign 和 RabbitMQ 的使用场景有什么区别?
  6. 为什么文档索引任务适合拆到 task-service?
  7. 微服务拆分后,排查问题为什么会比单体项目更复杂?
Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐