微服务架构设计实战:我的单体拆分之路
我是一个在互联网公司做后端开发的程序员,入职前三年一直维护着一个超大的 PHP 单体项目。随着业务量快速增长,部署、发版、调试都变得异常困难,系统经常出问题却难以定位。最头疼的一次是修改一处登录逻辑,直接导致整个站点无法访问,用户投诉如潮涌来。
那一次让我下定决心:我们必须把这套单体应用拆成微服务架构。今天我就想分享一下这次“从零开始”的经历,包括我们遇到的问题、踩过的坑,以及最后落地后的收获。
背景介绍:为什么我们要拆?

我们的核心平台是一个面向企业用户的 SaaS 系统,最初采用的是经典的 MVC 架构,前端 + 后端(PHP)+ MySQL 的组合。所有的功能模块都在一个代码库中,包括:
- 用户认证
- 订单管理
- 商品信息
- 库存控制
- 通知中心
- 运营报表
随着系统功能越来越多,团队也从最初的3个人扩展到10人以上,大家逐渐意识到以下几个痛点:
- 发布风险高:一个小改动也可能影响整个系统。
- 技术栈不灵活:想要用新语言写个模块?基本不可能。
- 性能瓶颈:某些接口慢得让人崩溃,但又不好独立扩容。
- 测试成本大:每次上线都要全回归一遍。
- 协作效率低:多个小组频繁冲突,Git Merge 时噩梦连连。
于是,我们决定尝试将这个庞大的系统拆分成多个微服务。
第一步:识别服务边界


这是我认为最难也是最重要的部分。我们做了几个动作:
1. 梳理现有模块关系图谱
通过分析日志和数据库表结构,画出了各个模块之间的调用依赖。结果发现其实有些看似强相关的模块之间联系并不紧密,例如库存和订单虽然是上下游,但实际上只有少量同步交互,其余大多是读操作。
2. 按照业务域划分服务
最终我们将原系统划分为如下几个主要服务:
| 原模块 | 拆分后服务 |
|---|---|
| 用户注册/登录 | auth-service(认证中心) |
| 商品资料管理 | product-service |
| 库存相关逻辑 | inventory-service |
| 订单下单与状态变更 | order-service |
| 报表及统计 | report-service |
这里有个小插曲:我们在讨论是否要将商品详情和库存服务合并的时候争论了很久。后来发现两者虽然相关,但一个是强读场景,一个是写多读少,分开后便于各自独立优化。
3. 明确每个服务的核心职责
举个例子:auth-service 主要处理 JWT 颁发和验证;而用户基础资料保存放在 user-service 里更合适。一开始我们就搞混了这两个服务的界限,导致后面重构花了好些时间。
技术选型:我们用了什么?

由于团队对 Java 和 Spring Boot 比较熟悉,同时希望引入一些现代框架提升开发效率,所以我们选择了如下技术栈:
- 语言框架:Spring Boot + Kotlin
- 通信协议:RESTful API + FeignClient
- 配置管理:Spring Cloud Config + Apollo
- 注册中心:Nacos
- 网关:自研简化版 + OpenResty
- 持久化:MySQL + Redis
- 监控告警:Prometheus + Grafana + ELK
- 部署方式:Docker + K8s
值得一提的是,我们没有采用非常复杂的方案,比如 gRPC 或者 Service Mesh。因为当时团队规模有限,不想引入太大复杂度。
接口设计实践:如何让服务之间更好地沟通?

