当一个项目同时需要"稳定的业务层"和"高性能的 AI 编排层"时,单一语言往往不够用。本文分享"领航英语"如何用 Spring Boot + Go 构建双服务架构,以及其中的通信、部署和权衡。
为什么拆成两个服务
项目初期是 Spring Boot 单体,所有逻辑在一个进程里。当 AI 功能加入后,问题出现了:
1. 并发模型冲突
Spring Boot 的 Tomcat 线程池处理 HTTP 请求,每个请求占用一个线程。AI 调用(特别是流式 SSE)可能持续数十秒,长时间占用线程池资源。而 Go 的 goroutine 处理长连接几乎零成本。
2. 部署节奏不同
业务层(用户、会员、订单)变更频率低,要求极高稳定性。AI 层(Prompt 优化、模型切换、RAG 策略)迭代非常快,有时一天发好几版。放在同一个服务里,发版互相阻塞。
3. 语言能力匹配
Java 生态在 ORM、事务、权限框架上成熟可靠。Go 在并发、流式处理、对 LLM SDK 的支持上更灵活。用各自擅长的语言做各自擅长的事。
架构总览
浏览器 / 移动端
│
▼
Nginx (反向代理 + SSL)
│
├── /api/* ──→ Spring Boot (8080) ──→ PostgreSQL
│ 用户/词库/会员/订单 Redis
│ │
│ 内网 REST 调用
│ │
└── (前端直连 AI 服务的 SSE 流量也经 Nginx 代理)
│
▼
Go AI 服务 (8090) ──→ LLM API
AI 编排/流式/ Qdrant
Prompt 管理
关键设计决策:前端不直连 AI 服务。所有请求先到 Spring Boot,由它决定哪些需要转发给 Go 服务。这样 AI 服务不需要关心认证鉴权,完全内网通信。
服务间通信
场景一:普通 AI 请求(非流式)
前端 → Spring Boot Controller → AiGatewayService → RestClient → Go 服务 → 返回 JSON
Spring Boot 侧用 RestClient(Spring 6 新 API,替代 RestTemplate):
java
@Service
public class AiGatewayService {
private final RestClient restClient;
public AiGatewayService(@Value("${ai-service.url}") String aiServiceUrl) {
this.restClient = RestClient.create(aiServiceUrl);
}
public VocabExplainResponse explainVocab(VocabExplainRequest request) {
return restClient.post()
.uri("/v1/explain-vocab")
.body(request)
.retrieve()
.body(VocabExplainResponse.class);
}
}
场景二:流式 SSE 请求
SSE 不能简单转发------Spring Boot 需要把 Go 返回的流"透传"给前端:
java
@PostMapping("/explain-vocab/stream")
public Flux<ServerSentEvent<String>> explainVocabStream(@RequestBody VocabExplainRequest req) {
// 校验会员
membershipService.requireActive(userId);
// 转发到 Go 服务
return webClient.post()
.uri(aiServiceUrl + "/v1/explain-vocab/stream")
.bodyValue(req)
.retrieve()
.bodyToFlux(String.class)
.map(data -> ServerSentEvent.<String>builder()
.data(data)
.build());
}
用 Spring WebFlux 的 Flux 做响应式透传,不阻塞 Tomcat 线程。
Go 服务的多模型路由
Go 侧的核心价值之一是多 LLM 供应商路由。根据配置和可用性自动选择:
go
type LLMClient struct {
providers map[string]Provider
fallback []string // 降级顺序
}
func (c *LLMClient) ChatCompletionStream(ctx context.Context, req Request) (<-chan Chunk, error) {
for _, name := range c.fallback {
provider := c.providers[name]
if !provider.IsAvailable(ctx) {
continue
}
stream, err := provider.Stream(ctx, req)
if err == nil {
return stream, nil
}
log.Warn("provider %s failed, trying next", name)
}
// 所有真模型都不可用时,使用 mock 降级
return c.mockStream(req)
}
支持的供应商:DeepSeek、百炼(阿里通义千问)、Clawvard(Anthropic 兼容)。配置方式:
yaml
# ai-service config
llm:
providers:
- name: deepseek
api_key: ${DEEPSEEK_API_KEY}
base_url: https://api.deepseek.com
priority: 1
- name: bailian
api_key: ${BAILIAN_API_KEY}
base_url: https://dashscope.aliyuncs.com
priority: 2
mock_fallback: true
没有配置任何 API Key 时,mock 模式模拟流式输出,方便本地开发。
部署:Docker Compose 一体化
8 个容器,一个 compose 文件:
yaml
services:
postgres: image: postgres:16
redis: image: redis:7.2
qdrant: image: qdrant/qdrant:v1.12.4
backend: build: ./backend # Spring Boot
ai-service: build: ./ai-service # Go
pc-web: build: ./frontend/pc # Vue 3 PC 端
mobile-web: build: ./frontend/mobile # Vue 3 移动端
admin-web: build: ./frontend/admin # Vue 3 运营端
Nginx 在最外层做域名路由:m.dobell.top → mobile-web,pc.dobell.top → pc-web,admin.dobell.top → admin-web,/api/ 路由到 backend。
权衡与教训
好处:
- AI 服务可以独立扩缩容(需要时多起几个实例)
- 两种语言各司其职,团队分工清晰
- AI 服务崩溃不影响用户登录、订单等核心业务
代价:
- 增加了一次网络调用的延迟(内网约 2-5ms,可接受)
- 两套日志、两套部署、两套监控
- 开发环境需要同时启动两个服务(但 docker compose 一键搞定)
什么时候不该拆:如果 AI 调用只是简单的 API 包装(没有复杂的 prompt 编排和流式处理需求),放在 Spring Boot 里完全够用。为了拆而拆只会增加复杂度。
以上是领航英语(m.dobell.top)的后端架构实践。前端是 Vue 3 卡片滑动交互,后端是 Spring Boot + Go 双服务,覆盖自考、学位、高考、四六级等 10 个英语考试场景。3 天免费试用,月卡 29 元。