微服务架构设计实战:从单体到分布式,我踩过的那些坑
引言:一场重构引发的“血案”

2019年那会儿,我在一家做在线教育平台的中型互联网公司工作。整个后台系统是一个典型的单体应用,Java + Spring MVC + MyBatis + MySQL 的老三样,部署在两台云服务器上。随着业务增长,团队扩张,这个原本还算稳定的系统开始变得越来越难维护。
最明显的表现就是:每次上线都像打仗。一个小小的改动可能牵一发而动全身;接口响应时间越来越长,数据库压力也越来越大;不同模块之间的代码互相调用混乱,改个 bug 一不小心就会带崩别的功能。
我们意识到,是时候搞点大动作了——启动微服务改造计划。
项目背景与挑战

我们决定将原有的单体应用拆分成几个核心服务:
- 用户中心(User Service)
- 课程中心(Course Service)
- 订单中心(Order Service)
- 支付中心(Payment Service)
- 内容中心(Content Service)
这些服务之间有复杂的调用关系,特别是在下单流程中,需要跨多个服务协作完成。同时,用户数和并发量也在持续攀升,每天活跃用户已经超过百万级,QPS 超过万级。
当时遇到的主要问题包括:
- 服务拆分边界不清晰:一开始不知道该按照什么维度去拆分,导致初期几次拆分后又不得不合并。
- 跨服务事务一致性难处理:比如下单成功后要扣减库存、生成订单、更新用户积分等,如何保证数据一致性是个大难题。
- 性能瓶颈频现:服务间通信频繁,RPC 性能差;数据库连接池配置不合理,经常出现数据库爆掉的情况。
- 运维复杂度陡增:原来就几台服务器,现在每个服务都要单独部署,日志分散、监控缺失,出了问题很难定位。
解决方案:技术选型与架构设计

技术栈选择
我们选择了如下这套主流但轻量的技术组合:
| 组件 | 工具/框架 |
|---|---|
| 开发语言 | Java 8 |
| 服务框架 | Spring Boot + Spring Cloud Alibaba (Nacos + Dubbo) |
| 注册中心 | Nacos |
| 配置中心 | Nacos Config |
| 网关 | Gateway + JWT |
| 数据库 | MySQL 5.7 + Druid 连接池 |
| 分布式事务 | Seata(AT模式) |
| 日志监控 | ELK + SkyWalking |
| 容器化 | Docker + Kubernetes(自建) |
之所以没有完全采用 Spring Cloud Netflix 的组件,是因为当时 Netflix 的很多组件已经进入停更状态,Nacos 和 Dubbo 由阿里巴巴维护,在国内生态相对成熟,社区活跃。
架构图大致如下:
[Gateway]
|
-------------------------------
| | |
[User] [Course] [Order]
| | |
[MySQL] [MySQL] [MySQL]
我们采用了 API 网关统一处理鉴权、限流、路由等功能;服务间调用使用 Dubbo 协议,走 RPC;并通过 Seata 来管理分布式事务。
拆分策略
我们最终采取的是领域驱动设计(DDD)的方式进行服务划分。举个例子,在“下单”场景中,我们识别出以下几个领域模型:
- 用户(属于用户域)
- 商品信息(属于课程域)
- 订单(属于订单域)
- 库存(属于商品域的一个子概念)
然后,根据这些模型划分为不同的服务,并确保每个服务内部高内聚、服务之间低耦合。
不过,刚开始我们也犯了个错误,把所有东西都强行按业务模块拆开,结果发现有些数据逻辑非常耦合,比如用户行为数据和内容推荐混在一起,最后不得不重新合并。
所以后来我们形成了一个经验:先以最小可行单元为起点拆分,逐步迭代,不要追求一次性完美拆解。
代码实践:关键实现片段

