微服务架构设计实战:从单体到分布式

贪心没贪够
2025-06-19 02:48
阅读 760

开篇:为何要分享这段旅程?

开篇:为何要分享这段旅程?

2019年,我在一家做在线教育平台的创业公司负责后端架构和系统维护。那会儿,整个后端系统是一个大大的单体应用——Spring Boot写的Java项目,跑在Tomcat里,前端通过Nginx反向代理直接访问API接口。

刚开始一切还行得通,但随着业务增长、新功能越来越多,团队规模也慢慢扩大,我们开始频频遇到几个痛点:

  • 部署缓慢:每次上线都要全量打包部署,影响范围大
  • 技术栈固化:想引入新框架或语言很难,因为所有逻辑都耦合在一起
  • 性能瓶颈明显:高并发场景下,数据库锁表频繁,接口响应变慢
  • 协作成本高:多个开发人员同时修改一个代码库,冲突多,测试环境也不稳定

于是我们决定尝试进行微服务拆分。过程磕磕绊绊,踩了很多坑,也学到了很多教训。这篇文章就是想结合我自己的经历,聊聊我们是怎么一步步实现这个转型的。


问题描述:单体应用带来的痛

问题描述:单体应用带来的痛

我们当时的核心服务集中在两个工程中:

  1. edu-api:提供用户、课程、订单等核心接口
  2. admin-api:后台管理系统的接口

这两个项目的数据库共用一张MySQL主库(虽然有读写分离),但数据表之间存在大量关联查询。每当某个模块出现性能瓶颈,整个系统就会跟着受影响。

举个最典型的例子:有一次直播课推流时调用了视频处理模块,导致线程池资源被占满,结果是整个edu-api服务不可用,前台连登录页面都打不开。

更头疼的是,我们在推进新功能的时候经常发生如下情况:

“你刚改了用户中心的代码?我这边也在改购物车流程啊,我们是不是需要先沟通一下?”

团队成员之间的协作变得异常低效。


解决方案:渐进式微服务拆分

我们并没有一开始就全面铺开微服务架构,而是采用了渐进式拆分策略。整个拆分过程持续了差不多6个月,期间边拆边修、边学边改,最终将原来的系统拆成了以下几个关键服务:

模块名称 功能描述 技术栈
用户中心 注册/登录/权限管理 Spring Cloud Alibaba + Nacos
课程中心 课程管理、目录结构 Spring Cloud Gateway + Feign
订单中心 支付、优惠券、订单流程 RocketMQ 异步队列
数据分析 埋点数据统计 ClickHouse + Spark Streaming
内容审核 AI图文内容检测 Python + RabbitMQ
文件服务 图片上传、CDN加速 FastDFS + MinIO

负载均衡配置-1

架构图示意(简化版)

+-------------------+
|      网关层        |
| (Gateway/Nginx)   |
+-------------------+
        |
+-------------------+
|     服务注册中心    |
|    (Nacos/Eureka)  |
+-------------------+
        |
+-------v-------+   +-------+-------+   +-------+-------+
| 用户中心       |   | 课程中心        |   | 订单中心        |
| Spring Boot App |   | Spring Boot App |   | Spring Boot App |
+---------------+   +---------------+   +---------------+
        |                   |                   |
+---------------+   +---------------+   +---------------+
| MySQL / Redis |   | MySQL / Redis |   | MySQL / Redis |
+---------------+   +---------------+   +---------------+


