这是「微服务从踩坑到填坑」系列的第 1 篇。这个系列会完整记录一个团队从单体架构演进到微服务架构的全过程,有决策、有代码、有事故复盘,不讲虚的,只聊实战。


一、那个“该死”的单体

我刚加入团队的时候,整个系统还是一个巨无霸单体应用,俗称“大泥球”。

所有业务模块——用户、订单、商品、库存、营销,全塞在一个 Spring Boot 工程里。编译一次十分钟,启动一次五分钟,本地开发的时候我都不敢随便重启,怕思路被打断。

更让人崩溃的是发布。三十多号人同时往一个仓库里提交代码,每周一次的发布日就像开盲盒。测试环境好好的,一上生产就炸,炸了以后谁也说不清是谁的提交导致的,只能一群人拉群、翻日志、回滚、集体祈祷。

最惨的一次,一个哥们改了个订单状态的枚举值,没通知下游,导致促销模块的校验逻辑全线崩溃。那一天,客服电话被打爆,老板站在我们工位后面,脸色铁青。

那是我第一次在内部技术会议上听到一句话:“我们是不是该拆一拆了?”

二、决定拆分的前夜,我们吵了一架

说实话,团队里想拆的人和不赞成拆的人,几乎五五开。

反对派的理由很充分:

  • “拆了以后分布式事务怎么处理?现在至少事务是稳的。”

  • “网络调用比本地调用慢一个数量级,性能怎么办?”

  • “团队人本来就不多,拆完运维成本谁来扛?”

  • “我们现在的业务逻辑都理不清,强行拆只会更乱。”

我当时是支持拆的,但也承认他们说得对。微服务不是银弹,它不会帮你解决代码烂的问题,只会把烂代码分散到不同的服务里,然后加一层网络调用的复杂度。

最终促使我们下决心的,不是技术,而是一个业务现实——商品和库存这两个模块,需要独立扩容。

每逢大促,商品浏览的 QPS 是平时的 10 倍,但订单创建的 QPS 是平时的 50 倍。在单体架构下,我们没法只扩订单模块,只能整应用扩容。服务器资源被大量浪费在不需要扩容的商品模块上,成本吃得我 CT O 直皱眉。

就是这一点,让反对派也点了头。

三、怎么拆?我们画了三天的白板

决定拆只是第一步,更头疼的是“怎么拆”。

市面上的书会告诉你什么“领域驱动设计”、“限界上下文”、“按业务能力拆分”,听起来特别有道理,但真到落地,你会面对一些特别具体的问题:

问题1:订单表和用户表有外键怎么办?

单体里,查订单的时候顺手 join 一下用户表就能拿到昵称和头像。拆成两个服务以后,这条 SQL 直接跑不通了。你的选择是:让订单服务通过 RPC 调用用户服务补齐信息,还是在订单库里冗余一份用户的基础信息?

我们选择了后者。加了一个 user_snapshot 字段,下单的时候把当时的用户快照存进去。这样做的好处是不依赖用户服务的可用性,缺点是一致性需要自己维护。后面我会单独开一篇讲这个方案的选择和代价。

问题2:拆多细算合适?

有人说一个微服务不能超过 1000 行代码,有人说一个团队对应一个微服务。我们讨论下来,选了一个更务实的标准:一个服务对应一个独立的部署和扩容单元,且其内部的变更不会导致其他服务被迫跟着改。

商品服务和库存服务要不要拆开?我们当时觉得库存变更频率和商品完全不同,且大促时扩容需求差异明显,拆。

用户服务和账户服务要不要拆开?账户涉及余额、积分、会员等级,和用户基础信息耦合紧密,且目前由同一个小组维护,不拆。等哪天账户逻辑复杂到需要独立团队维护了,再拆。

问题3:拆完以后数据库怎么办?

这是当时吵得最凶的问题。有人提出每个服务独立数据库,彻底解耦。也有人反对,说先把数据库放一起,服务层面拆开就行,等跑稳了再拆库。

我们的做法有点折中——先拆服务,数据库共用,但严格禁止跨服务的数据库 join 操作,代码 review 时重点卡这一关。然后逐步把表按服务归属迁移到独立数据库,期间用数据同步做过渡。这个方案不优雅,但能降低前期的拆分风险。

四、我们画的第一个服务拓扑图

吵了三天以后,我们在白板上画出了第一版的服务拆分方案:

```text

┌─────────┐    ┌──────────┐    ┌──────────┐
│ 网关层   │───▶│ 订单服务  │───▶│ 库存服务  │
│ (Gateway)│    └──────────┘    └──────────┘
└─────────┘          │                │
     │               │                │
     │          ┌────▼─────┐    ┌─────▼────┐
     │          │ 用户服务  │    │ 商品服务  │
     │          └──────────┘    └──────────┘
     │               │                │
     │          ┌────▼─────┐          │
     └─────────▶│ 营销服务  │◀─────────┘
                └──────────┘

```

  • 订单服务:核心交易链路,创建订单、状态流转。

  • 库存服务:库存扣减、预约、回补,独立扩容。

  • 用户服务:注册登录、用户信息、地址管理。

  • 商品服务:商品发布、类目、SPU/SKU 管理。

  • 营销服务:优惠券、满减、秒杀活动。

  • 网关层:统一入口,路由、鉴权、限流。

第一期先拆这六个服务,内部先上线,对外接口保持不变。

五、拆分落地后,意外比惊喜多

第一个拆分版本上线那天,我加了一夜的班。原因不是服务挂了,而是——日志找不着了。

单体时代,出了 bug,打开一个日志文件,从请求入口到数据库查询,全链路清清楚楚。拆成六个服务以后,一个请求可能在订单服务里打印了 error,在库存服务里才真正抛异常,但库存服务的日志里又看不到上游订单 ID。那晚我切了六台服务器的日志,才拼出一个完整的调用链。

那一刻我才意识到,微服务不是把代码拆开就完事的,配套的东西不跟上,就是自找苦吃。

这也让我想通了后续几篇文章要写什么:

  • 服务拆开了,怎么互相找到对方?——服务注册与发现

  • 调用链路断了,怎么追踪?——链路追踪

  • 一个服务挂了,怎么不拖垮全家?——熔断降级

  • 日志分散了,怎么集中排查?——日志中心

  • 配置改了,怎么不停机更新?——配置中心

  • 最后,才是服务间通信的主角——RPC

你看,微服务从来不是一个技术决策,而是一个工程体系的抉择。

六、系列开篇的结尾:给犹豫中的你

如果你现在也在纠结要不要拆单体,我送你三句大白话,都是我们用加班换来的:

  1. 业务不复杂到一定程度,别急着拆。 拆了以后运维成本是指数级上升的,不是线性的。

  2. 如果非要拆,先从那个和你团队边界最清晰的模块下手。 别一上来就动核心交易链路,那是找死。

  3. 配套的基础设施先搭好,再拆服务。 日志、监控、链路追踪、CI/CD,这四个东西比你的代码更重要。

这篇文章是这个系列的开篇,讲的都是“思辨”。从下一篇开始,我们就动真格的了——服务注册与发现,手写一个注册中心,看看它到底是怎么工作的。

更多推荐