去年参与了一个电商后台系统的重构,把跑了几年的单体应用拆成了微服务。这篇文章不是教程,而是记录其中的决策过程和踩过的坑。
起点:一个"还能跑"的单体
重构前的系统大概是这个样子:一个 Spring Boot 大项目,所有模块在一个代码仓库、一个进程里。订单、商品、用户、支付、物流,几十万行代码。
好处是部署简单,一个 war 包搞定。坏处也明显:发版要全员协调、代码冲突频繁、一个不起眼的修改可能拖垮整个服务——因为所有模块共享同一个 JVM。
业务量增长后,真正促使决策的是两个触发器:
- 大促期间数据库 CPU 顶到 100%,因为报表查询和订单写入争用同一库
- 一个实习生引入的第三方库有内存泄漏,拖着整个应用一起 OOM
怎么拆:边界比技术重要
最难的其实不是选什么框架(Spring Cloud / Dubbo),而是怎么切。切错了比不切还糟——远程调用性能下降、事务跨服务协调复杂、排查问题要翻好几个服务的日志。
我们的拆法按业务领域来:
┌─────────────────────────────────────────────┐
│ API Gateway │
│ (路由、限流、鉴权) │
└─────┬──────┬──────┬──────┬──────┬──────────┘
│ │ │ │ │
┌───┴──┐ ┌┴────┐ ┌┴────┐ ┌┴────┐ ┌┴────────┐
│ 用户 │ │商品 │ │订单 │ │支付 │ │ 物流 │
│ 服务 │ │服务 │ │服务 │ │服务 │ │ 服务 │
└──────┘ └─────┘ └─────┘ └─────┘ └─────────┘
│ │ │ │ │
└──────┴──────┴──────┴──────┘
│
┌──────┴──────┐
│ 消息队列 │
│ (异步解耦) │
└─────────────┘
拆的过程逐步进行,先拆业务逻辑最独立的用户和商品模块,稳定后再拆订单和支付。整个过程持续了两个月,期间新旧两套系统并行。
三个现实问题
分布式事务——这是最大痛点。订单创建涉及库存扣减、支付、积分,原来一个 @Transactional 搞定,现在跨三个服务。最终选的是本地消息表 + 定时补偿的方案,不引入 Seata 之类的框架——团队没精力学。
服务间调用——一拆开链路变长,一个下单请求经过网关→订单→商品→用户→积分,网络开销翻了好几倍。排查问题从翻一个日志文件变成翻五个服务的日志。必须上链路追踪(最后选了 SkyWalking,对代码侵入最小)。
数据一致性——远程调用总有超时的时候,重试就可能重复。接口设计要支持幂等——调用方传一个唯一的 transaction_id,被调用方根据 id 去重,保证"不管调多少次,效果等于调一次"。
值不值
坦白说,拆完前三个月的维护成本是上升的。以前一个 Bug 在单体里改一行代码,现在要在两个服务间配消息、写补偿逻辑、注意幂等。
但长期收益是有的——团队可以独立发版了(不用挤在同一时间窗口)、出问题爆炸半径变小了(支付挂了不影响用户登录)、技术栈也可以按场景差异化。
架构演进的规律是:**复杂度不会消失,只会转移。**单体把复杂度集中在代码里,微服务把复杂度扩散到网络、运维和团队协作中。你选择接受哪种复杂度。
评论