微服务架构设计实战:从单体到分布式——一次真实的重构之旅

调皮猴
2025-06-29 21:03
阅读 667

开篇:为什么要写这篇文章?

开篇:为什么要写这篇文章?

我是某一线互联网公司的一名后端开发工程师,入行已经五年多了。最近两年,我主要负责一个电商平台核心系统的架构优化和拆分工作。这个系统最开始是一个典型的单体架构,随着业务量的增长、功能模块的膨胀,整个项目代码库越来越大,部署周期越来越长,团队协作也越来越困难。

于是我们决定尝试用微服务的方式对它进行重构。这条路走得并不顺利,踩过很多坑,也积累了不少经验。今天我想以第一人称的视角,聊聊我们是如何一步一步把一个单体应用拆成多个微服务的,期间遇到了哪些挑战,又是怎么解决的。

希望这篇真实经历分享能给正在面临类似问题的朋友一些启发和参考。


背景介绍:一场“技术债爆发”引发的架构升级

背景介绍:一场“技术债爆发”引发的架构升级

原始项目情况:

我们这套平台最初是基于 Java 技术栈搭建的 Spring Boot + MyBatis 项目,所有功能都集中在同一个应用里,包括商品管理、订单处理、用户中心、优惠券、库存等多个模块。数据库方面也是统一的一个 MySQL 实例,表结构复杂、关联度高。

主要痛点包括:

  • 发布频繁冲突:不同模块在同一个代码仓库中,每次上线都要全量打包、部署,很容易出现冲突;
  • 性能瓶颈明显:高峰期一来,CPU飙升,接口响应时间明显变长;
  • 技术栈受限:所有模块强耦合,难以尝试新的框架或中间件;
  • 团队协作成本高:几十人的团队共用一个 Git 分支,review 和测试流程拖慢了上线节奏。

于是我们决定启动一次大规模的架构改造,目标是将各个模块拆分为独立的微服务,实现功能解耦、弹性扩展和服务治理。


拆分初期遇到的问题和挑战

第一个挑战:如何划分服务边界?

刚一开始,我们几个开发骨干坐下来讨论,应该怎么切分服务?这时候才发现问题比想象中复杂得多。

比如“订单”和“库存”这两个模块之间有强关联(下单扣减库存),如果强行切割,会不会导致跨服务调用增多,反而影响性能和一致性?

我们当时的思路是先从业务边界出发,结合领域驱动设计(DDD)的思想,把职责相对明确的模块先拆出来。例如:

  • 用户信息 → user-service
  • 商品信息 → product-service
  • 订单中心 → order-service
  • 支付结算 → payment-service
  • 消息推送 → notification-service

这些模块基本都满足“单一职责、数据边界清晰”的要求,可以作为首批拆分对象。

但很快我们就意识到,光有逻辑划分还不行,数据层面怎么做?特别是像订单和商品之间的状态关联问题,还有事务一致性怎么保障?

这是我们面临的第二个挑战。

第二个挑战:跨服务事务如何处理?

传统的本地事务在这个模型下失效了。比如创建订单需要检查商品库存是否充足,并执行扣减操作。如果这两个模块已经变成两个独立的服务怎么办?

我们尝试过使用两阶段提交(2PC),结果发现性能太差,且存在协调者故障的风险。最后选择了更现实的做法 —— 异步最终一致性的补偿机制

简单来说,就是通过事件驱动的方式通知对方,如果出错就进行重试和补偿。比如:

  1. 创建订单前,调用 product-service 查询库存;
  2. 如果库存充足,创建订单并发送 “OrderCreatedEvent” 到消息队列;
  3. inventory-service 监听到事件后执行库存扣减;
  4. 如果失败,记录日志并通过定时任务补账。

这样虽然牺牲了实时一致性,但在性能和可用性上找到了一个平衡点。


技术方案与选型决策

为了支撑微服务架构,我们搭建了一套完整的基础设施体系,主要包括:

1. 注册中心 & 配置中心:Nacos

原本我们使用的是 Zookeeper 作为注册中心,后来改成了 Nacos,因为其提供了配置中心的能力,还能动态刷新配置而无需重启服务。

# 示例:nacos配置文件 config-data.yaml
server:
  port: ${PORT:8080}
spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
      config:
        server-addr: 127.0.0.1:8848
        extension-configs:
          - data-id: common.properties
            group: DEFAULT_GROUP
            refresh: true

Spring Cloud Alibaba 对 Nacos 的支持非常友好,只需加上 @RefreshScope 注解即可自动更新配置。

2. API网关:Spring Cloud Gateway

API网关负责对外暴露统一入口,并做权限校验、限流熔断等公共逻辑。

我们自己基于 Spring Cloud Gateway 搭建了一个简单的网关项目,核心组件是 RouteDefinition 和 FilterChain。

示例路由定义如下:

@Configuration
public class GatewayConfig {

    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
            .route("user-service", r -> r.path("/api/user/**")
                .filters(f -> f.stripPrefix(2))
                .uri("lb://user-service"))
            .build();
    }
}

