背景痛点:传统客服系统为什么“慢”
去年做客服系统重构时,老板只丢下一句话:“高峰期排队 30 秒,用户就流失 50%。”
我们把老系统拆开一看,典型“单体+同步”架构的坑一个不落:
- 业务层、数据层、消息层全部挤在一个 WAR 里,线程池打满就整站卡死
- 管理后台用 JSP 直怼数据库,运营同学点一次“导出对话记录”,MySQL CPU 飙到 90%,前台跟着掉线
- 语音留言先落盘再转文本,链路一长,用户说完“你好”要等 3 秒才收到“请问有什么可以帮您”
一句话:性能瓶颈在“同步 IO + 单点计算”,体验瓶颈在“人找数据,不是数据找人”。想提效,先拆同步,再拆单体。
架构设计:让 SpringBoot 与 Vue 各跑各的,却又能对上暗号
前后端分离到底省了什么
- 前端静态资源走 CDN,首屏 1.2 s 降到 0.4 s
- 后端只暴露 JSON,Docker 镜像从 380 MB 瘦到 68 MB,发版 30 秒搞定
- 多端复用——同一套接口既供 Vue-Admin,也供 H5 小程序,少写 30% 代码
微服务切口怎么下
我们把“问答”拆成三块独立服务,方便按需扩容:
- qa-router:负责 NLP 路由,无状态,最耗 GPU,k8s HPA 按 CPU 65% 水平扩容
- qa-history:对话落库 + 检索,IO 密集,绑 SSD,夜间合并小文件
- qa-admin:运营后台,低频但权限复杂,独立部署防止后台操作拖垮前台
消息队列选型:Kafka vs RabbitMQ
指标对比(实测 8C16G,压测 5w qps):
- 吞吐量:Kafka 11.2w qps,RabbitMQ 2.8w qps
- 延迟:Kafka p99 28 ms,RabbitMQ p99 8 ms
- 幂等与事务:RabbitMQ 原生支持,Kafka 0.11 以后支持“幂等 Producer”
结论:对话事件流(量大可重复)走 Kafka;运营指令(量小需可靠)走 RabbitMQ。双队列混跑,谁也别等谁。
核心实现:把“能跑”写成“好跑”
1. JWT 鉴权的 RESTful API
Spring Security 配置片段(SpringBoot 3.2):
@Configuration @EnableMethodSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http .csrf(csrf -> csrf.disable()) .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/admin/**").hasRole("ADMIN") .requestMatchers("/api/public/**").permitAll() // 其余接口需携带 JWT .anyRequest().authenticated()) // 自定义过滤器,统一校验 JWT .addFilterBefore(new JwtFilter(userDetailsService()), UsernamePasswordAuthenticationFilter.class) .build(); } }JwtFilter 核心逻辑:解析 token → 查 Redis 黑名单 → 构造 UsernamePasswordAuthenticationToken → 放入 SecurityContext。全程无 Session,适配集群水平扩容。
2. WebSocket 实时对话 + 心跳
后端采用 STOMP 子协议,方便前端直接订阅/user/{uid}/queue。
关键代码(心跳 30 s 一次,断线 2 次即重连):
@Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableSimpleBroker("/queue", "/topic") .setHeartbeatValue(new long[]{30000, 30000}) // 服务端心跳 .setTaskScheduler(heartBeatScheduler()); }前端 Vue:
const stomp = Stomp.over(new SockJS('/ws')) stomp.heartbeat.outgoing = 30000 stomp.connect(headers, frame => { stomp.subscribe(`/user/${uid}/queue/reply`, msg => { // 渲染 AI 回复 }) })实测 4G 弱网,心跳保活成功率 98%,比裸 WebSocket 高 12%。
3. H5 语音提问的音频流处理
思路:WebRTC 采集 → 浏览器端 PCM 重采样 16 kHz → 分片 200 ms → 通过 WebSocket 二进制帧直传 → 后端转文本(流式 ASR)。
前端采集片段:
navigator.mediaDevices.getUserMedia({audio: true}) .then(stream => { const recorder = new MediaRecorder(stream, { mimeType: 'audio/webm;codecs=opus' }) recorder.ondataavailable = e => { if (e.data.size > 0) ws.send(e.data) } recorder.start(200) // 每 200 ms 触发一次 ondataavailable })后端用 Netty 拆包,攒够 0.8 s 音频就调用一次 ASR,首字延迟从 2.1 s 降到 0.9 s。
性能优化:把“能跑”升级成“抗打”
Nginx 负载均衡要点
- upstream 使用 least_conn,防止长连接倾斜
- keepalive 300 条,减少三次握手
- 开启 http2,多路复用,H5 语音页首包 1.2 s → 0.6 s
Elasticsearch 检索对话历史
- 按“租户+日期”建索引模板,每天滚动,避免单索引过 50 G
- 把“用户原句”“客服回复”放进同一个 nested 字段,查询时用
inner_hits高亮,RT 150 ms 内 - 热数据 7 天走 SSD 节点,冷数据走机械盘,省 40% 硬件预算
压测报告(JMeter 5.6)
场景:模拟 1w 并发长连接,持续 15 min
指标结果:
- 平均响应 120 ms
- p99 480 ms
- 错误率 0.2%(全是心跳超时,可接受)
- 峰值 CPU 68%,内存 12 G,尚有 30% 余量
避坑指南:掉过的坑,写给别人看
跨域问题终极方案
- 网关层统一加
Access-Control-Allow-Credentials:true,让前端带 cookie - 自定义
CorsFilter放最前,防止被其他 Filter 截胡 - 本地联调让 Chrome 跑
—disable-web-security的日子一去不复返
Vuex 状态管理误区
- 把“当前会话列表”放 Vuex,刷新页面就清空,运营同学骂娘——解决:刷新前
beforeunload写 sessionStorage,回来再 hydrate - 用
mapState一把梭,结果组件重渲染 3 次——解决:只 map 需要的最小粒度过滤器,其余用 getter 缓存
SpringBoot 热部署坑
- devtools 与 lombok 1.18.24 以下版本一起用,编译期报“找不到符号”——升级 lombok 即可
- 多模块项目,IDEA 需要给每个子模块都勾选
Build project automatically,否则改 mapper XML 不认
经验小结
- 先拆同步,再拆服务,最后拆数据,顺序反了就是灾难
- 语音流一定要“边采边传边识别”,落盘再读就是延迟元凶
- 压测别只看平均,p99 决定老板心情
开放讨论
如何设计支持百万级并发的智能客服架构?
是继续无状态水平扩容,还是引入边缘节点做流式推理?
欢迎把你的思路留在评论区,一起把“排队 30 秒”干到“排队 0 秒”。