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

纯真的思想家
2025-06-15 11:54
阅读 438

引言:为什么我们决定拆分单体系统?

引言:为什么我们决定拆分单体系统?

大家好,我是某互联网公司的一名后端开发工程师,主要负责电商平台的后端系统搭建与维护。去年我们团队面临了一个“成长的烦恼”:我们的核心订单系统,起初是一个标准的Spring Boot单体应用,跑在一台机器上,结构清晰、部署简单。但随着用户量的暴涨,业务功能也越来越多,这个原本“稳如老狗”的系统开始变得不堪重负。

订单模块不仅要处理下单、支付、退款,还要对接库存、物流、优惠券、积分等多个子系统。代码越来越臃肿,上线一次要小心翼翼地测试整套流程;一旦出现故障,排查定位困难,牵一发而动全身;新同学接手成本极高,系统响应时间也越来越不可控。

我们意识到——是时候重构系统了,把这块庞大的蛋糕切成一个个小块,各自独立运行,才能真正支撑起高速发展的业务需求。于是,我们正式启动了微服务拆分项目,目标是将原有的订单系统逐步拆分为多个小型的服务,比如订单服务、支付服务、优惠服务等,并实现服务间协作、容错、监控等能力。

这篇文章,我想结合这段经历,和大家分享一下我们在实际项目中是如何推进微服务架构落地的。包括遇到的技术挑战、关键决策点、踩过的坑以及最终取得的成果,希望对正在或即将踏上微服务之路的朋友有所帮助。


项目背景:一个典型单体系统的困境

项目背景:一个典型单体系统的困境

微服务架构示意图-2

我们最初的服务结构如下:

订单中心(Spring Boot)
├── 下单逻辑
├── 支付回调
├── 物流同步
├── 库存扣减
└── 优惠券核销

所有的接口、数据模型都在同一个工程下,用的是单一的MySQL数据库,通过Spring Data JPA操作数据层。

刚开始没问题,毕竟代码量不大,功能也不多。但随着时间推移,问题逐渐暴露出来:

  1. 发布频繁导致风险大:每次发布一个小功能,都需要重新部署整个订单服务,出错代价高。
  2. 性能瓶颈明显:订单写入高峰期经常拖慢其他逻辑,甚至造成线程阻塞。
  3. 扩展性差:想单独优化某个模块,必须牵动整个系统。
  4. 代码耦合严重:各个功能之间相互依赖,调用关系混乱。
  5. 运维压力增大:日志复杂、定位问题耗时长,自动化程度低。

所以,微服务化势在必行,我们也希望借此机会引入更现代的架构实践,比如注册中心、负载均衡、链路追踪、熔断降级等等。


微服务拆分的目标与思路

微服务拆分的目标与思路

我们定下了几个目标:

  • 拆分订单核心逻辑为多个独立服务(订单、支付、优惠等);
  • 保持原有业务不变的前提下逐步过渡;
  • 提供跨服务通信能力(RPC + 消息队列);
  • 构建统一的微服务治理体系;
  • 实现服务治理可视化监控;
  • 保障数据一致性(至少满足最终一致性)。

拆分的总体思路是:

先垂直划分,后水平拆解。

先根据业务边界划分出不同的服务模块。例如,我们按照业务场景将系统拆分成了以下几个主要服务:

  • order-service:订单创建、状态管理、履约等;
  • payment-service:支付相关的处理逻辑;
  • promotion-service:优惠券、促销活动相关;
  • inventory-service:库存控制;
  • logistics-service:物流状态跟踪与同步。

这些服务之间使用 Spring Cloud Feign 进行远程调用,必要时借助 Kafka 进行异步通知。

同时,为了应对服务数量增加后的管理问题,我们引入了以下组件:

  • Nacos 作为注册中心和服务配置中心;
  • Sentinel 进行限流、熔断、降级;
  • SkyWalking 做全链路追踪;
  • Ribbon + OpenFeign 处理服务发现和调用;
  • Seata 管理分布式事务(TCC 模式);
  • Redis + RocketMQ 实现缓存与消息队列能力。

面临的主要挑战

1. 接口拆分带来的不确定性

最开始我们以为只是换个名字,把类挪到不同工程里就行。结果发现,业务之间的依赖非常复杂。例如,订单创建需要判断用户是否符合优惠条件,这时候就要调用 promotion-service 来判断能否使用该优惠券。

