从单体到微服务:我在一次系统重构中的实战经验

红黑树下乘凉
2025-06-20 07:38
阅读 256

引言:为什么我选择走这条路

引言:为什么我选择走这条路

2021年,我加入了一个中型电商平台的技术团队。当时整个后端系统是一个庞大的Spring Boot单体应用,支撑着商品、订单、支付、用户管理、促销等多个模块。随着业务发展,这个单体项目越来越臃肿,每次上线一个小小的功能变更都要打包整个工程,部署半天,而且一不小心就会把其他功能搞挂了。

更头疼的是,线上出问题排查起来特别麻烦。比如,某次促销期间,订单模块的QPS飙升导致JVM频繁Full GC,整个系统都开始抖动,连首页加载都慢得离谱。那一刻我就意识到:再不拆分架构,迟早会把自己埋进去。

于是我们决定开启一场“大手术”——把原有单体系统逐步迁移到微服务架构上。

这篇文章想和大家分享这次转型过程中的真实经历,包括技术选型、踩坑过程、代码结构以及上线后的运维细节。希望能给同样在做类似事情的同学带来一些启发和参考。


我们遇到的问题

我们遇到的问题

1. 单体项目的痛点暴露无遗

  • 构建与部署效率低下:全量部署耗时15分钟以上,严重影响迭代速度。
  • 发布风险高:一个小bug可能影响整个系统,灰度发布的粒度也做不到。
  • 资源无法隔离:核心模块(如下单)和非核心模块(如CMS)共用线程池和数据库连接池,出现热点问题难以定位。
  • 代码耦合严重:各模块之间的调用都是通过本地方法调用,逻辑混乱,没有统一接口规范。

2. 新业务需求倒逼改造

新上线的营销活动中心、直播带货等模块需要快速响应市场变化,而原系统的开发模式根本跟不上节奏。

我们急需将系统解耦,实现“按域部署、独立开发、各自扩容”。


微服务架构设计思路

经过一番调研和技术讨论,我们最终采用了以下架构设计方案:

技术栈选型

模块 技术选型
微服务框架 Spring Cloud Alibaba + Dubbo3
注册中心 Nacos
配置中心 Nacos Config
网关 Spring Cloud Gateway
日志聚合 ELK Stack
监控告警 Prometheus + Grafana + SkyWalking
数据库分片 ShardingSphere
消息队列 RocketMQ

我们最初也考虑过Kubernetes,但出于对运维成本和团队熟悉程度的综合考量,最终还是选择了以虚拟机+Docker为主,后续计划再逐步上云。

核心服务拆分策略

我们并没有盲目地追求“大而全”,而是从实际业务出发,优先拆出核心链路模块:

  • 商品中心(product-service)
  • 用户中心(user-service)
  • 订单中心(order-service)
  • 库存中心(inventory-service)
  • 支付中心(payment-service)

每个服务之间通过RPC进行通信,接口定义采用Dubbo的方式,使用IDL(Protocol Buffer)来描述API,确保跨语言兼容性和清晰的接口契约。

接口设计原则

我们在设计接口时遵循几个关键点:

  1. 强类型参数:拒绝Map或JSON传递,明确输入输出格式;
  2. 失败降级机制:例如订单创建失败要保证能回滚库存状态;
  3. 幂等性处理:尤其是在支付和服务重试场景中尤为重要;
  4. 异步解耦:对于非即时结果的操作,例如发券、通知等,采用消息队列进行异步处理。

实战代码示例

下面展示几个关键的代码片段,帮助大家理解服务拆分的具体落地方式。

接口定义(Protobuf)

// user.proto
syntax = "proto3";

package com.example.user;

message UserInfoRequest {
    int64 user_id = 1;
}

message UserInfoResponse {
    int64 id = 1;
    string name = 2;
    string email = 3;
}

service UserService {
    rpc GetUserInfo(UserInfoRequest) returns (UserInfoResponse);
}

Dubbo 服务提供方配置

