本文记录了我在实际项目中将单体应用拆分为微服务架构时踩过的坑,希望能帮你少走弯路。
前言
2024年初,我们团队接到一个任务:把一个运行了3年的单体Spring Boot项目拆分成微服务。当时我信心满满,觉得"不就是拆几个服务嘛"。结果三个月后,我深刻体会到了什么叫"拆分一时爽,维护火葬场"。
今天这篇文章,我不讲理论,只讲我真实踩过的坑。
一、后悔决定1:按技术层拆分而不是按业务域拆分
我当时怎么想的
刚开始拆分时,我按照技术层来拆:
user-service:所有用户相关common-service:公共逻辑gateway-service:网关data-service:所有数据库操作
出了什么问题
很快我发现,一个"下单"操作需要调用user-service(查用户信息)→data-service(写订单数据)→common-service(发通知),链路又长又脆弱。
更要命的是,data-service成了上帝服务,所有业务都往里塞,跟单体有什么区别?
正确做法
按业务领域(DDD)拆分:
order-service → 负责订单生命周期 payment-service → 负责支付结算 user-service → 负责用户注册/认证 notification-service → 负责消息推送每个服务拥有自己的数据库,自己管自己的事。
核心原则
高内聚、低耦合 一个服务对应一个业务能力(Business Capability) 服务之间通过API或事件通信,不共享数据库二、后悔决定2:一开始就上全套Spring Cloud
我当时怎么想的
“既然要做微服务,那Eureka、Config Server、Zuul、Hystrix全上吧!”
出了什么问题
- Eureka集群搭了3个节点,开发环境根本用不到,白白占资源
- Config Server每次改配置要重启服务,不如Nacos好用
- Zuul 1.x性能堪忧,后来换Gateway又改了一遍
- Hystrix已经停更,结果项目上线后发现官方推荐Resilience4j
我的建议
# 技术选型建议(2024版本)注册中心:Nacos(兼顾注册+配置)网关:Spring Cloud Gateway熔断:Resilience4j 或 Sentinel链路追踪:SkyWalking 或 Jaeger配置中心:Nacos Config不要一次性上全套,按需引入。先把服务跑起来,遇到问题再加组件。
三、后悔决定3:分布式事务用了2PC
我当时怎么想的
“跨服务事务一致性很重要,用Seata的AT模式应该没问题吧。”
出了什么问题
// 下单流程:扣库存 + 创建订单 + 扣余额@GlobalTransactionalpublicvoidcreateOrder(OrderDTOdto){inventoryService.deduct(dto.getSkuId(),dto.getQuantity());orderService.create(dto);accountService.debit(dto.getUserId(),dto.getAmount());}问题来了:
- 性能急剧下降:加了全局事务后,TPS从1200降到300
- 锁等待超时:高并发下频繁出现全局锁冲突
- Seata Server单点故障:挂了一次,全部订单卡住
正确做法:最终一致性
大多数业务场景不需要强一致性,最终一致性就够了:
// 方案1:本地消息表 + MQpublicvoidcreateOrder(OrderDTOdto){// 1. 本地事务:创建订单 + 写消息表orderMapper.insert(order);messageMapper.insert(newMessage("deduct_inventory",dto));// 2. 定时任务扫描消息表,发送MQ// 3. 库存服务消费MQ,扣减库存// 4. 扣减成功后回调确认}// 方案2:Saga模式(推荐)// 每个步骤有对应的补偿操作// 扣库存失败 → 补偿:回滚订单我的总结
| 场景 | 推荐方案 |
|---|---|
| 强一致性(转账) | Seata AT/TCC |
| 最终一致性(下单) | 本地消息表 + MQ |
| 长事务 | Saga模式 |
| 简单场景 | 重试 + 幂等 |
四、后悔决定4:服务间通信全用同步HTTP
我当时怎么想的
“用Feign调一下就行了,多简单。”
出了什么问题
// 订单服务调用链OrderService→UserService→AddressService→LogisticsService某天LogisticsService响应变慢(P99从50ms飙到3s),导致整条链路全部超时,引发雪崩效应:
OrderService 线程池打满 → 拒绝所有请求 → 前端504 → 用户疯狂重试 → 更多请求涌入 → 彻底崩溃正确做法:异步化 + 事件驱动
// 改造前:同步调用@FeignClient("logistics-service")publicinterfaceLogisticsClient{@PostMapping("/api/shipment")ShipmentResultcreateShipment(ShipmentRequestrequest);}// 改造后:事件驱动@ServicepublicclassOrderService{@AutowiredprivateRocketMQTemplatemqTemplate;publicvoidonOrderPaid(Orderorder){// 发事件,不等结果mqTemplate.asyncSend("order-paid-topic",newOrderPaidEvent(order.getId(),order.getAddress()));}}// 物流服务订阅事件@RocketMQMessageListener(topic="order-paid-topic")publicclassShipmentListenerimplementsRocketMQListener<OrderPaidEvent>{@OverridepublicvoidonMessage(OrderPaidEventevent){// 异步创建物流单shipmentService.create(event);}}通信方式选择指南
| 场景 | 推荐方式 |
|---|---|
| 需要实时返回结果 | 同步HTTP/gRPC |
| 不关心结果/允许延迟 | MQ异步 |
| 广播通知 | 事件驱动 |
| 高性能内部调用 | gRPC |
五、后悔决定5:没有做好服务可观测性
我当时怎么想的
“先把功能做出来,监控以后再加。”
出了什么问题
上线一周后,用户反馈"下单有时候很慢"。我们排查了两天:
- 看了Nginx日志:请求确实慢
- 看了各服务日志:单个服务都不慢
- 最后发现:是服务之间的网络调用慢,但没有链路追踪,根本定位不到是哪一跳的问题
正确做法:Day 1就要有三板斧
# 可观测性三板斧Metrics(指标):Prometheus + GrafanaLogging(日志):ELK / LokiTracing(链路追踪):SkyWalking / Jaeger最小化接入方案(SkyWalking为例):
<!-- 只需加一个agent,零代码侵入 --><!-- 启动参数 -->-javaagent:/path/to/skywalking-agent.jar -Dskywalking.agent.service_name=order-service -Dskywalking.collector.backend_service=localhost:11800接入后你能看到:
- 每个请求经过了哪些服务
- 每一跳耗时多少
- 哪个服务是瓶颈
- 异常发生在哪个节点
六、拆分微服务的正确姿势(总结)
经过这些坑,我总结了一套微服务拆分的决策框架:
拆分前的灵魂三问
- 你的团队有几个人?3-5人的团队搞微服务就是自找麻烦
- 你的业务复杂度到了吗?如果单体还能hold住,别急着拆
- 你的基础设施Ready了吗?CI/CD、容器化、监控缺一不可
拆分节奏建议
阶段1:单体内模块化(Package by Feature) 阶段2:识别核心域,抽出1-2个独立服务 阶段3:基础设施补齐(注册中心、网关、配置中心) 阶段4:逐步拆分更多服务 阶段5:引入Service Mesh(可选)我的避坑清单
- 不要为了微服务而微服务
- 先定义好服务边界(推荐Event Storming)
- 每个服务独立数据库
- 优先选择最终一致性
- 异步优于同步
- Day 1就接入可观测性
- CI/CD必须自动化
- 做好服务降级和熔断
写在最后
微服务不是银弹,它解决了一些问题,也引入了大量新问题。如果你的团队还小、业务还简单,好的单体远胜于烂的微服务。
但如果你确定要拆,希望我踩的这些坑能帮你少走一些弯路。有问题欢迎评论区交流!
如果这篇文章对你有帮助,别忘了点赞收藏,你的支持是我持续输出的动力!