接口设计是个特别容易被忽视但又极其关键的环节。我在第一个版本里犯了个错误——定义了一个“通用请求体”,所有服务都统一使用一种 JSON 格式传参。结果当其中一个服务需要上传文件时彻底炸锅,不得不停工返工。
最后我们吸取教训,制定了以下原则:
- 尽可能使用标准 HTTP 方法语义,GET 查询、POST 创建等
- 请求体按业务实际需要定制,不强行复用
- 使用 OpenAPI 规范文档化每个接口,并生成 SDK 包供其他服务调用
- 对外暴露的服务加一层 Gateway,避免客户端直连业务服务
下面是 order-service 中创建订单的一个接口示例:
@RestController
@RequestMapping("/orders")
public class OrderController {
@PostMapping
public ResponseEntity<OrderDTO> createOrder(@RequestBody CreateOrderRequest request) {
// 内部校验参数并调用 service 层
return ResponseEntity.ok(orderService.create(request));
}
}
为了保证接口稳定性,我们为每个服务生成了对应的 Swagger 文档,并通过 Jenkins Pipeline 强制检查接口变动是否兼容。
踩过的坑和解决方法
1. 数据一致性怎么破?
单体架构里可以很方便地使用数据库事务来保证数据一致性,比如“扣库存减1”和“创建订单”放在一起提交。但在分布式环境下这么做就麻烦了。
我们尝试过使用 TCC 补偿事务模型,比如:
- 下单前先调用库存服务预占库存
- 创建订单成功后调用 confirm,失败则 cancel
- 加入重试机制确保执行到底
但这种模式太复杂,尤其要考虑幂等性、网络超时等问题。最后我们妥协了——允许最终一致性的存在,采用异步消息队列做对账补偿。
经验建议:除非有严格的金融级要求,否则优先考虑 Eventual Consistency 模型,避免过度设计。
2. 本地事务 vs 分布式事务
这个问题我们在支付服务上吃了不少亏。当时支付流水必须和订单状态变更同时完成,否则系统就乱套了。早期尝试使用 Seata 的 AT 模式,结果性能下降明显,而且锁竞争严重。
后来改成了基于 Kafka 的事件驱动架构:
- 订单服务发送 "订单支付成功" 事件
- 支付服务消费该事件,更新自身记录
- 失败后重试直到最终成功
虽然不能保证实时一致,但我们加入了一个后台扫描任务来兜底补漏。
3. 日志追踪怎么办?
拆了服务之后,排查问题变得非常痛苦。你根本不知道一个请求经过了多少个服务节点。我们一开始只能靠人工串联各个系统的日志,效率极低。
后来引入了 Sleuth + Zipkin 来实现分布式链路追踪:
spring:
sleuth:
sampler:
probability: 1.0 # 采样率设为100%
zipkin:
base-url: http://zipkin-server:9411
有了链路 ID 之后,我们可以清晰看到一条请求从 gateway 到 order,再到 inventory 的完整路径,排查速度提升了好几个数量级。
服务治理的初步建设

除了拆分本身,服务治理也是不可忽略的一部分:
- 服务注册发现:Nacos 提供了健康检查和自动剔除故障节点的能力
- 客户端负载均衡:FeignClient 默认支持 Ribbon,能做轮询、权重选择
- 断路熔断机制:集成 Hystrix,防止雪崩效应
- 限流降级策略:在入口网关加上 Rate Limiting
比如我们设置了一个基础限流规则,在高峰期避免某个服务被压垮:
hystrix:
threadpool:
default:
coreSize: 20
maximumSize: 30
keepAliveTimeMinutes: 1
maxQueueSize: 100
实际收益
拆分完成后,我们看到了明显的好处:
- 部署更快了:每个服务独立构建部署,CI/CD 时间减少一半
- 问题隔离更好:库存服务挂掉不会影响登录流程
- 弹性伸缩更容易:订单服务高峰扩容时不影响其他模块
- 新技术试点可行:我们用 Go 编写了新的营销引擎,通过 HTTP 对接老系统
- 开发协作顺畅:各组专注自己负责的服务,冲突大大减少
不过,我们也牺牲了一些东西:比如原本简单的 JOIN 查询变成了跨服务调用,有时候为了获取一个聚合视图,不得不多次调用多个服务。为此我们也引入了 CQRS 架构中的 Read Side 来做查询优化。
总结 & 建议
如果你也打算从单体转向微服务,这里是我总结的一些心得:
✅ 不要盲目追求微服务,适合自己才是最好的
尤其是小型团队,微服务会带来可观的运维复杂度和学习曲线。如果你的日活不足万级,不妨先尝试模块化+垂直切分。
✅ 重视服务边界的设计
前期花足够时间梳理领域模型和边界,宁可在纸上画烂几个版本也不要仓促编码。
✅ 逐步演进而不是推倒重来
我们采取了边拆边保留旧接口的方式,确保过渡期平稳。新功能一律走微服务架构,老功能逐步迁移。
✅ 配套基础设施要跟上
日志、监控、链路追踪这些工具一旦缺失,后期查问题会非常痛苦。
最后送大家一句话:微服务不是银弹,它只是帮助我们在复杂系统面前保持灵活性的一种手段。
如果你现在正面临类似的困境,希望这篇文章能给你一些启发。欢迎留言交流你的经历,一起成长 💪
本文所述案例均为真实项目经验改编,文中涉及系统细节已脱敏处理。如有雷同,纯属巧合。

评论 0