从单体到微服务,我踩过的三个坑
拆分微服务时,拆分的时序比拆分本身更重要。三个真实事故,以及从中学到的边界划分原则。
"我们要把单体拆成微服务"——这句话听过太多次了。拆完之后呢?往往是一地鸡毛:接口循环依赖、分布式事务到处都是、一个改动要发五个服务。我自己踩过三个大坑,每个都对应一次线上事故。
坑一:按"技术层"拆,而不是按"业务能力"拆
第一次拆服务,我们按技术分层:用户服务管数据访问、订单服务管业务逻辑、通知服务管发消息。听起来很"职责清晰"。
结果是:下一个订单要跨三个服务调用,还引入了分布式事务。
用户服务 ← 订单服务 → 通知服务
(查用户) (建订单) (发通知)
下单这个本该原子的操作,现在散落在三个服务里。通知服务挂了,订单要不要回滚?用户服务超时,订单算不算成功?为了协调这些,我们引入了 Saga,然后花了两个月填分布式事务的坑。
正确做法:按业务能力(vertical slice)拆。下单这件事——用户校验、订单创建、通知发送——如果它们总是一起变,就应该是同一个服务里的同一个事务。只有当某个能力"独立变化、独立伸缩、独立部署"的需求足够强时,才把它拆出去。
判断标准:能不能独立讲清楚"这个服务对什么业务负责"。讲不清,就是拆错了。
坑二:同步调用链太长,雪崩是必然
拆完服务后,一次请求要串行经过 5 个服务。每个服务 99.9% 可用,链路整体可用性是 0.999^5 ≈ 99.5%——听起来还行?但只要任意一个服务变慢,整条链就堵住,线程池迅速耗尽,雪崩。
我们的事故:通知服务因为下游短信厂商抖动,响应从 50ms 变成 3s,上游订单服务的线程池在 8 秒内全部占满,整个下单功能瘫痪。
正确做法:
- 非关键路径异步化。通知这种"最终一致即可"的,用消息队列解耦,下单不等通知。
- 每个调用都要有超时和熔断。没有熔断的调用链就是定时炸弹。我们用 Hystrix(现在会用 resilience4j 或 sentinel)给每个下游调用加超时 + 熔断 + 降级。
- 容量按链路最弱一环算。整条链的容量上限是上游线程池 / 最慢服务的乘积,不是单服务指标。
坑三:共享数据库,拆了服务但没拆数据
这个坑最隐蔽。我们"拆"了服务,代码分仓库了,但为了"省事",几个服务还连着同一个 MySQL 库,各读各的表。
后果:
- 数据库成为隐形的耦合点。一次给 orders 表加字段,三个服务都得改代码、发版本,因为它们都直接读这张表。
- 锁冲突。两个服务对同一张表高频写入,行锁互相阻塞。
- 故障扩散。数据库慢了,所有"拆开"的服务一起慢。
正确做法:每个服务拥有自己的数据存储,不共享数据库。服务间要拿对方数据,走 API,不直连对方的库。这是微服务最硬的一条边界,违反它,所谓"拆服务"就是假的。
数据库迁移是痛苦的一步,但这是从"分布式单体"走向"真微服务"的必经之路。
总结
三个坑,本质是同一件事没做好:边界划分。
- 按技术层拆 → 边界没对齐业务,引入不必要的分布式事务
- 同步长链 → 没划清"关键"和"非关键"路径,缺熔断
- 共享数据库 → 没划清数据所有权,耦合仍在
拆服务不是目的,让团队能独立地、安全地演进各自的业务才是。拆之前先问:这个边界划下去,两边的团队能不能各自独立部署、独立发布、独立故障,而不用天天找对方开会?能,才拆;不能,先别拆。