从单体到微服务:我在一次系统重构中的实战经验
引言:为什么我选择走这条路

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,确保跨语言兼容性和清晰的接口契约。
接口设计原则
我们在设计接口时遵循几个关键点:
- 强类型参数:拒绝Map或JSON传递,明确输入输出格式;
- 失败降级机制:例如订单创建失败要保证能回滚库存状态;
- 幂等性处理:尤其是在支付和服务重试场景中尤为重要;
- 异步解耦:对于非即时结果的操作,例如发券、通知等,采用消息队列进行异步处理。
实战代码示例
下面展示几个关键的代码片段,帮助大家理解服务拆分的具体落地方式。
接口定义(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));
}
}
这些只是最基础的服务间调用实现,真正复杂的地方在于如何协调服务间的依赖关系,确保数据一致性,同时避免分布式事务带来的性能损耗。
踩坑实录:那些让我半夜睡不着的日子
坑点一:服务注册发现不稳定

初期使用Zookeeper作为注册中心,但在高峰期经常出现节点失联的情况,导致服务调用超时甚至失败。
后来我们改用了Nacos,不仅提供了服务注册/发现功能,还内置了配置中心,极大简化了我们的运维工作。
小建议:微服务架构下,注册中心的稳定性直接影响整个系统的可用性。不要图省事,一定要选一个成熟稳定的方案。
坑点二:分布式事务难搞
订单创建涉及到减库存、扣余额、生成日志等多个操作。起初我们尝试用Seata来做TCC事务,但因为业务逻辑复杂、补偿机制维护困难,最终放弃了这种做法。
最后我们采用了最终一致性方案:使用RocketMQ发送事务消息,在确认库存减少后,异步更新订单状态,失败则延迟重试,并通过定时任务对账补漏。
坑点三:链路追踪不够清晰
微服务一多,请求链就变得异常复杂。如果没有一个好的链路追踪工具,排查问题就像大海捞针。
我们早期用的是Zipkin,后来换成SkyWalking,它自带的UI界面非常直观,还能监控SQL执行情况和JVM指标。
上线后的效果评估
经过半年的逐步迁移,我们基本完成了核心服务的拆分。整体来看,系统稳定性和可维护性有了显著提升:
- 部署效率提升:从原来15分钟全量部署,缩短为3~5分钟独立部署;
- 故障影响范围缩小:某个服务出现问题不会拖累整个平台;
- 弹性伸缩能力增强:高峰时段可根据不同服务的负载情况动态扩容;
- 开发协同顺畅:各个服务由不同小组负责,职责更清晰,沟通成本大幅降低。
当然,也不是完全没有副作用。比如:
- 多服务之间调用网络开销增加;
- 数据一致性控制变复杂;
- 测试环境搭建变得更加繁琐。
但我们相信这些问题都是可以通过基础设施优化、自动化流程等方式逐步解决的。
我的经验总结
如果你也在考虑微服务化,或者已经在路上,我想送你几条真心话:
- 微服务不是银弹,不要为了拆而拆。先理清楚你的业务边界,拆分要围绕领域模型展开。
- 别忽视基础设施,日志收集、链路追踪、服务治理是微服务成功的三大基石。
- 接口设计要慎重,一旦发布出去就要兼容演进,否则后期改动代价巨大。
- 异步很重要,尤其是涉及到多系统协作时,消息队列可以大大提升系统吞吐能力和容错能力。
- 运维要跟上,不然你会陷入“天天修机器”的恶性循环中。
- 别急着用K8s,除非你有足够的人力和经验去运维,不然可能会让你压力更大。
写在最后

微服务并不是简单的技术堆砌,它背后是一整套思维方式的变化,是从集中式架构向分布式的思维跃迁。它带来了灵活性,也带来了新的挑战。
这场重构让我成长了很多,也深刻体会到:好的架构不是一开始就能设计出来的,它是在一次次实践中不断打磨出来的。
如果你正在经历类似的转变,希望我的分享能帮你少走些弯路。如果你有更好的实践经验,也欢迎留言交流,我们可以一起探讨!
共勉!

评论 0