3. 服务间通信:OpenFeign + Ribbon

服务内部采用 OpenFeign 做远程调用,底层走 HTTP 协议,搭配 Ribbon 进行负载均衡。

比如调用用户服务获取用户详情的接口:

@FeignClient(name = "user-service")
public interface UserServiceClient {

    @GetMapping("/users/{id}")
    User getUserById(@PathVariable Long id);
}

不过我们也踩了一些 Feign 的坑,比如默认的连接超时时间太短导致偶发异常,后面通过自定义 Feign 的 Config 加上了重试策略和超时控制。


数据库拆分设计

数据库设计模型-1

这是最难的部分之一。

初期做法:每个服务一套数据库实例

我们给每个服务分配了单独的数据库实例,比如 user-service 使用 user_db,order-service 使用 order_db。这样做的好处是数据彻底隔离,不会互相污染。

但随之而来的问题也出现了:

  • 大量跨服务的数据冗余(如订单里要存用户昵称、收货地址)
  • 各种定时同步任务频繁运行,维护成本上升

后期演进:引入数据聚合层 + CQRS

为了解决这个问题,我们在服务外部构建了一个「数据汇总服务」,定期从各个微服务拉取数据进行整合,用来支撑报表统计和运营后台查询。

同时,我们在部分场景下采用了 CQRS 模式,读写分离,将写操作交给原服务,读操作由专用的数据视图提供。


踩过的坑和解决方案

坑一:服务依赖复杂,启动顺序难控制

微服务数量一旦多起来,服务 A 依赖 B,B 又依赖 C……本地开发调试的时候经常因为某个服务没启动而报错。

我们的解决方案是在本地使用 Docker Compose 编排一组服务,模拟线上部署环境:

services:
  user-service:
    build: ./user-service
    depends_on:
      - mysql-user
  product-service:
    build: ./product-service
    depends_on:
      - mysql-product

并且结合健康检查机制,确保只有当依赖服务准备好了才真正发起请求。

坑二:链路追踪缺失

最初没有埋点,出了问题只能靠日志 grep + 看日志堆栈,排查效率极低。

后来我们接入了 SkyWalking 来做全链路追踪,效果非常好。它不仅可以看完整请求链路,还能看到 SQL 执行耗时、HTTP 请求次数等信息。

坑三:日志聚合不够方便

每个服务的日志分散在不同的服务器上,查看麻烦。为此我们用了 ELK 技术栈,把所有的日志打到 Kafka,再被 Logstash 消费,最终展示在 Kibana 中。


效果总结:从拆分到落地的真实收益

经过半年多的时间,我们成功将原来的单体系统拆分成 6 个核心微服务 + 若干辅助组件,效果显著:

  • 发布效率提升:每个服务独立打包部署,发布周期从整天缩短到小时级;
  • 性能提升:资源利用率更合理,CPU/内存压力大幅下降;
  • 团队协作顺畅:模块解耦,各组分工明确,减少代码冲突;
  • 可扩展性强:新功能模块可以直接作为新服务快速上线;
  • 容灾能力增强:某个服务挂掉不会波及全局系统;

经验建议:给还在路上的你

如果你也在考虑拆分微服务,或者正处于转型过程中,下面几点是我亲身实践得出的一些经验和心得:

1. 服务边界不是一开始就完美的,要敢于试错

不要想着第一次拆分就能一步到位。我们可以从小模块开始,逐步探索合适的边界划分方式。边做边总结,边调整。

2. 数据一致性是个大难题,别轻易承诺 ACID

除非业务强烈要求强一致性,否则推荐使用异步+补偿机制,比如事件驱动 + 补偿任务,会比分布式事务更容易落地。

3. 架构工具链要尽早完善,否则后期运维会很痛苦

注册中心、配置中心、链路追踪、日志收集、监控告警这些基础设施要在早期就搭好,不然等业务跑起来再补就晚了。

4. 不要盲目追求“微”

服务并不是拆得越细越好。过度拆分会带来更多的通信开销和维护成本。我们要做的是职责清晰、边界合理、便于扩展的系统,而不是“为了微服务而微服务”。

5. 拆分不意味着放弃单体思想

有些场景下单体依然适用,尤其是对于中小规模、业务变化快的项目。微服务适合长期可持续迭代、具备一定成熟度的系统。


结语:技术演进是一条不断权衡的路

这次微服务拆分的过程让我深刻体会到,架构设计从来都不是非此即彼的选择题。没有银弹,也没有标准答案。每一次架构演变的背后,都是产品需求、业务发展、工程能力、团队结构等多个因素共同作用的结果。

我希望这篇文章不仅能让你了解微服务设计的一些关键技术点,更能传达一种思考方式和实践经验。技术服务于业务,架构服务于人。愿你在自己的架构之路上少踩坑,走得更稳、更远。

如果有任何问题或想进一步交流,欢迎随时留言或私信我,我们一起探讨!

评论 0

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