微服务架构设计实战:从单体到分布式,我踩过的那些坑

码上见山
2025-06-19 06:23
阅读 275

引言:一场重构引发的“血案”

引言:一场重构引发的“血案”

2019年那会儿,我在一家做在线教育平台的中型互联网公司工作。整个后台系统是一个典型的单体应用,Java + Spring MVC + MyBatis + MySQL 的老三样,部署在两台云服务器上。随着业务增长,团队扩张,这个原本还算稳定的系统开始变得越来越难维护。

最明显的表现就是:每次上线都像打仗。一个小小的改动可能牵一发而动全身;接口响应时间越来越长,数据库压力也越来越大;不同模块之间的代码互相调用混乱,改个 bug 一不小心就会带崩别的功能。

我们意识到,是时候搞点大动作了——启动微服务改造计划


项目背景与挑战

项目背景与挑战

我们决定将原有的单体应用拆分成几个核心服务:

  • 用户中心(User Service)
  • 课程中心(Course Service)
  • 订单中心(Order Service)
  • 支付中心(Payment Service)
  • 内容中心(Content Service)

这些服务之间有复杂的调用关系,特别是在下单流程中,需要跨多个服务协作完成。同时,用户数和并发量也在持续攀升,每天活跃用户已经超过百万级,QPS 超过万级。

当时遇到的主要问题包括:

  1. 服务拆分边界不清晰:一开始不知道该按照什么维度去拆分,导致初期几次拆分后又不得不合并。
  2. 跨服务事务一致性难处理:比如下单成功后要扣减库存、生成订单、更新用户积分等,如何保证数据一致性是个大难题。
  3. 性能瓶颈频现:服务间通信频繁,RPC 性能差;数据库连接池配置不合理,经常出现数据库爆掉的情况。
  4. 运维复杂度陡增:原来就几台服务器,现在每个服务都要单独部署,日志分散、监控缺失,出了问题很难定位。

解决方案:技术选型与架构设计

解决方案:技术选型与架构设计

技术栈选择

我们选择了如下这套主流但轻量的技术组合:

组件 工具/框架
开发语言 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

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