这就涉及到两个问题:

  • 如何设计 API 接口,使得服务间调用清晰可读?
  • 如何确保在异常情况下能正确兜底处理?

我们花了很多时间来定义接口规范,制定统一的返回格式,还加了超时、降级机制。

2. 数据一致性难题

原来的单体服务使用本地事务可以很好地保证一致性,现在拆成多个服务后,就需要面对真正的分布式事务挑战。

举个例子:用户下单后,需要扣除库存、生成订单、冻结优惠券。

这三个动作分别属于不同的服务,且都涉及数据库变更。如果其中一个失败,如何回滚?我们尝试过几种方案:

  • 最终一致性 + 重试补偿(适合非强一致性场景);
  • TCC 模式(适用于强一致性要求高的场景);
  • Saga 模式(适合流程长、步骤多的业务);

最后选择 TCC 是因为部分业务确实不允许中间状态长期存在(如库存),而且 Seata 支持得比较好。

3. 服务治理能力缺失

拆分完才发现,原来没考虑服务治理是多么可怕。我们当时没有限流、熔断、降级等机制,第一次压测就发现订单服务被调崩了。后来才补上了 Sentinel 和 Hystrix(选用了 Sentinel,因为适配更好)。

另外,服务注册、健康检查、负载均衡等也都得靠 Nacos 搞起来。初期因为网络配置的问题,还有服务实例反复上线/下线的小插曲,浪费了不少调试时间。


技术实现细节分享

1. 服务通信方式的选择

我们优先选择了基于 HTTP 的 OpenFeign + Ribbon 方案,用于服务之间的直接调用。虽然性能不如 gRPC 或 Dubbo 的二进制协议,但对于以 Java 为主栈的项目来说,Feign 更易于上手,集成简单。

示例代码如下:

@FeignClient(name = "payment-service")
public interface PaymentServiceClient {
    @PostMapping("/payment/create")
    ApiResponse<PaymentResultDTO> createPayment(@RequestBody CreatePaymentDTO dto);
}

不过对于某些高并发、低延迟的场景,我们也尝试用 RocketMQ 做异步解耦。例如,在订单完成后触发优惠券的核销动作,使用 MQ 而不是直接调用服务。

2. 使用 Sentinel 实现熔断限流

我们在所有对外服务的 Feign Client 上添加了全局拦截器,并启用 Sentinel 的支持。

feign:
  sentinel:
    enabled: true

然后通过 Sentinel 控制台动态配置资源保护规则,避免服务雪崩效应。

例如,我们可以设置某个接口的每秒最大调用量,超过则限流拒绝;也可以配置调用失败次数,超过阈值则自动熔断一段时间。

3. 利用 SkyWalking 实现链路追踪

为了方便排查线上问题,我们引入了 SkyWalking。它不仅记录每个请求的完整调用链路,还能分析 SQL 执行情况、慢查询、HTTP 请求耗时等。

接入非常简单,只需要给 JVM 启动参数加上 agent:

-javaagent:/path/to/skywalking-agent.jar -Dskywalking.agent.service_name=order-service

然后就能在 SkyWalking UI 中看到详细的调用链信息,大大提升了排障效率。

4. 数据一致性方案:Seata + TCC 模式

这是整个系统最难搞的地方之一。我们最终采用 TCC 模式,即 Try - Confirm - Cancel 三阶段提交。

举个例子:下单过程中要锁库存 → 下单成功则确认库存 → 失败则取消库存。

TCC 模式的优点在于可以在不锁表的情况下实现分布式事务,但也带来了开发上的复杂度。

我们封装了一套通用模板,让业务逻辑只关心“Try”、“Confirm”、“Cancel”方法:

@Override
@Transactional(rollbackFor = Exception.class)
public void deductStock(StockDeductDTO dto) {
    // Step 1: Try 阶段:检查并预留资源
    checkAvailable(dto);

    // Step 2: 真正执行预扣
    preDeduct(dto);
}

@Override
@Transactional(rollbackFor = Exception.class)
public void confirmStockDeduction(StockDeductDTO dto) {
    commitDeduct(dto);
}

@Override
@Transactional(rollbackFor = Exception.class)
public void cancelStockDeduction(StockDeductDTO dto) {
    releasePreDeduct(dto);
}