dubbo:
  application:
    name: user-service
  protocol:
    name: dubbo
    port: 20880
  registry:
    address: nacos://nacos-host:8848

Dubbo 服务消费者调用示例

@RestController
@RequestMapping("/order")
public class OrderController {

    @Reference
    private UserService userService;

    @GetMapping("/{userId}")
    public UserInfoResponse getUser(@PathVariable long userId) {
        return userService.getUserInfo(new UserInfoRequest(userId));
    }
}

这些只是最基础的服务间调用实现,真正复杂的地方在于如何协调服务间的依赖关系,确保数据一致性,同时避免分布式事务带来的性能损耗。


踩坑实录:那些让我半夜睡不着的日子

坑点一:服务注册发现不稳定

服务器部署方案-2

初期使用Zookeeper作为注册中心,但在高峰期经常出现节点失联的情况,导致服务调用超时甚至失败。

后来我们改用了Nacos,不仅提供了服务注册/发现功能,还内置了配置中心,极大简化了我们的运维工作。

小建议:微服务架构下,注册中心的稳定性直接影响整个系统的可用性。不要图省事,一定要选一个成熟稳定的方案。

坑点二:分布式事务难搞

订单创建涉及到减库存、扣余额、生成日志等多个操作。起初我们尝试用Seata来做TCC事务,但因为业务逻辑复杂、补偿机制维护困难,最终放弃了这种做法。

最后我们采用了最终一致性方案:使用RocketMQ发送事务消息,在确认库存减少后,异步更新订单状态,失败则延迟重试,并通过定时任务对账补漏。

坑点三:链路追踪不够清晰

微服务一多,请求链就变得异常复杂。如果没有一个好的链路追踪工具,排查问题就像大海捞针。

我们早期用的是Zipkin,后来换成SkyWalking,它自带的UI界面非常直观,还能监控SQL执行情况和JVM指标。


上线后的效果评估

经过半年的逐步迁移,我们基本完成了核心服务的拆分。整体来看,系统稳定性和可维护性有了显著提升:

  • 部署效率提升:从原来15分钟全量部署,缩短为3~5分钟独立部署;
  • 故障影响范围缩小:某个服务出现问题不会拖累整个平台;
  • 弹性伸缩能力增强:高峰时段可根据不同服务的负载情况动态扩容;
  • 开发协同顺畅:各个服务由不同小组负责,职责更清晰,沟通成本大幅降低。

当然,也不是完全没有副作用。比如:

  • 多服务之间调用网络开销增加;
  • 数据一致性控制变复杂;
  • 测试环境搭建变得更加繁琐。

但我们相信这些问题都是可以通过基础设施优化、自动化流程等方式逐步解决的。


我的经验总结

如果你也在考虑微服务化,或者已经在路上,我想送你几条真心话:

  1. 微服务不是银弹,不要为了拆而拆。先理清楚你的业务边界,拆分要围绕领域模型展开。
  2. 别忽视基础设施,日志收集、链路追踪、服务治理是微服务成功的三大基石。
  3. 接口设计要慎重,一旦发布出去就要兼容演进,否则后期改动代价巨大。
  4. 异步很重要,尤其是涉及到多系统协作时,消息队列可以大大提升系统吞吐能力和容错能力。
  5. 运维要跟上,不然你会陷入“天天修机器”的恶性循环中。
  6. 别急着用K8s,除非你有足够的人力和经验去运维,不然可能会让你压力更大。

写在最后

缓存策略对比-1

微服务并不是简单的技术堆砌,它背后是一整套思维方式的变化,是从集中式架构向分布式的思维跃迁。它带来了灵活性,也带来了新的挑战。

这场重构让我成长了很多,也深刻体会到:好的架构不是一开始就能设计出来的,它是在一次次实践中不断打磨出来的。

如果你正在经历类似的转变,希望我的分享能帮你少走些弯路。如果你有更好的实践经验,也欢迎留言交流,我们可以一起探讨!

共勉!

评论 0

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