服务注册与发现(Nacos + Dubbo)
// provider端服务暴露
@Service
public class UserServiceImpl implements UserService {
// ...
}
// consumer端调用
@Reference
private UserService userService;
application.yml 中的 Dubbo 配置:
dubbo:
application:
name: user-service
registry:
address: nacos://nacos-host:8848
接口幂等性设计(用于支付防重提交)
在支付中心,我们加了一个简单的幂等校验层,使用 Redis 缓存请求标识符(requestId):
@Override
public PayResponse pay(PayRequest request) {
String redisKey = "pay_req_" + request.getRequestId();
if (redisTemplate.hasKey(redisKey)) {
log.warn("重复请求:{}", request.getRequestId());
return new PayResponse().setCode(400).setMessage("请勿重复提交");
}
try {
// 执行实际支付逻辑
doPay(request);
// 设置5分钟有效期
redisTemplate.opsForValue().set(redisKey, "1", 5, TimeUnit.MINUTES);
} catch (Exception e) {
// ...
}
return new PayResponse().setCode(200).setMessage("success");
}
这种设计虽然简单,但在支付、退款等对重复提交敏感的场景中非常有效。
分布式事务(Seata)
我们在下单服务中引入了全局事务:
@GlobalTransactional
@Override
public Order createOrder(CreateOrderRequest request) {
// 调用用户服务减少余额
userAccountService.reduceBalance(userId, amount);
// 调用课程服务扣减库存
courseService.reduceStock(courseId);
// 创建订单记录
orderRepository.save(order);
return order;
}
通过 @GlobalTransactional 注解,让 Seata 自动处理 XA 或 AT 模式的事务协调。
踩坑经验:那些年我被坑哭的地方
坑一:线程池配置不当导致雪崩效应
有一次在促销期间,突然网关全部挂掉。排查下来发现问题出在一个 Dubbo 线程池配置错了。
默认情况下 Dubbo 使用的线程池是 fixed 类型,大小只有 200。当某个下游服务响应缓慢时,所有线程都被阻塞,上游服务也被拖死。
解决办法:我们后来统一改为异步调用,并且使用隔离的资源池(如 Hystrix 或 Sentinel)。同时限制每个接口的最大并发数,避免“坏服务拖垮整个链路”。
坑二:日志散乱导致问题难以复现
微服务一多,日志就分散在各个节点上。一旦线上报错,光找日志就要花半小时。
后来我们上了 SkyWalking + ELK,实现了全链路追踪 + 集中式日志聚合。
例如,使用 MDC 实现日志上下文传递:
String traceId = UUID.randomUUID().toString();
MDC.put("traceId", traceId);
// 下游服务接收到请求头后继续设置
String receivedTraceId = request.getHeader("X-Trace-ID");
if (StringUtils.isNotBlank(receivedTraceId)) {
MDC.put("traceId", receivedTraceId);
}
这样就能在日志里看到整个调用链路了。
坑三:数据库连接池配置不合理
刚拆分的时候为了快速上线,直接复制了原来的 spring.datasource 配置,结果每秒钟几千次的访问直接把数据库打爆,CPU飙到了 100%。
后来我们做了几个优化:
- 将 Druid 替换为 HikariCP(性能更好)
- 合理控制最大连接数(一般不超过 CPU 核心数 × 2)
- 对慢查询加上索引,必要时使用缓存(Redis、Caffeine)
效果总结:架构升级带来的收益
经过大约半年的迭代重构,我们最终完成了大部分服务的微服务化。效果非常明显:
- 上线频率提升:过去一周最多上线一次,现在每天都可以灰度发布
- 故障隔离增强:某一服务异常不再影响其他服务
- 扩展更灵活:高峰时期可以单独扩容某几个热点服务
- 团队协作更顺畅:不同小组负责不同服务,职责明确
而且,最重要的是——我们终于可以在生产环境淡定吃鸡腿,不用再提心吊胆盯着报警短信了。
经验分享:给同行兄弟们的几点建议
1. 不要为了拆分而拆分
微服务不是银弹,也不是灵丹妙药。如果你的系统还没有到非拆不可的程度,那完全可以继续单体架构。毕竟,微服务带来的是更高的开发成本、运维复杂度和调试难度。
2. 做好服务治理和可观测性建设
- 上监控(Prometheus+Grafana)
- 上链路追踪(SkyWalking 或 Zipkin)
- 上限流熔断(Sentinel or Hystrix)
- 上日志聚合(ELK)
否则你会陷入一种状态:“我知道有问题,但不知道问题在哪。”
3. 设计要面向失败
微服务环境下网络是不可靠的,服务随时可能宕机或超时。你的接口设计、调用方式、重试机制、降级策略都要围绕这个前提来考虑。
4. 团队协同必须跟上
微服务不仅仅是技术上的变化,更是组织结构和协作流程的变化。如果没有良好的沟通机制、文档体系和 DevOps 流程,拆完以后反而更容易扯皮打架。
写在最后:技术人要有“架构敬畏感”
这几年干下来最大的感触就是——架构真的不能拍脑袋决定。一个决策可能影响几个月甚至几年的开发效率和维护成本。
微服务是一场“持久战”,它不是简单的技术替换,而是整个思维方式和工程能力的跃迁。
希望这篇文章能帮你少走一些弯路,或者至少在面对抉择时,心里有个底。
如果你也正在经历微服务转型,欢迎留言交流,咱们一起成长!

评论 0