这部分代码会被 Seata 自动识别为分支事务,在全局事务协调器中进行统一调度。


踩坑经验分享

1. Feign 的默认超时太短

一开始我们没改 Feign 的超时配置,结果服务之间调用经常出现 Timeout,尤其在高并发环境下更加明显。

解决方案是在 application.yml 中调整 Feign 的超时时间:

feign:
  client:
    config:
      default:
        connectTimeout: 5000
        readTimeout: 5000

当然,也可以根据不同服务做精细化配置。

2. Nacos 注册实例异常

我们曾遇到某个服务注册到 Nacos 后显示 unhealthy 的情况。后来发现是因为服务启动太快,还没准备好就被 Nacos 标记为可用,结果请求打过来就挂了。

解决办法是延长健康检查的时间,或者让服务在完全准备就绪后再注册自己。

3. 分布式事务数据不一致

TCC 在生产环境中遇到了一些诡异的情况,比如 Confirm 方法没被执行或者执行了两次。

原因很多样,有时是消息丢失,有时是重复消费。我们的做法是加上幂等处理,每个分支事务都带上唯一 ID,防止重复执行。

4. 日志和链路追踪不一致

早期 SkyWalking 的 traceId 没有传递过去,导致日志中的 traceId 无法与链路信息关联。后来我们使用 MDC + Interceptor 统一把 traceId 注入到日志中,这样排查问题的时候就方便多了。


拆分后的效果与收益

经过半年的持续迭代,整个订单系统已经完成了微服务化改造,目前运行稳定,具体收益如下:

项目 拆分前 拆分后
发布频率 每周一次 每天多次
服务稳定性 偶尔卡顿或崩溃 SLA > 99.5%
排查耗时 小时级 分钟级
新人熟悉周期 半个月以上 一周内
性能(TPS) 约 800 提升至 2000+

特别是在高并发场景下(比如双十一活动期间),整个系统表现远优于之前的单体版本,未发生大规模宕机事故,运维人员反馈服务监控指标也更加直观透明。


我的经验总结

系统架构设计图-1

作为一名一线开发者,我真心觉得微服务不是银弹,但也确实是当前大型系统不可或缺的解决方案之一。以下是我在实战中的一些心得体会,分享给大家:

1. 拆分不能贪多求快,要循序渐进

我们一开始就犯了个错误——想一次性把所有服务都拆出去,结果导致大量接口不稳定、事务难以处理、团队协作混乱。

建议是从小切口入手,从最容易拆、影响最小的模块下手,逐步积累经验和信心。

2. 一定要重视基础设施建设

不要想着先把服务拆出去,再慢慢搞治理。否则等到后面服务多了,各种问题就会集中爆发。

提前规划好注册中心、配置中心、监控报警、日志聚合、CI/CD 等平台,才能真正提高交付效率。

3. 接口设计要规范,文档要齐全

服务多了以后,接口就是契约。建议使用 Swagger 或 SpringDoc 自动生成 API 文档,配合权限控制和版本管理。

4. 数据一致性是分布式系统的大敌

TCC、Saga、消息补偿各有适用场景,关键是要结合自身业务特性选择合适的模式,并做好异常兜底。

5. 团队协作要统一技术栈

我们在微服务初期采用了不同的框架组合,有的项目用Dubbo,有的用Feign,结果导致调用风格混乱,升级困难。

建议在整个组织范围内统一技术栈和工具链,避免碎片化。

6. 生产环境的运维比编码更重要

微服务带来便利的同时,也意味着你得面对更多的运维挑战。比如服务重启策略、灰度发布、流量控制、故障隔离等。这些都是后期必须补齐的能力。


写在最后

微服务架构的确很“酷”,但它不是一蹴而就的过程。它考验着我们的架构设计能力、团队协作精神,以及对技术深度的理解。

在这次项目中,我们经历了从迷茫到坚定,从磕磕绊绊到稳定高效的过程。我相信只要方向是对的,每一步努力都会沉淀下来,成为后续发展的重要基石。

如果你也在思考要不要踏上微服务之路,我的建议是:别怕复杂,也不要盲目追求新技术,而是围绕业务价值去做合理的拆解和技术选型

微服务的本质不是炫技,而是更好地支撑业务增长和快速响应市场变化。

愿你在自己的架构之路上越走越远,我们一起加油!

评论 0

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