背景痛点:流量洪峰下的“雪崩”现场
去年双十一,我们给某头部电商做的 AI 客服在 0 点 30 分迎来 3.2 万并发,结果:
- 消息在 RocketMQ 里堆积 47 万条,消费者 Lag 最高 9 min,用户端“已读不回”。
- 会话状态放在本地 HashMap,4 台实例负载不均,用户刷新页面后机器人“失忆”,重复问候。
- 后端 NLP 模型每次冷启动要 6 s,线程被阻塞,Tomcat 最大线程 200 全打满,CPU 飙到 95%,触发连锁超时。
痛定思痛,我们决定用 Java 技术栈彻底重构,目标只有一个:2000 TPS 下 99 线 600 ms,故障率 < 0.1%。
技术选型:为什么放弃 gRPC 和 RESTful
| 协议 | 头部开销 | 双工 | 穿透防火墙 | 状态推送 | 改造成本 |
|---|---|---|---|---|---|
| RESTful | 大 | 无 | 易 | 轮询 | 低 |
| gRPC | 小 | 有 | 难 | 流式 | 中 |
| WebSocket | 极小 | 有 | 易 | 实时 | 低 |
- 客服场景对“实时”极度敏感,轮询延迟不可接受。
- gRPC 的 HTTP/2 虽然多路复用,但 Nginx 七层转发需要额外做
grpc_pass,运维同事强烈反对。 - WebSocket + STOMP 帧头只有 2 B,Spring 直接提供
@MessageMapping,前后端一把梭,最终敲定。
架构总览
graph TD A[用户] -->|WSS| B(Nginx-4 worker, ip_hash) B --> C[Gateway(Spring Cloud Gateway)] C --> D[客服实例-1...n(Spring Boot + WebSocket)] D --> E(Redis-Cluster: 3-Master-3-Slave) D --> F(RocketMQ-2×Broker) D --> G(NLP-推理节点-2×GPU) E --> H[MySQL 主从] H --> I[ES 知识库]核心实现
1. 对话流程不再“写死 if-else”——Spring StateMachine
状态枚举:
public enum ChatState { INIT, AWAIT_INPUT, AWAIT_NLP, REPLY_OK, TIMEOUT, END }事件枚举:
public enum ChatEvent { USER_MSG, NLP_OK, NLP_FAIL, TIME_OUT, AGENT_JOIN }配置:
@Configuration @EnableStateMachineFactory public class ChatStateMachineConfig extends StateMachineConfigurerAdapter<ChatState, ChatEvent> { @Override public void configure(StateMachineStateConfigurer<ChatState, ChatEvent> states) throws Exception { states.withStates() .initial(ChatState.INIT) .state(ChatState.AWAIT_INPUT) .state(ChatState.AWAIT_NLP) .state(ChatState.REPLY_OK) .end(ChatState.END) .and() .withStates() .parent(ChatState.AWAIT_NLP) .initial(ChatState.AWAIT_NLP) .state(ChatState.AWAIT_NLP); } @Override public void configure(StateMachineTransitionConfigurer<ChatState, ChatEvent> transitions) throws Exception { transitions .withExternal().source(ChatState.INIT).target(ChatState.AWAIT_INPUT).event(ChatEvent.USER_MSG) .and() .withExternal().source(ChatState.AWAIT_INPUT).target(ChatState.AWAIT_NLP).event(ChatEvent.USER_MSG) .and() .withExternal().source(ChatState.AWAIT_NLP).target(ChatState.REPLY_OK).event(ChatEvent.NLP_OK) .and() .withExternal().source(ChatState.AWAIT_NLP).target(ChatState.AWAIT_INPUT).event(ChatEvent.NLP_FAIL) .and() .withExternal().source(ChatState.REPLY_OK).target(ChatState.AWAIT_INPUT).event(ChatEvent.USER_MSG) .and() .withExternal().source(ChatState.AWAIT_INPUT).target(ChatState.END).event(ChatEvent.AGENT_JOIN); } }业务代码只关心状态变更,彻底解耦。
2. 会话上下文统一管理——Redis + Protobuf
@Data @Builder @RedisHash(value = "ctx", timeToLive = 1800 ) public class DialogContext implements Serializable { private static final long serialVersionUID = 1L; private String sessionId; private Long userId; private ChatState state; private List<Utterance> history; private Map<String, Object> slots; }- 30 min 过期,节省内存。
- Protobuf 序列化后平均 0.8 KB,比 JSON 省 40%。
3. 分布式锁——Redisson 避坑
RLock lock = redissonClient.getFairLock("chat:lock:" + sessionId); boolean locked = false; try { locked = lock.tryLock(3, 10, TimeUnit.SECONDS); if (!locked) { throw new BizException("系统繁忙,请稍后重试"); 成熟度 99.9% 的客服系统,Java 也能玩得转。 } // 执行业务 } catch (InterruptedException e) { Thread.currentArtifactThread().interrupt(); } finally { if (locked && lock.isHeldByCurrentThread()) { lock.unlock(); } }- 公平锁防止“线程饥饿”。
isHeldByCurrentThread防止误释放。
4. 熔断降级——Sentinel 令牌桶
spring: cloud: sentinel: transport: dashboard: localhost:8080 datasource: ds: nacos: server-addr: nacos:8848 >ThreadPoolExecutor executor = new ThreadPoolExecutor( 200, 300, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(54000), new NamedThreadFactory("chat-nlp"), new ThreadPoolExecutor.CallerRunsPolicy() );2. JMeter 压测结果
- 4 C8 G × 10 容器,2000 TPS 持续 30 min。
- 99 RT 580 ms,CPU 68%,内存 4.2 G。
- 异常率 0.05%,全部来自主动熔断,无雪崩。
避坑指南
- NLP 冷启动 6 s → 预加载 + 本地缓存
启动阶段顺序:- 加载词典到 GPU 显存。
- 预热 10 条虚拟句子,JIT 编译后平均耗时降到 280 ms。2. WebSocket 心跳
前端每 25 s 发ping,Nginxproxy_read_timeout 35s,避免 60 s 默认断链。
- Redisson 解锁必须判空,否则在
tryLock失败时把别人锁解掉。 - RocketMQ 消费组重平衡,拉取队列数 < 消费者实例时,部分实例空闲,需做好分片键。
延伸思考:把知识库搬进 Elasticsearch
- 将历史问答对清洗后写入 ES,字段:
question.keyword、answer、category、hot。 - 用户问题先走 ES
more_like_this召回 Top5,再送 NLP 做语义精排,命中率提升 12%。 - 通过 Function Score 把“热点问题”加权,普通客服直接返回,无需 GPU 推理,RT 降到 80 ms。
写在最后
整套方案上线三个月,经历了两次大促,目前稳定跑在 15 台容器上。最深刻的体会是:高并发场景下,锁一定要“快进快出”,状态机一定要“可视化”,限流一定要“提前一步”。如果你也在用 Java 搭智能客服,不妨把 StateMachine + Redisson 这套组合跑一遍,相信你会少踩很多坑。