优化围绕一个目标:让「选题→大纲→初稿→润色」这条多阶段生成链路在并发下不卡、不多烧钱。落地的优化分七块——JVM 与容器参数、Tomcat 线程池、HikariCP 连接池、Redis 缓存、SSE 流式通道、接口压缩、前端首屏。其中收益最明显的是线程池隔离和 SSE 心跳策略,这两处是 AI 流式项目特有的优化点,普通 CRUD 项目的优化经验覆盖不到。下面按投入产出比从高到低讲。
一、先建基准再动手
优化前先量数据。给后端接 Actuator + Micrometer,把 Prometheus 端点暴露出来,压测工具用 k6 模拟真实并发。记录三个基线指标:P99 响应时间、线程池活跃数、数据库连接池水位。没有基线就动手调参,等于盲调,改完也不知道是变好还是变差。建立基准分三步:
- 接入 Actuator 与 Prometheus,暴露
health、metrics、prometheus三个端点; - 用 k6 脚本压出并发从 10 到 100 的响应时间曲线,记录 P99 与错误率;
- 跑一轮 10 分钟稳定性测试,观察线程池、连接池、内存三个指标是否线性增长。
| 优化项 | 改前问题 | 改后效果 |
|---|---|---|
| 线程池隔离 | 生成任务把 Tomcat 工作线程占满 | 生成走独立线程池,普通接口不排队 |
| HikariCP 调参 | 默认 10 连接,并发一上来就排队 | 稳定在 20 连接,无等待 |
| Redis 缓存 | 热门选题反复查库 | 命中率 85%,数据库压力降一半 |
| SSE 心跳 | 长连接被中间层掐断,反复重连 | 连接稳定,重连率下降 |
| Gzip 压缩 | 文章列表 JSON 大,弱网慢 | 传输体积降约 70% |
| 前端路由懒加载 | 首屏 bundle 3MB | 首屏 JS 拆包后小于 1MB |
二、线程池隔离:AI 项目最值得做的一步
文章生成的每个阶段都要调大模型 API,一次完整生成耗时 60 到 120 秒。如果直接在 Controller 线程里同步等待,100 个并发生成任务就把 Tomcat 默认的 200 个线程吃满,普通接口全部 503。做法是把生成任务扔进独立的线程池,Controller 立即返回任务 ID,前端用 SSE 订阅进度。
@Configuration
public class AsyncConfig {
@Bean("genExecutor")
public ThreadPoolTaskExecutor genExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(16);
executor.setQueueCapacity(200);
executor.setThreadNamePrefix("gen-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
executor.initialize();
return executor;
}
}
AbortPolicy 是刻意的:生成任务队列满就快速失败返回提示,而不是无限排队把内存拖垮。队列上限 200 意味着同时最多 216 个生成任务在跑,超过就提示”生成服务繁忙”。
2.1 企业级 AI 网关的同款思路
网关项目里,多模型对话同样走独立线程池,但按模型分组隔离——大模型的调用参数(并发上限、超时)各不相同,一个池里混跑会互相拖累。用两个 ThreadPoolTaskExecutor,一个管流式对话,一个管批量任务,模型路由层只做分发不做等待。
三、连接池:HikariCP 别用默认值
数据库连接池默认 10 个连接,文章创作是写多读少的场景,选题库查询、文章落库、任务状态更新都走 MySQL。并发一上来,10 个连接很快耗尽,线程排队等连接。按机器配置调成 20,并加泄漏检测:
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 3000
leak-detection-threshold: 5000
max-lifetime: 1800000
connection-timeout: 3000 让拿不到连接的请求快速失败,而不是无限等待;leak-detection-threshold 在连接泄漏时能打出告警日志。
四、Redis 缓存与限流
热门选题和已发布文章列表是典型的读多数据,用 Spring Cache 缓存在 Redis 里:
@Cacheable(cacheNames = "hot-topics", key = "#date")
public List<Topic> hotTopics(String date) {
return topicMapper.selectHotByDate(date);
}
生成任务的进度也放 Redis,SSE 断线重连后能找回上次的状态。限流在网关层做:按用户维度对生成接口限流,防止刷接口把模型调用额度烧光。
五、SSE 通道的两个优化
长连接是 AI 项目的心跳所在,两个细节直接影响体验。
5.1 心跳保活
后端每 15 秒推一条 event: heartbeat,防止 Nginx、负载均衡等中间层因空闲超时掐断连接。配合前端 EventSource 的自动重连,断线后 3 秒内重连。
5.2 按阶段限流输出
多阶段生成里,初稿阶段 token 密集、选题阶段 token 稀疏。后端在稀疏阶段主动放慢心跳频率,减少无意义流量;前端根据 stage 事件切换 UI,不因为一段时间没收到正文就误判超时。
@Scheduled(fixedDelay = 15000)
public void heartbeat() {
for (SseEmitter emitter : activeEmitters) {
try {
emitter.send(SseEmitter.event().name("heartbeat").data(System.currentTimeMillis()));
} catch (IOException e) {
activeEmitters.remove(emitter); // 连接已断,清理
}
}
}
六、传输与前端优化
6.1 Gzip 与静态缓存
后端开启响应压缩,前端静态资源由 Nginx 压缩并强缓存带 hash 的文件:
server:
compression:
enabled: true
mime-types: application/json,text/html,application/javascript
6.2 路由懒加载
前端把每个页面拆成独立 chunk,首屏只加载入口需要的代码。改完 bundle 从 3MB 降到 1MB 以内,白屏时间明显缩短。
七、一个完整例子:初稿阶段的并发优化
把上面几块串起来看一个具体场景。初稿生成阶段需要调用大模型生成 2000 字正文,耗时最久。优化前:同步调用占满 Tomcat 线程,80 个并发时 P99 到 8 秒,普通接口超时率 15%。优化后:生成任务进独立线程池,数据库连接池扩容到 20,生成结果分页落库(每次写 500 字,避免长事务锁表),接口 P99 降到 1.2 秒,超时率归零。整个改动只涉及配置和一处代码拆分,没有动生成逻辑本身。
常见问题(FAQ)
Q1:线程池队列设多大合适?
按峰值并发与单任务耗时的乘积估算,再留余量。生成任务 90 秒、峰值 2 并发/秒,队列 200 够用。
Q2:HikariCP 连接数越大越好吗?
不是。连接数超过数据库能并发的上限反而浪费,且连接太多拖慢数据库。按压测找到拐点。
Q3:SSE 心跳频率多少合适?
低于路径上所有中间层空闲超时即可,一般 10 到 15 秒一次,太频繁反而增加无谓流量。