背景痛点:业务耦合带来的“慢”与“乱”
去年双十一前,我们老客服系统被流量冲得“七荤八素”:
- 所有业务逻辑塞在一个 WAR 里,改一句“工单状态校验”就要全量回归。
- 扩容时只能整体水平复制,知识库这种 CPU 密集模块抢不到资源,用户服务却空跑。
- 平均响应 1.2 s,P99 飙到 5 s,客服同学一边接电话一边刷新页面,体验感人。
归根到底,业务边界模糊、代码耦合、数据纠缠,让“智能客服”既不智能,也不服务。
技术方案:DDD + 微服务 + 智能路由
1. 子域划分(DDD 战略图)
先拉业务、产品、测试一起“撕逼”出统一语言,把核心域、支撑域、通用域拍在桌上:
- 用户核心域:注册、认证、画像
- 工单核心域:创建、分派、完结、评价
- 知识库支撑域:FAQ、意图词、相似问法
- 通用域:消息、文件、权限
每个域对应一个独立 Spring Boot 应用,包名直接体现边界:cn.mcp.user、cn.mcp.ticket……后面谁越界调用数据库,Code Review 直接打回。
2. 微服务架构全景
- 网关统一鉴权、灰度、流控
- 注册中心用 Nacos,元数据里打标签“cpu_intensive”或“io_intensive”,给后面路由做依据
- 流量从网关→智能路由→下游服务,全链路 WebFlux + Reactor,WebClient 异步调用,阻塞降到最低
3. 智能路由算法(核心代码)
需求:同一句话“我要退货”可能走工单,也可能走售后知识库,要让模型实时决策,且<10 ms。
思路:轻量级 TextCNN + 本地缓存,模型输出=目标服务名,网关根据服务名做直连转发。
/** * 智能路由服务 */ @Slf4j @Service public class SmartRouter { private final LoadingCache<String, String> modelCache = Caffeine.newBuilder() .maximumSize(10_000) .expireAfterWrite(3, TimeUnit.MINUTES) .build(this::predict); /** * 根据用户文本返回目标微服务名称 * @param text 用户原始问题 * @return serviceName 如 ticket-service */ public String route(String text) { return modelCache.get(text); } private String predict(String text) { // TextCNN 推理,省略模型加载细节 return "ticket-service"; } }实现细节:代码落地三板斧
1. 服务注册与 Feign 调用(含熔断)
- 生产者
spring: application: name: ticket-service cloud: nacos: discovery: server-addr: nacos:8848 metadata: pattern: cpu_intensive- 消费者
@FeignClient(name = "ticket-service", fallback = TicketClientFallback.class) public interface TicketClient { /** * 根据工单 ID 获取详情 */ @GetMapping("/tickets/{id}") TicketDTO getTicket(@PathVariable("id") Long id); } @Component public class TicketClientFallback implements TicketClient { @Override public TicketDTO getTicket(Long id) { return TicketDTO.errorOf("服务降级,请稍后重试"); } }2. 业务上下文 UML
- 聚合根:Ticket、User、KBCard
- 值对象:Status、Priority
- 领域事件:TicketCreatedEvent、UserTaggedEvent
3. 事件驱动解耦
Kafka Topic 命名规范:{domain}.{event},如ticket.created。
工单服务产生事件:
@Service public class TicketEventPublisher { @Autowired private KafkaTemplate<String, Object> template; public void publish(TicketCreatedEvent event) { template.send("ticket.created", event.getTicketId().toString(), event); } }用户服务监听,更新用户活跃度:
@KafkaListener(topics = "ticket.created", groupId = "user-stat") public void onTicketCreated(TicketCreatedEvent event) { userStatService.incActive(event.getUserId()); }性能优化:让 40% 不是拍脑袋
1. 基准测试
JMeter 3 台 4C8G 压测机,500 线程循环 10 min:
| 指标 | 老系统 | 新系统 |
|---|---|---|
| TPS | 420 | 610 |
| RT90 | 1.1 s | 0.55 s |
| 错误率 | 2.3% | 0.1% |
2. 线程池调优
Netty 工作线程数 = CPU 核心 × 2;
业务线程池参考公式:
core = n * requests_per_second * average_latency我们实测 4C8G 容器,core=16,max=32,queue=200,拒绝策略 CallerRuns,压测曲线最平稳。
避坑指南:别在坑里反复横跳
1. 分布式事务选型
- Seata:AT 模式对业务无侵入,但全局锁在热点数据上性能掉 30%。
- Saga:编排式,自己补回滚接口,适合长事务,但开发量翻倍。
工单&库存场景用 Seata,用户积分兑换场景用 Saga,按“短事务→Seata,长事务→Saga”口诀基本不翻车。
2. 领域模型 vs 数据模型
别直接把 UserDO 当 User 聚合根,DO 为了连表加字段,领域层却不需要,结果一改字段全编译。
建议:
- 领域层纯 POJO,只包业务方法;
- infrastructure 层做 DO↔POJO 转换,MapStruct 一键生成;
- 仓库接口返回聚合根,禁止外露 DO。
开源代码与生产 Tips
- 代码仓库已放 GitHub(地址略),分支规则:main 跑生产,dev 合并最新,feature/* 个人开发。
- 网关层加 Lua 脚本做 AB,灰度按用户尾号,回滚 1 秒生效。
- 监控用 Prometheus + Grafana,核心看三个黄金指标:延迟、流量、错误。
- 上线前跑一遍 ChaosMesh,把 Pod 随机杀光,验证路由熔断与重试是否生效。
结尾思考:热更新动态业务规则
目前路由模型靠打包上线,如果运营想临时“618 期间‘退货’全部直连人工客服”,还得走发版。
下一步打算引入轻量级规则引擎 + ConfigMap 热刷:
- Drools / Aviator 表达式做规则池;
- Nacos 配置监听,秒级推送;
- 本地缓存版本号,CAS 替换。
感兴趣的小伙伴可以先看看开源项目EasyRules和Spring Cloud Bus,提前撸一遍源码,也许下次分享就轮到你来踩坑报告了。