Spring Boot + Python 双栈架构:AI 服务的工程化实践
- 约 8 分钟读完 - 全文 2926 字Spring Boot + Python 双栈架构:AI 服务的工程化实践
我开发的方糖猫 AI 视频平台不是一个纯 Python 项目,也不是一个纯 Java 项目。它同时使用了 Spring Boot 3 和 Python 3.11:Java 负责业务系统和 API 服务,Python 负责 AI 推理、渲染和多媒体处理。这篇文章会分享我们为什么要这样做,以及如何解决两个技术栈之间的协作问题。
一、为什么必须双栈?
做一个 AI 视频生成平台,需要两类能力:
| 能力类型 | 典型需求 | 更适合的栈 |
|---|---|---|
| 企业级业务系统 | 用户、订单、权限、支付、任务管理 | Java / Spring Boot |
| AI 推理与多媒体 | LLM 调用、Whisper、RAG、Remotion、FFmpeg | Python |
Java 的优势在于生态成熟、工程规范、团队熟悉;Python 的优势在于 AI 库丰富、模型推理和多媒体处理工具链完善。
如果全用 Java,AI 库支持差,很多模型没有 Java SDK;如果全用 Python,业务系统的稳定性、事务处理、监控运维又很难做好。
所以我们的选择是:Java 做业务底盘,Python 做 AI 引擎,两者通过清晰的接口协作。
二、整体架构
flowchart TD
FE["前端 (Vue 3)"]
GW["API 网关 / BFF (Spring Boot)<br/>用户认证、请求路由、限流、任务状态查询、SSE 推送"]
BIZ["业务服务<br/>(Spring Boot)"]
SCHED["任务调度服务<br/>(Spring Boot)"]
RES["资源管理服务<br/>(Spring Boot)"]
AI["AI Worker (Python)<br/>ReAct Agent、RAG 检索、Whisper、TTS、Remotion、FFmpeg"]
STORE["存储层:MySQL / Redis / OSS / ChromaDB"]
FE --> GW
GW --> BIZ
GW --> SCHED
GW --> RES
SCHED -- "RocketMQ / Kafka" --> AI
AI --> STORE
三、职责划分
3.1 Java 侧职责
- API 网关:统一入口、鉴权、限流、日志
- 用户与任务管理:用户注册登录、任务创建、任务状态机、历史记录
- 素材元数据管理:素材上传、分类、标签、权限
- 支付与计费(如有):订单、套餐、用量统计
- 任务调度:把渲染/生成任务发送到消息队列
- 实时通知:SSE 推送任务进度给用户
3.2 Python 侧职责
- AI Agent 编排:ReAct 任务规划、Tool 调用
- RAG 检索:Embedding 计算、向量库查询
- 语音识别:Whisper 转写
- 语音合成:TTS 配音
- 视频渲染:Remotion 组件化渲染
- 后处理:FFmpeg 转码、压缩、字幕、多分辨率输出
3.3 边界原则
我们定了几条原则来避免职责混乱:
- Python 不直接暴露给用户。所有用户请求都走 Java API。
- Python 只做无状态计算。任务状态、用户信息、素材元数据都在 Java 侧维护。
- 通信以异步为主。AI 任务通常耗时较长,同步调用容易导致超时和耦合。
- 数据以 ID 为纽带。Java 和 Python 之间不传大对象,只传任务 ID 和必要参数。
四、服务间通信
4.1 异步通信:RocketMQ
AI 生成和渲染任务都走 RocketMQ:
// Java 侧发送任务
SendResult result = rocketMQTemplate.syncSend(
"video-render",
MessageBuilder.withPayload(taskMessage).build()
);# Python 侧消费任务
class RenderConsumer:
def on_message(self, msg):
task = json.loads(msg.body)
result = render_engine.process(task)
self.update_task_status(task["task_id"], "completed", result)
return ConsumeStatus.CONSUME_SUCCESS异步通信的优点:
- 解耦 Java 和 Python 服务
- Python Worker 可以独立扩缩容
- 任务失败可以重试,不阻塞主流程
4.2 同步通信:HTTP / gRPC
少数场景需要同步调用,比如:
- 查询 RAG 素材(用户在前端点击「生成」前希望看到素材推荐)
- 获取任务实时状态
- 健康检查
我们主要用 HTTP REST,因为:
- 简单直观,Java 和 Python 生态都完善
- 调试方便,curl 即可测试
- 对延迟要求不极端
gRPC 我们也考虑过,但因为团队对 gRPC 熟悉度不高,且收益不明显,暂时没引入。
4.3 数据一致性
双栈架构下,任务状态是核心数据。我们用数据库作为唯一真相源:
flowchart TD
A["用户提交任务"] --> B["Java 写入任务记录(status=pending)"]
B --> C["发送 RocketMQ 消息"]
C --> D["Python Worker 消费"]
D --> E["Python 更新任务状态(status=processing/completed/failed)"]
Python Worker 更新数据库时,通过乐观锁防止并发冲突:
UPDATE render_task
SET status = 'processing',
worker_id = 'worker-01',
updated_at = NOW()
WHERE task_id = 'xxx'
AND status = 'pending'五、数据库设计
5.1 按服务分库还是分表?
我们没有完全按微服务分库,而是按业务域分表:
user:用户信息task:生成任务material:素材元数据template:视频模板subscription:订阅/套餐
所有表都在一个 MySQL 实例里,通过业务 ID 关联。这种方案在业务量不大时性价比最高,未来可按域拆分。
5.2 Python 侧如何访问数据库
Python 侧需要读取/更新任务状态,我们用的是 SQLAlchemy + 只读/写分离:
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
engine = create_engine("mysql+pymysql://user:pass@host/db")
Session = sessionmaker(bind=engine)
def update_task_status(task_id: str, status: str):
with Session() as session:
session.execute(
"UPDATE render_task SET status = %s WHERE task_id = %s",
(status, task_id)
)
session.commit()为了降低耦合,Python 侧只更新少量任务状态字段,不碰业务核心表。
六、部署与运维
6.1 容器化
两个栈都用 Docker 打包:
# Java 服务 Dockerfile
FROM eclipse-temurin:17-jdk
COPY target/app.jar /app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]# Python Worker Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "worker.py"]6.2 CI/CD
我们用一个简单的 GitHub Actions / Jenkins 流程:
- 代码提交触发构建
- 分别构建 Java 镜像和 Python 镜像
- 推送到镜像仓库
- 生产环境滚动更新
6.3 日志与链路追踪
双栈环境下,排查问题最大的难点是链路跨语言。我们的做法:
- 所有请求携带
trace_id - Java 和 Python 都把
trace_id打到日志里 - 用 ELK / Loki 做日志聚合,按
trace_id查询全链路
import logging
logger = logging.getLogger(__name__)
logger.info("Start render", extra={"trace_id": trace_id, "task_id": task_id})6.4 监控告警
| 层级 | 监控项 |
|---|---|
| Java 服务 | JVM 内存、GC、接口响应时间、错误率 |
| Python Worker | CPU/GPU 利用率、任务处理速率、队列堆积 |
| 数据库 | 连接数、慢查询 |
| 消息队列 | 堆积量、消费速率 |
七、踩过的坑
7.1 序列化不一致
Java 侧用 Jackson,Python 侧用默认 json,时间格式、枚举值、空值处理不一致,导致接口对接时经常报错。
解决:
- 统一用 ISO 8601 时间格式
- 枚举值用字符串传递
- 制定统一的 JSON Schema,用 Pydantic 和 Java Bean Validation 双向校验
7.2 Python 服务内存泄漏
Python Worker 长时间运行后,内存占用越来越高,尤其是加载大模型后。
解决:
- Worker 处理一定数量任务后自动重启
- 模型用单例模式加载,避免重复初始化
- 显式清理大对象,调用
gc.collect()
7.3 跨语言异常处理
Python 异常信息传到 Java 侧变成一团乱码,前端无法友好展示。
解决:
- 定义统一的错误码体系
- Python 捕获异常后按规范封装错误响应
- Java 侧根据错误码映射为用户友好的提示
7.4 开发环境复杂度
开发时需要同时启动 Java 服务、Python Worker、MySQL、Redis、RocketMQ,新成员上手成本高。
解决:
- 提供
docker-compose.yml一键启动基础设施 - 编写本地开发文档
- 支持 Windows 开发环境降级(Python Worker 用 HTTP 模拟 RocketMQ)
八、双栈架构的收益与代价
8.1 收益
| 方面 | 说明 |
|---|---|
| 团队分工清晰 | Java 工程师做业务,Python 工程师做 AI |
| 技术选型合理 | 每个栈都用最合适的工具 |
| 稳定性高 | 业务系统独立演进,AI 故障不直接影响用户注册/支付 |
| 扩展灵活 | Python Worker 可以根据渲染负载单独扩容 |
8.2 代价
| 方面 | 说明 |
|---|---|
| 运维复杂 | 两套构建、部署、监控体系 |
| 链路排查难 | 跨语言日志追踪需要额外设计 |
| 接口契约维护 | Java 和 Python 之间需要稳定的 API 契约 |
九、什么情况下不要双栈?
虽然双栈适合我们,但不是所有项目都需要:
- 纯业务系统:全 Java 即可
- 纯模型研究/实验:全 Python 即可
- 团队规模小:双栈会增加认知负担
- MVP 早期:先用最熟悉的栈跑通,后期再拆分
我们是在产品验证基本跑通、团队有 5 人以上时才明确拆成双栈。
十、总结
Spring Boot + Python 双栈不是最优架构,而是适合我们团队能力和业务形态的架构。几个关键经验:
- 职责边界要清晰,否则双栈会变成双倍的混乱。
- 异步通信是主力,同步只用于低延迟查询场景。
- 数据库是状态的唯一真相源,避免两端各自维护状态。
- 日志追踪要跨语言打通,否则线上出问题很难排查。
- 不要为双栈而双栈,MVP 阶段先用单栈验证。
如果你也在做 Java + Python 的混合架构,欢迎交流接口契约、部署和监控的经验。