![数据库设计模型-2](https://code-guide.oss.shanghai.autogptai.club/common/file/download?name=date2025061902/ca2797df-5510-48a9-8efd-83d8e03dc484.jpg)

这种架构让我们实现了模块解耦,独立部署,也方便将来引入其他语言的服务。


关键代码实践与配置说明

1. 网关整合 —— 使用 Spring Cloud Gateway + Nacos

我们的网关使用了 Spring Cloud Gateway 来统一处理请求路由和服务发现。

spring:
  cloud:
    gateway:
      routes:
        - id: user-service
          uri: lb://user-service
          predicates:
            - Path=/api/user/**
          filters:
            - StripPrefix=1
        - id: course-service
          uri: lb://course-service
          predicates:
            - Path=/api/course/**
          filters:
            - StripPrefix=1

配合 Nacos 做服务注册中心:

@EnableDiscoveryClient
@SpringBootApplication
public class GatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }
}

这样我们就实现了动态路由和自动服务发现。

2. 服务间通信 —— OpenFeign + LoadBalancer

例如订单中心调用课程中心获取课程详情:

@FeignClient(name = "course-service", path = "/api/course")
public interface CourseServiceClient {

    @GetMapping("/{id}")
    ResponseEntity<CourseDetailDTO> getCourseById(@PathVariable Long id);
}

Feign + Ribbon 的组合能很好地完成负载均衡和远程调用。

不过我们也遇到了一些问题,比如 Feign 客户端默认不开启 Hystrix 断路器,后来我们手动引入并配置熔断机制。

3. 日志与链路追踪 —— 使用 Sleuth + Zipkin

为了定位跨服务调用中的问题,我们引入了 Sleuth 和 Zipkin 来记录日志链路:

spring:
  zipkin:
    base-url: http://zipkin-server:9411
  sleuth:
    sampler:
      probability: 1.0 # 记录所有请求

每个服务的日志都会带上 Trace ID,便于运维排查。

4. 异步通知 —— RocketMQ 实现解耦

例如订单创建成功后,通过 RocketMQ 发送消息给内容审核服务去检查商品图文信息:

public void sendOrderCreatedMessage(Order order) {
    Message msg = new Message("ORDER_TOPIC", "TAG", JSON.toJSONString(order).getBytes());
    rocketMQTemplate.convertAndSend(msg);
}

消费端监听:

@RocketMQMessageListener(topic = "ORDER_TOPIC", consumerGroup = "content-consumer-group")
public class OrderMessageConsumer implements RocketMQListener<Order> {
    
    @Override
    public void onMessage(Order order) {
        contentAuditService.processOrder(order);
    }
}

这样避免了服务间的强依赖,提高了可用性。


踩坑经验总结

在整个过程中,我们踩了不少坑,有些是架构层面的,有些是运维上的,下面列举几个印象特别深刻的。

坑1:服务雪崩效应

某个晚上,用户中心服务因为数据库连接池满了而崩溃,进而导致课程中心调用失败,最终波及整个系统。我们意识到必须加入服务熔断与降级机制,于是引入了 Hystrix,并对关键接口做了 fallback。

@GetMapping("/detail/{id}")
@HystrixCommand(fallbackMethod = "fallbackGetUserDetail")
public UserDetailDTO getUserDetail(@PathVariable String id) {
    return userService.getUserDetail(id);
}

private UserDetailDTO fallbackGetUserDetail(String id) {
    return new UserDetailDTO();
}

后来还考虑过 Resilience4j,但由于时间紧张最终还是继续使用了 Hystrix。

坑2:数据库事务难跨服务

早期有个需求是“下单成功后减少库存”,这涉及到订单服务和库存服务两个系统。原本打算用本地事务加消息队列来保证一致性,但实际上实现起来非常复杂,而且可能出现幂等问题。

后来我们改为使用Saga 分布式事务模式,在订单服务内部先减库存,再生成订单;如果失败就调用补偿服务回退库存。

不过这也带来了额外开发工作量,建议如果不是特别重要的一致性需求,可以采用最终一致+重试补偿的方式。

坑3:网关路由配置不当引发性能下降

最初我们把所有的路径都配置在网关里,结果在一次大规模促销活动中,网关成为了性能瓶颈,CPU飙到了80%以上。

后来我们优化了配置,将一部分静态资源直接打到 CDN 上,同时对 API 接口进行了限流配置(使用 Sentinel)。

坑4:日志系统没有统一接入

微服务拆分初期,每台机器的日志都是本地存储的,出现问题时需要一台一台地查,效率极低。

后来我们搭建了 ELK 日志收集系统,并在各个服务里集成了 Logback 的远程写入能力,大大提升了排错效率。


实施效果与收益总结

经过大约半年的迭代拆分,我们的系统发生了以下显著变化:

  1. 部署效率提升:现在只需要构建对应服务镜像即可发布,不影响其它模块。
  2. 故障隔离增强:某个服务挂掉不会造成整体瘫痪,熔断机制减少了影响范围。
  3. 性能提升:热点数据和服务被分离,缓存命中率提高,接口响应速度平均缩短了 30%。
  4. 扩展性强:当流量突增时,可以迅速扩缩容特定服务,而不是整个系统一起动。
  5. 团队协作顺畅:各模块由不同小组负责,职责清晰,沟通成本降低。

当然,也不是说微服务完美无缺。我们为此也付出了不小的代价:

  • 开发门槛变高:微服务涉及更多组件(如网关、配置中心、注册中心等)
  • 调试成本增加:联调、测试比以前更麻烦,需要 mock 掉很多依赖
  • 运维复杂度上升:监控指标、服务健康检查、日志采集等工作都需要专门维护

但总体而言,这次架构升级对我们支撑业务高速发展起到了至关重要的作用。


经验分享 & 建议

如果你也在考虑从单体转向微服务,或者已经在实施中,这里有几个实用建议送给你:

1. 不要盲目追“微”

微服务不是银弹,它适合有一定规模、有长期规划的产品。小团队前期不要轻易拆分,先把模块边界理清楚更重要。

2. 渐进式拆分优于一步到位

我们采取的策略是从核心模块入手,逐步剥离非关键服务。这样可以在实践中不断修正方向,不至于一开始就陷入困境。

3. 提前规划好基础建设

包括服务治理、日志统一、链路追踪、监控报警这些基础设施。否则你会陷入每天都在修bug的状态。

4. 接口设计非常重要

服务拆分后,接口的设计直接影响到后期的可维护性和稳定性。建议制定一套合理的接口规范文档,比如 OpenAPI 或 Protobuf。

5. 合理选择技术栈,兼容为主

我们最早用的是 Spring Cloud Netflix 一套,后来切换为 Spring Cloud Alibaba,主要是因为 Nacos 更稳定,适配国内云厂商也更方便。

技术选型时不要只看新不新,而是要看是否成熟、是否容易运维。


结语:技术服务于业务,架构服务于人

最后我想说的是:无论你采用哪种架构,最终目的都是更好地支撑产品和业务的发展。微服务不是目的,而是手段。

在我参与的这场架构改造中,让我印象最深的其实不是技术本身,而是整个过程中的协同与成长——从最初的争论、焦虑、加班熬夜,到最后大家逐渐形成默契、彼此信任,这种团队氛围才是最宝贵的财富。

希望我的这篇分享,能帮你少走一些弯路,也能让你在面对类似挑战时更有底气。愿你在架构设计的路上越走越远!


文章作者:某在线教育公司后端架构师,专注于 Java 全栈开发、微服务演进、系统高并发设计。如有交流欢迎留言!

评论 0

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