微服务架构与拆分
讲清微服务"怎么拆、怎么连、怎么保持一致、何时别拆"的决策方法论,与组件用法笔记互补,给出可直接照做的拆分与取舍框架。
一、拆分方法论
1.1 拆分的本质
微服务拆分不是技术动作而是管理复杂度的动作:把"一个大泥球"切成"多个边界清晰的小世界",每个小世界可以独立演进。判断一次拆分成功与否,标准只有一个——是否让某个维度的复杂度下降了(发布更快、故障更小、协作更顺),而不是"服务数量变多了"。
1.2 四个拆分驱动视角
| 视角 | 核心问题 | 适用信号 |
|---|---|---|
| 按业务能力 | 业务上有哪些独立能力域 | 模块间职责重叠、需要独立演进 |
| 按子域(DDD) | 领域边界画在哪 | 规则复杂、需要模型自治(见《DDD领域驱动设计》) |
| 按团队结构 | 谁拥有什么(康威定律) | 团队间提交冲突、发布互相阻塞 |
| 按变更频率 | 哪些代码总是一起变 | 一处改动高频牵连多处回归 |
四个视角不是并列选项而是校验组合:一般先用"业务能力/子域"画出候选边界,再用"团队结构"检查是否有人长期拥有,最后用"变更频率"验证——如果两个候选服务每两个迭代就要一起改,就该合并。
1.3 按业务能力拆分
把业务拆成能力域,如电商:用户、商品、订单、库存、支付、营销、售后。
- 优点:边界直观、业务团队可认领;
- 风险:容易只按"名词"切(订单、商品),忽略"动词/流程"(下单链路、履约链路),造成跨服务编排泛滥。
1.4 按子域拆分(DDD 视角)
以限界上下文为边界来源(详版见《DDD领域驱动设计》),要点:
- 同一限界上下文内的数据由同一服务独占(数据不共享是拆分的底线);
- 一次事务只改一个聚合 → 一个聚合归属一个服务,跨服务事务尽量消灭;
- 上下文间用防腐层/开放主机服务协作。
1.5 按团队结构与康威定律
康威定律:系统结构会镜像组织沟通结构。反向利用它:
- 服务边界与团队归属对齐,一个服务(或一组内聚服务)一个团队长期负责("你拆的你维护");
- 若团队按"前端组/后端组/数据库组"划分,强行上微服务只会得到"分布式的单体加更长的沟通链";
- 反模式:"架构组画完服务边界,分配给各团队"——边界必须由未来的服务 Owner 参与共同决定。
1.6 按变更频率拆分
- 变更频率 + 发布节奏:低频稳定能力(如账号注册)与高频迭代能力(如营销活动)混在一起,是发布冲突的根源;把它们分开,高频服务可独立发版;
- 判断工具:看两个模块的历史共同变更率——共同变更率超过 70% 的模块应属同一服务。
1.7 拆分边界权衡(粒度)
| 维度 | 拆得太粗(大单体) | 拆得太细(微单体粒度过碎) |
|---|---|---|
| 部署 | 发布互相阻塞 | 一次发布涉及几十个服务编排 |
| 数据 | 单一库便于事务 | 跨服务调用链深、延迟叠加 |
| 运维 | 定位问题难 | 实例数爆炸、监控告警疲劳 |
| 成本 | 一个超大库难扩展 | 基础设施成本随服务数线性上涨 |
建议的粒度判据:一个服务应做到"团队规模与业务复杂度匹配 + 能在 1 个迭代内独立发布 + 内部聚合内聚、对外 API 精简"。实践上 3~9 人团队负责 1~3 个服务是比较稳的配比。
1.8 拆分实操步骤
- 从高变化高耦合的模块开始试点(不要一次全拆);
- 先代码库内分包(模块化单体),稳定 2~4 周验证边界;
- 再拆数据库(表归属到服务,是最痛的一步,需处理历史关联查询);
- 服务独立部署、注册进注册中心、切流量灰度;
- 每拆一个就复盘"发布频率/故障半径/团队协作"三个指标是否变好。
1.9 拆分正确性校验清单
- [ ] 每个服务数据独占,无跨服务 join 的强需求;
- [ ] 一次用户请求默认只穿 1~2 个服务(超过 3 个是坏味道);
- [ ] 每个服务能被一个团队长期拥有;
- [ ] 拆分后高频变更服务能独立发版;
- [ ] 没有为拆分引入"分布式事务才能完成"的新用例。
二、服务间通信
2.1 通信方式全景
| 方式 | 典型实现 | 特点 |
|---|---|---|
| 同步 REST | Spring MVC + Feign | 简单直观、易于调试、强实时 |
| 同步 RPC | gRPC / Dubbo | 高性能、强类型、支持流式 |
| 异步消息 | RocketMQ / Kafka / RabbitMQ | 削峰、解耦、最终一致 |
| 事件驱动 | 领域事件 + MQ | 天然贴合 DDD 边界(见 DDD 笔记) |
2.2 同步:REST vs gRPC
| 维度 | REST(HTTP+JSON) | gRPC(HTTP/2+Protobuf) |
|---|---|---|
| 契约 | OpenAPI/Swagger 文档 | .proto 强类型定义 |
| 序列化 | JSON(可读、略慢、体积大) | Protobuf(二进制、快、体积小) |
| 生态 | 浏览器友好、调试工具多 | 需 grpcurl/反射等工具链 |
| 浏览器直连 | 支持 | 不支持(需 grpc-web 网关转换) |
| 适用 | 对外 API、异构系统、调试频繁的场景 | 内部服务间、高吞吐、低延迟、强类型 |
选型结论:对外与跨语言边界用 REST;对性能敏感且工具链齐全的内部南北向/东西向调用用 gRPC。规模不大时统一 REST 更省心。
2.3 异步:消息队列
消息的核心价值是削峰、解耦、异步化(用法与产品对比见《消息队列》笔记)。本笔记强调三个系统性决策:
- 削峰:请求先落 MQ,消费端按能力匀速消费(秒杀等场景见《经典系统设计案例》);
- 解耦:核心链路只发事件,扩展动作(通知、审计、风控)通过订阅接入;
- 缓冲:瞬时流量打平,避免下游被冲垮。
使用 MQ 必须同时设计三种兜底:消息不丢(生产端确认 + 消费端手动 ack)、消息不重(消费幂等)、消息不乱序(分区键按业务主键路由)。
2.4 同步与异步的选型决策
判断顺序:
- 调用方是否必须立刻拿到结果 → 是则同步(或同步 + 异步兜底);
- 结果可容忍延迟(秒级~分钟级)且失败可重试 → 倾向异步;
- 是否需要削峰/解耦/广播 → 异步;
- 是否强一致跨服务写 → 都不行,先看能否合并事务(见第六章)。
一个实用经验:查询走同步、命令走异步。读链路(订单详情聚合)用同步拼装;写链路(下单后发券、发短信、记流水)用事件异步化。
2.5 服务调用链的降级思考
无论同步还是异步,调用链上都要回答三个问题:下游超时怎么办(超时时间 + 重试上限)、下游挂了怎么办(降级预案)、请求激增怎么办(限流/熔断,见《高并发系统设计》第六章)。
三、注册中心与服务发现
3.1 注册中心解决什么问题
服务实例的地址是动态的(扩缩容、故障替换),调用方不能写死 IP。注册中心提供:服务注册、健康检查、地址发现、负载均衡的实例列表来源。是否引入注册中心取决于是否有"动态实例"需求——单机部署或 K8s 场景可交给平台能力。
3.2 主流方案对比
| 维度 | Nacos | Eureka | Consul | Kubernetes(K8s DNS/SVC) |
|---|---|---|---|---|
| 部署形态 | 独立中间件(需高可用集群) | 独立中间件 | 独立中间件 | 平台内置(etcd + DNS + Service) |
| 一致性 | AP + 配置中心(CP 可选) | AP(自我保护) | CP(Raft) | 平台保证 |
| 健康检查 | 心跳 + 主动探测 | 心跳(自我保护) | 心跳 + HTTP/gRPC 探针 | 存活/就绪探针 |
| 配置中心 | 内置(强项) | 无(需配 Spring Cloud Config) | 内置 KV | ConfigMap/外部配置中心 |
| 多环境/命名空间 | Namespace/Group 完善 | 弱 | 弱 | Namespace 按集群 |
| 适用 | Spring Cloud Alibaba 栈、需要配置中心一体 | Spring Cloud Netflix 传统栈 | 多数据中心、对一致性敏感 | 已容器化/K8s 化、无需自建注册中心 |
3.3 注册中心选型决策
- 已容器化并上 K8s → 优先用 K8s 服务发现,不要再自建一套注册中心(除非业务需要 Spring Cloud 生态的配置/网关联动);
- Spring Cloud Alibaba 技术栈 → Nacos(注册 + 配置一体,运维成本最低);
- 存量 Eureka 栈 → 可继续维护,但新服务不建议再引入(维护停滞);
- 需要严格一致的选主/强一致场景 → Consul。
组件配置方法见《SpringCloud微服务》笔记,本笔记不再重复。
3.4 服务发现的常见故障模式
- 脑裂/假死:网络分区时注册中心把健康实例误判——Nacos 的保护阈值、Eureka 的自我保护机制正是为此设计,不要轻易关闭;
- 慢启动被打流量:实例注册即被分配流量,但 JVM 尚未热身——配合预热与最小连接数负载均衡;
- 注册中心抖动雪崩:注册中心不可用时调用方全挂——客户端必须有本地缓存兜底 + 快速失败。
四、网关层与配置中心
4.1 网关的职责清单
网关是南北向流量的统一入口,职责应克制在横切能力:
| 职责 | 说明 | 是否必须 |
|---|---|---|
| 路由转发 | 按 URL/Header 分发到服务 | 是 |
| 统一鉴权 | Token 校验、白名单,不下沉业务逻辑 | 是 |
| 统一限流 | 按 IP/用户/接口维度限流(Sentinel 网关限流用法见 SpringCloud 笔记) | 是(高并发时) |
| 跨域/协议转换 | CORS、HTTP/HTTPS 终止 | 按需 |
| 日志与埋点 | 统一访问日志、Trace ID 注入 | 是 |
网关的禁忌:不要把业务聚合(拼装多个服务响应)、业务校验(订单规则)写进网关——网关会变成第二个单体,且升级成本极高。
4.2 网关 vs BFF
- 网关:横切(鉴权、限流、路由),对所有端统一;
- BFF(Backend For Frontend):按端适配(App 端 / Web 端 / 小程序端各自接口不同),做响应裁剪与聚合,是"应用层"而非"基础设施层"。
多端差异大时在网关后加 BFF 层;差异小时一个网关 + 服务端聚合接口即可,避免每端一个 BFF 导致团队分裂。
4.3 配置中心:管理的不是配置而是变更
配置中心选型(Nacos Config/ Apollo / Spring Cloud Config)见组件笔记,此处讲三个配置管理方法论:
- 分环境隔离:dev/test/prod 用命名空间隔离,禁止 prod 配置与测试配置混用;
- 变更即风险:配置发布要像发代码一样走审批与灰度,避免"改一个配置下线整个集群";
- 配置下沉/收敛:业务开关(功能开关、降级开关)集中在配置中心,配合监控在故障时快速"一键降级";敏感配置(密码、密钥)必须加密存储,不落代码仓库。
五、契约与 API 版本管理
5.1 为什么服务契约要单独管理
服务间通过契约(接口定义)协作,契约变更的破坏面是"所有消费者"。没有契约管理,一次"删个字段"就可能让下游全体报错——架构层的 LSP 要求"提供方演进不得破坏消费者"。
5.2 消费者驱动契约(CDC)
- 由消费者定义自己期望的契约(Consumer-Driven Contract,工具如 Spring Cloud Contract/Pact);
- 提供方用消费者契约做持续校验,发现"我的改动会破坏谁";
- 效果:兼容性问题在 CI 阶段暴露,而不是上线后爆炸。
5.3 API 版本管理策略
| 策略 | 做法 | 适用 |
|---|---|---|
| URI 版本 | /v1/orders、/v2/orders | 对外公开 API 的标准做法,简单直观 |
| Header/媒体类型版本 | Accept: application/vnd.api+json;version=2 | 需要同一 URL 语义收敛时 |
| 兼容性演进(首选) | 只增不改不删:加字段、加默认值、字段可空 | 内部服务间尽量走这条路 |
内部服务间原则:默认做兼容性演进(加字段),确实破坏兼容时走 URI 版本 + 双版本并行 + 消费者迁移 + 旧版本下线(设明确下线日期),同一时刻最多维护两个大版本。
5.4 契约反模式
- 服务间共享同一个"大 DTO"工程(改一处全量重编);
- 直接共享数据库表作为"接口"(表结构即契约,改动即爆炸);
- 无版本、无所有者、无契约测试的裸 HTTP 调用。
六、分布式事务与最终一致性
6.1 挑战的本质:从 CAP 说起
本地事务由单库保证 ACID;跨服务 = 跨库 = 分布式事务,受 CAP 约束:网络分区(P)不可避免时,只能在一致性与可用性间取舍。强一致方案(2PC/XA)牺牲可用性换取 ACID,弱一致方案(SAGA/消息)换取高可用但只能最终一致。
组件落地(Seata 的 AT/TCC/Saga/XA 配置)见《SpringCloud微服务》笔记第六章,本笔记只讲模式取舍与决策。
6.2 强一致:2PC / XA
- 原理:协调者分两阶段(Prepare 全部成功 → Commit;任一失败 → Rollback);
- 优点:业务侵入小(XA 由数据库实现)、语义接近本地事务;
- 致命伤:两阶段期间资源被锁住(阻塞),协调者单点、参与者故障需人工介入,性能与可用性差;
- 适用:极少使用——仅用于并发低、必须强一致、可接受锁时段的场景(如某些对账核心)。互联网高并发业务基本排除。
6.3 TCC(Try-Confirm-Cancel)
- 原理:业务方把每个操作实现成 Try(预占资源)、Confirm(确认提交)、Cancel(回滚释放)三段,Try 成功则最终 Confirm,任一失败则全部 Cancel;
- 优点:无长事务锁、性能较好;
- 代价:侵入最强(每个参与方都要写三段逻辑 + 空回滚/幂等/悬挂处理),开发量最大;
- 适用:强一致要求较高且必须即时、参与方少的核心链路(如跨行资金冻结),典型如"预占 + 确认"语义的资源类操作。
6.4 SAGA(长事务/补偿)
- 原理:把长事务拆成有序本地事务序列,每步成功推进、失败则执行反向补偿(已提交步骤逐一补偿);
- 两种编排:编排式(Choreography)——各服务通过事件互相触发(实现简单但流程隐式);协同式(Orchestration)——中央 SAGA 协调器显式编排步骤(流程可见、易管控,推荐);
- 优点:无锁、高可用、适合长流程;
- 代价:最终一致(中间态对外可见,需业务容忍 + 展示兜底)、补偿逻辑需完整设计(补偿幂等、失败重试);
- 适用:订单履约、跨境汇款等"长流程 + 可补偿 + 容忍最终一致"的业务。
6.5 本地消息表
- 原理:业务本地事务内"写业务 + 写消息表",同库同事务保证不丢;异步任务扫表投递 MQ,消费方幂等消费;投递成功后删除/标记消息。
-- 本地消息表(与业务表同库同事务)
CREATE TABLE t_message_outbox (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
msg_key VARCHAR(64) NOT NULL COMMENT '业务幂等键,如 order:123:paid',
topic VARCHAR(64) NOT NULL,
payload TEXT NOT NULL,
status TINYINT NOT NULL DEFAULT 0 COMMENT '0待投递 1已投递',
retry_count INT NOT NULL DEFAULT 0,
next_retry_at DATETIME NOT NULL,
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
) COMMENT '本地消息表:业务事务与消息同库落盘';-- 本地消息表(与业务表同库同事务)
CREATE TABLE t_message_outbox (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
msg_key VARCHAR(64) NOT NULL COMMENT '业务幂等键,如 order:123:paid',
topic VARCHAR(64) NOT NULL,
payload TEXT NOT NULL,
status TINYINT NOT NULL DEFAULT 0 COMMENT '0待投递 1已投递',
retry_count INT NOT NULL DEFAULT 0,
next_retry_at DATETIME NOT NULL,
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
) COMMENT '本地消息表:业务事务与消息同库落盘';- 优点:不依赖 MQ 的事务特性,任何 MQ 都可用;消息可靠性由 DB 保证;
- 代价:需写扫表任务(可用延迟队列轮询),消息表与业务表耦合在同一库;
- 适用:RocketMQ/Kafka 等不具备事务消息、或团队想自控投递可靠性时的通用兜底方案。
6.6 事务消息(RocketMQ 半消息)
- 原理:先发"半消息"(对消费者不可见),本地事务执行成功后 commit 半消息使其可见,失败则 rollback;本地事务结果未知时靠回查接口兜底;
- 优点:消息可靠性与本地事务原子绑定,无扫表任务;
- 适用:使用 RocketMQ 且希望少维护代码的场景(主流电商下单 → 异步扣减/发券的标准解法);
- 局限:依赖 MQ 产品能力(RocketMQ 支持,Kafka 原生不支持)。
6.7 模式对比与取舍决策
| 方案 | 一致性 | 锁 | 业务侵入 | 性能 | 典型适用 |
|---|---|---|---|---|---|
| 2PC/XA | 强一致 | 长锁 | 低 | 差 | 极少用(低频强一致) |
| TCC | 准强一致 | 无长锁 | 极高 | 较好 | 资金冻结类短操作 |
| SAGA | 最终一致 | 无锁 | 中高(补偿) | 好 | 长流程业务 |
| 本地消息表 | 最终一致 | 无 | 低 | 好 | 通用可靠投递 |
| 事务消息 | 最终一致 | 无 | 低 | 好 | RocketMQ 栈的下单异步化 |
选型决策树:
6.8 实践原则:先想方设法消灭分布式事务
最重要的"方案"是不让分布式事务出现:
- 服务内聚:把高频强一致的操作放在同一服务同一库(如"下单 + 扣库存"强一致要求极高时,先把库存并入订单服务,或库存聚合在订单服务的库内);
- 业务幂等:所有写接口带幂等键(
orderId + action),重试安全; - 对账兜底:最终一致不等于不校验——每日对账 + 差错处理任务(SAGA 补偿失败的最终防线);
- 展示层补偿:中间态(如"支付中")用状态文案引导用户,而不是阻塞等结果。
参考案例:电商"下单"通常是"订单服务本地事务落单 + 事务消息通知库存扣减 + 对账兜底"的组合,而不是一个全局 XA 事务。
七、何时不该上微服务
7.1 不适合微服务的信号
| 信号 | 说明 |
|---|---|
| 团队小于等于 10 人 | 独立服务无人长期维护,治理成本压垮收益 |
| 业务以 CRUD 为主 | 没有复杂协作,单体事务是最大优势 |
| 无独立部署/发布诉求 | 一个版本发全站即可满足 |
| 基础设施空白 | 没有监控、日志、CI/CD,先补基建再谈拆 |
| 强一致业务密集 | 订单-库存-账户必须同一事务且不可最终一致,拆分代价极高 |
| 代码库尚可维护 | 没有实际的耦合痛点,拆分只是想象出来的收益 |
7.2 单体优先与演进路线
正确的路线是"先单体,后按需演进"(详版见《设计原则与架构演进》):
- 单体 + 良好模块化(模块化单体)起步;
- 出现明确的"独立演进 / 独立伸缩 / 独立团队"需求后再拆;
- 从变化最频繁、最独立的模块开始,逐块拆出。
7.3 模块化单体:被低估的中间形态
在代码库内用包边界 + 模块依赖规则(禁止跨模块直接引用 DAO、禁止循环依赖,用 ArchUnit 强约束)模拟服务边界,数据仍在一库便于事务。它拿到了"边界清晰"的大部分收益,却不用支付微服务的全部成本,可作为"拆之前"和"不敢拆"两种场景的共同答案。
7.4 落地误区清单
| 误区 | 后果 |
|---|---|
| 为"技术先进"上微服务 | 治理成本吞掉业务效率 |
| 只拆代码不拆库 | 服务独立了,库还是共享大库,等于没拆 |
| 引入分布式事务解决所有跨服务写 | 用最大复杂度解决本可内聚的问题 |
| 网关写业务逻辑 | 网关膨胀成新单体 |
| 服务间共享数据库/共享 DTO 工程 | 契约失效,牵一发动全身 |
| 没有监控与链路追踪就上线 | 故障定位靠猜 |
| 一次性全量拆分 | 大面积返工风险失控 |
7.5 上微服务前自查清单
- [ ] 存在明确的独立演进/独立伸缩/团队自治需求;
- [ ] 已跑过模块化单体,确认边界稳定;
- [ ] 监控、日志、CI/CD、链路追踪四件套就绪;
- [ ] 每个候选服务都有长期 Owner 团队;
- [ ] 数据归属已明确,无跨服务强 join;
- [ ] 已设计好跨服务一致性的兜底(幂等 + 对账)。
Cao Note