背景与问题
微服务最容易“看起来先进,做起来痛苦”的地方,通常不是技术栈,而是服务到底该怎么拆。
很多团队刚开始做微服务时,往往会经历两个极端:
- 拆得太粗:一个“订单服务”里塞进下单、支付、库存、营销、履约,最后还是一个“分布式单体”
- 拆得太细:一个业务动作要调 6 个服务,链路又长又脆,排查问题像破案
我见过不少项目,初期拆分是按“数据库表”或者“组织架构”来的,短期看开发很快,等业务开始变化时,问题就一起冒出来:
- 服务职责不清,接口越改越乱
- 数据跨服务共享,导致强耦合
- 事务很难处理,补偿逻辑到处都是
- 一个小需求,需要改动多个服务和多张表
- 线上故障时,不知道该怪谁,也不知道从哪查
所以,微服务拆分的核心不是“拆”,而是找到合理的业务边界,并持续治理这些边界。
这篇文章我想从一个更落地的视角来讲这件事:
不是只停留在“DDD 很重要”,而是从领域建模 -> 服务切分 -> 接口协作 -> 生产治理一路串起来,讲清楚在真实项目里怎么做、怎么验证、怎么避坑。
核心原理
1. 服务拆分的本质:按业务变化率和责任边界切
一个服务是不是应该独立,关键不是“代码量大不大”,而是它是否具备以下特征:
- 有明确的业务能力
- 有自己的核心数据
- 有独立的变化节奏
- 可以由相对稳定的团队负责
换句话说,服务不是“功能模块”,而是“业务责任单元”。
比如一个电商系统里:
- 用户:注册、登录、身份资料
- 商品:商品信息、上下架、类目
- 库存:可售库存、预扣、回补
- 订单:下单、取消、状态流转
- 支付:支付单、支付状态、回调处理
这些能力天然有边界,但在很多项目里,经常会被混着实现。最典型的是:订单服务顺手管库存,支付服务顺手改订单状态,营销服务顺手写订单优惠明细。短期方便,长期失控。
2. 从领域建模开始,而不是从接口开始
很多团队做微服务拆分,第一步就是画接口清单。这往往会把问题做反。
更好的方式是先回答三个问题:
- 核心业务对象是什么?
- 每个对象由谁负责?
- 谁能修改,谁只能引用?
这其实就是领域建模里常说的几个概念:
- 实体(Entity):有身份标识,会持续变化,比如订单、用户
- 值对象(Value Object):描述属性组合,比如收货地址、金额
- 聚合(Aggregate):一组必须保持一致性的对象集合
- 限界上下文(Bounded Context):业务语义一致的边界
其中最关键的是:限界上下文常常就是服务边界的候选者。
3. 一个实用判断标准:谁拥有数据,谁定义规则
边界不清时,我一般会用一个很有效的问题去判断:
这条业务规则,究竟应该由谁说了算?
例如:
- “库存不能为负” —— 这是库存域规则,应由库存服务保证
- “订单待支付 30 分钟关闭” —— 这是订单域规则,应由订单服务保证
- “支付成功后订单改为已支付” —— 支付服务只负责发布结果,订单服务负责消费结果并变更自身状态
这就是边界治理里很重要的一条原则:
服务只维护自己的真相(Source of Truth)
不要让多个服务同时维护同一份业务真相,否则早晚会打架。
4. 一个电商示例:从领域到服务边界
先看一个简化的上下文拆分图。
flowchart LR
U[用户域] --> O[订单域]
P[商品域] --> O
I[库存域] --> O
O --> Pay[支付域]
O --> M[营销域]
O --> F[履约域]
U -. 提供用户身份与地址快照 .-> O
P -. 提供商品信息快照 .-> O
I -. 负责库存锁定与释放 .-> O
Pay -. 发布支付结果 .-> O
M -. 提供优惠试算结果 .-> O
F -. 接收已支付订单进入发货流程 .-> O
这里有一个非常重要的落地原则:
订单域不应该实时依赖所有外部信息来“活着”。
比如下单时,订单可以保留必要快照:
- 用户收货地址快照
- 商品名称/价格快照
- 优惠金额快照
这样做的好处是:
- 订单历史可追溯
- 外部服务改数据不会污染已创建订单
- 链路依赖减少,系统韧性更好
5. 事务边界:强一致只留在单服务内
微服务拆分后,最常见的焦虑就是:“以前一个事务能搞定,现在分成几个服务怎么办?”
答案很朴素:
- 单服务内部:尽量维持本地事务
- 跨服务之间:接受最终一致性,用事件、补偿、幂等来保证
不要一上来就追求分布式强一致。绝大多数业务并不值得付出这个复杂度。
一个典型下单流程如下:
sequenceDiagram
participant C as Client
participant O as 订单服务
participant I as 库存服务
participant P as 支付服务
participant MQ as 消息总线
C->>O: 创建订单
O->>I: 预扣库存
I-->>O: 预扣成功
O->>O: 本地事务保存订单
O->>MQ: 发布 OrderCreated
C->>P: 发起支付
P->>P: 支付成功
P->>MQ: 发布 PaymentSucceeded
MQ-->>O: PaymentSucceeded
O->>O: 更新订单状态为已支付
O->>MQ: 发布 OrderPaid
这里的关键不是“全程不出错”,而是:
- 每个步骤都可重试
- 每个事件都幂等
- 每个状态都可追踪
- 失败后有补偿路径
方案对比与取舍分析
微服务拆分没有绝对标准答案,只有更适合当前阶段的方案。
1. 常见拆分方式对比
| 拆分方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 按技术层拆分 | 易上手 | 业务割裂严重 | 不建议用于微服务 |
| 按数据库表拆分 | 实施快 | 极易形成数据耦合 | 过渡期勉强可用 |
| 按组织架构拆分 | 沟通顺畅 | 架构随人员变化 | 短期有效,长期危险 |
| 按业务能力拆分 | 边界清晰 | 前期建模成本高 | 推荐 |
| 按领域/限界上下文拆分 | 可持续演进 | 需要业务理解深 | 最推荐 |
2. 应该拆到多细?
这是一个高频问题。我的经验是:先粗后细,比先细后崩更稳。
判断是否需要继续拆分,可以看这些信号:
适合进一步拆分的信号
- 某个服务内部存在两套明显不同的业务规则
- 同一服务由不同团队频繁冲突改动
- 发布频率差异很大,互相拖累
- 数据模型已经明显不一致
- 性能瓶颈集中在一个子能力上
暂时不要拆的信号
- 只是代码文件太多,但业务仍高度一致
- 团队规模很小,拆分后沟通成本更高
- 事务要求极强,拆完反而要造大量补偿逻辑
- 业务还在快速探索期,边界尚未稳定
一句话总结:
不要把“服务数量增长”误当成“架构升级”。
实战代码(可运行)
下面用一个简化但能运行的例子,演示“订单服务”和“库存服务”的边界设计,以及如何用事件实现最终一致性。
为了便于直接运行,我用 Python Flask 模拟两个服务和一个简单事件总线。示例重点不在框架,而在边界与状态流转。
1. 目录结构
microservice-demo/
├── inventory_service.py
├── order_service.py
└── requirements.txt
2. 安装依赖
pip install flask requests
3. 库存服务
职责非常单一:
- 管理库存真相
- 执行预扣与释放
- 不直接改订单状态
# inventory_service.py
from flask import Flask, request, jsonify
app = Flask(__name__)
inventory = {
"SKU-1": {"available": 10, "reserved": 0},
"SKU-2": {"available": 5, "reserved": 0},
}
reservation_log = set()
@app.route("/inventory/<sku>", methods=["GET"])
def get_inventory(sku):
item = inventory.get(sku)
if not item:
return jsonify({"error": "sku not found"}), 404
return jsonify({"sku": sku, **item})
@app.route("/reserve", methods=["POST"])
def reserve():
data = request.json
request_id = data["request_id"]
sku = data["sku"]
qty = data["qty"]
if request_id in reservation_log:
return jsonify({"message": "idempotent success"}), 200
item = inventory.get(sku)
if not item:
return jsonify({"error": "sku not found"}), 404
if item["available"] < qty:
return jsonify({"error": "insufficient inventory"}), 409
item["available"] -= qty
item["reserved"] += qty
reservation_log.add(request_id)
return jsonify({
"message": "reserved",
"sku": sku,
"available": item["available"],
"reserved": item["reserved"]
}), 200
@app.route("/release", methods=["POST"])
def release():
data = request.json
sku = data["sku"]
qty = data["qty"]
item = inventory.get(sku)
if not item:
return jsonify({"error": "sku not found"}), 404
if item["reserved"] < qty:
return jsonify({"error": "reserved inventory not enough"}), 409
item["reserved"] -= qty
item["available"] += qty
return jsonify({
"message": "released",
"sku": sku,
"available": item["available"],
"reserved": item["reserved"]
}), 200
if __name__ == "__main__":
app.run(port=5001, debug=True)
4. 订单服务
职责:
- 创建订单
- 协调库存预扣
- 管理自己的订单状态
- 接收支付成功事件并更新状态
# order_service.py
from flask import Flask, request, jsonify
import requests
import uuid
app = Flask(__name__)
orders = {}
processed_events = set()
INVENTORY_URL = "http://127.0.0.1:5001"
@app.route("/orders", methods=["POST"])
def create_order():
data = request.json
sku = data["sku"]
qty = data["qty"]
user_id = data["user_id"]
order_id = str(uuid.uuid4())
reserve_request_id = f"reserve-{order_id}"
reserve_resp = requests.post(f"{INVENTORY_URL}/reserve", json={
"request_id": reserve_request_id,
"sku": sku,
"qty": qty
})
if reserve_resp.status_code != 200:
return jsonify({
"error": "inventory reserve failed",
"detail": reserve_resp.json()
}), 409
orders[order_id] = {
"order_id": order_id,
"user_id": user_id,
"sku": sku,
"qty": qty,
"status": "CREATED"
}
return jsonify(orders[order_id]), 201
@app.route("/orders/<order_id>", methods=["GET"])
def get_order(order_id):
order = orders.get(order_id)
if not order:
return jsonify({"error": "order not found"}), 404
return jsonify(order)
@app.route("/events/payment-succeeded", methods=["POST"])
def on_payment_succeeded():
event = request.json
event_id = event["event_id"]
order_id = event["order_id"]
if event_id in processed_events:
return jsonify({"message": "duplicate event ignored"}), 200
order = orders.get(order_id)
if not order:
return jsonify({"error": "order not found"}), 404
if order["status"] == "PAID":
processed_events.add(event_id)
return jsonify({"message": "already paid"}), 200
order["status"] = "PAID"
processed_events.add(event_id)
return jsonify({
"message": "order marked as paid",
"order": order
}), 200
@app.route("/orders/<order_id>/cancel", methods=["POST"])
def cancel_order(order_id):
order = orders.get(order_id)
if not order:
return jsonify({"error": "order not found"}), 404
if order["status"] == "PAID":
return jsonify({"error": "paid order cannot be cancelled here"}), 409
if order["status"] == "CANCELLED":
return jsonify({"message": "already cancelled"}), 200
release_resp = requests.post(f"{INVENTORY_URL}/release", json={
"sku": order["sku"],
"qty": order["qty"]
})
if release_resp.status_code != 200:
return jsonify({
"error": "inventory release failed",
"detail": release_resp.json()
}), 500
order["status"] = "CANCELLED"
return jsonify(order), 200
if __name__ == "__main__":
app.run(port=5000, debug=True)
5. 运行与验证
先启动库存服务:
python inventory_service.py
再启动订单服务:
python order_service.py
创建订单
curl -X POST http://127.0.0.1:5000/orders \
-H "Content-Type: application/json" \
-d '{"user_id":"U1001","sku":"SKU-1","qty":2}'
返回示例:
{
"order_id": "c2a8d2b3-76f4-4d16-8181-45ed31c8fdbf",
"qty": 2,
"sku": "SKU-1",
"status": "CREATED",
"user_id": "U1001"
}
查看库存
curl http://127.0.0.1:5001/inventory/SKU-1
模拟支付成功事件
把上一步返回的 order_id 替换进去:
curl -X POST http://127.0.0.1:5000/events/payment-succeeded \
-H "Content-Type: application/json" \
-d '{"event_id":"evt-10001","order_id":"c2a8d2b3-76f4-4d16-8181-45ed31c8fdbf"}'
验证幂等
重复发送同一个事件:
curl -X POST http://127.0.0.1:5000/events/payment-succeeded \
-H "Content-Type: application/json" \
-d '{"event_id":"evt-10001","order_id":"c2a8d2b3-76f4-4d16-8181-45ed31c8fdbf"}'
这时订单服务不会重复处理。
边界治理的落地方法
上面的示例只是“能跑起来”,但生产环境里真正拉开差距的,是边界治理是否长期有效。
1. 定义清晰的服务契约
每个服务要明确三件事:
- 我对外提供什么能力
- 我拥有什么数据
- 我绝不负责什么
例如订单服务可以写成这样的边界说明:
| 项目 | 内容 |
|---|---|
| 核心职责 | 订单创建、状态流转、订单快照维护 |
| 拥有数据 | 订单主表、订单项、订单状态历史 |
| 输入依赖 | 用户信息快照、商品快照、库存预扣结果、支付结果 |
| 不负责 | 库存余额维护、支付渠道交互、商品主数据管理 |
这个动作看似“文档化”,其实很重要。很多边界混乱,根本不是代码问题,而是团队里默认共识不存在。
2. 通过状态机约束业务演进
订单这类核心对象,建议一定要有显式状态机,否则状态会越改越随意。
stateDiagram-v2
[*] --> CREATED
CREATED --> PAID: 支付成功
CREATED --> CANCELLED: 超时关闭/主动取消
PAID --> FULFILLING: 进入履约
FULFILLING --> COMPLETED: 确认收货
PAID --> REFUNDING: 发起退款
REFUNDING --> REFUNDED: 退款完成
CANCELLED --> [*]
COMPLETED --> [*]
REFUNDED --> [*]
我的建议是:
- 状态流转必须集中管理
- 非法状态变更要直接拒绝
- 每个状态变化都记录事件和时间
这样线上问题一出来,至少能快速回答:
“订单到底在哪一步坏掉了?”
3. 用反腐层隔离外部模型污染
如果你的订单服务直接使用支付服务、营销服务、CRM 系统传来的对象模型,边界迟早会被侵蚀。
更稳妥的做法是:
- 外部系统模型进入本服务时先转换
- 本服务内部只认自己的领域模型
- 外部字段变化不会直接冲击内部逻辑
这个模式就是常说的反腐层(ACL)。
常见坑与排查
这一部分我尽量讲得“生产一点”。这些坑,我基本都见过。
坑 1:按数据库拆服务,结果共享库剪不断
现象
- 多个服务连同一个库
- 一个字段变更,所有服务都可能受影响
- 看似是微服务,实则只是“远程调用版单体”
排查方式
- 检查是否存在跨服务共享表
- 检查服务是否直接查别人的核心表
- 检查 SQL 是否写进了跨域逻辑
建议
- 每个服务独立数据库或独立 schema
- 跨服务数据通过 API、事件或只读投影同步
- 不要让“查库更快”成为破坏边界的借口
坑 2:一个请求串行调用太多服务
现象
- 下单链路要依次调用户、商品、库存、营销、风控、支付预检查
- RT 越来越长
- 某个依赖抖一下,整个链路就超时
排查方式
- 画调用链拓扑
- 看 P95/P99 延迟
- 看失败是不是都集中在某几个下游超时
建议
- 能前置缓存的前置缓存
- 能快照的快照
- 能异步的异步
- 核心同步链路只保留“必须当场决策”的步骤
一句很实用的话:
不是所有正确性都要靠实时远程调用来换。
坑 3:事件消费重复,导致状态错乱
现象
- 支付成功事件被重复投递
- 订单多次扣积分、多次发券、多次发货
排查方式
- 检查消费者是否记录
event_id - 检查处理逻辑是否天然幂等
- 检查消息重试机制与超时机制
建议
- 每个事件必须有唯一 ID
- 消费侧必须有去重表/去重缓存
- 状态迁移要基于当前状态做校验
坑 4:把“公共服务”做成“超级服务”
现象
为了复用,团队会造一个“大中台服务”:
- 用户信息也放这
- 字典配置也放这
- 营销规则也放这
- 审批流也放这
最后所有系统都依赖它,它成了新的单点和瓶颈。
排查方式
- 看依赖入度是否异常高
- 看其职责是否持续膨胀
- 看发布一次是否需要全公司回归
建议
“公共”不等于“中心化大一统”。
真正适合公共化的,往往是:
- 认证授权
- 配置中心
- 消息基础设施
- 可观测性平台
而不是把所有业务都揉进去。
坑 5:边界一开始合理,后续迭代被慢慢打穿
现象
- 订单服务开始加库存字段
- 库存服务开始提供订单查询
- 支付服务开始改用户资产
- 每次都是“先这么搞,后面再收敛”
最后就没有后面了。
排查方式
可以定期做一次边界审计:
- 是否新增了跨域字段写入
- 是否出现了跨域 SQL
- 是否有不合理的同步强依赖
- 是否有服务接口语义开始模糊
建议
- 架构评审不只评“新建服务”,也要评“边界变更”
- 建立 API owner 机制
- 对跨域改动设置更高的审核门槛
安全/性能最佳实践
微服务落地到生产环境,边界治理不只关乎设计优雅,也直接影响安全性和性能。
1. 安全最佳实践
身份认证与服务间授权
- 外部流量统一从 API Gateway 进入
- 用户身份认证建议用 OAuth2 / JWT
- 服务间调用不要裸奔,至少做 mTLS 或签名校验
- 不同服务账号最小权限分配
数据隔离
- 每个服务使用独立数据库账号
- 不给服务访问其他服务数据库的权限
- 敏感字段如手机号、身份证号要脱敏或加密存储
事件与接口防重放
- 请求使用唯一请求 ID
- 事件使用唯一事件 ID
- 回调接口增加签名、时间戳、重放窗口限制
2. 性能最佳实践
减少同步依赖
- 商品展示类信息尽量缓存
- 非关键路径改为异步事件
- 聚合查询通过读模型解决,而不是链路实时拼装
做好容量估算
一个简单估算方法:
假设:
- 峰值 QPS:500
- 下单链路平均调用 3 个同步服务
- 每个调用平均 30ms
- 峰值放大系数:2
那么链路总请求量大致为:
500 * 3 * 2 = 3000 次内部请求/秒
这还没算重试、超时和突发流量。所以拆服务前要意识到:
微服务不是把压力变小,而是把压力分散到了网络、序列化、连接池、消息系统和治理平台上。
关键治理项
- 连接池大小合理配置
- 熔断、限流、超时必须显式设置
- 避免无上限重试
- 核心接口做压测和降级预案
下面是一个简化的治理关系图:
flowchart TD
A[客户端请求] --> G[API Gateway]
G --> O[订单服务]
O --> R[注册中心]
O --> C[配置中心]
O --> T[链路追踪]
O --> M[监控告警]
O --> MQ[消息队列]
O --> I[库存服务]
O --> P[支付服务]
I --> M
P --> M
MQ --> T
这张图想表达的是:
生产环境里的微服务,真正复杂的往往不是业务代码,而是治理设施是否跟得上。
生产落地建议
如果你所在团队正在从单体走向微服务,我建议按下面的节奏推进,而不是一次性大拆特拆。
第一阶段:先识别领域边界
输出物至少包括:
- 核心业务流程图
- 主要领域对象
- 限界上下文划分
- 服务职责边界说明
第二阶段:优先拆高收益模块
优先考虑这些模块:
- 变化频繁且独立
- 性能瓶颈明显
- 团队边界清晰
- 对其他模块侵入小
例如订单、库存、支付,通常就比“报表服务”更值得先认真设计。
第三阶段:补齐治理基础设施
至少要有:
- 日志集中化
- 链路追踪
- 指标监控
- 消息可靠投递机制
- 配置中心
- 灰度与回滚能力
第四阶段:持续做边界审计
建议每个季度回看一次:
- 是否新增不合理同步依赖
- 是否出现共享库/共享表
- 是否有数据归属争议
- 是否有某个服务膨胀成“万能服务”
微服务架构不是画完图就结束,而是一个持续治理过程。
总结
服务拆分做得好不好,核心不在“拆了多少个服务”,而在于:
- 是否从领域建模出发识别边界
- 是否明确了每个服务的数据归属和职责
- 是否把强一致性收敛在单服务内
- 是否通过事件、幂等、状态机支撑最终一致性
- 是否在生产环境持续治理边界,而不是放任侵蚀
如果让我给中级工程师一个最可执行的建议,我会给这 5 条:
- 先按业务能力拆,不要按表拆
- 谁拥有数据,谁定义规则
- 同步链路只保留必须实时决策的动作
- 跨服务交互默认考虑幂等、重试和补偿
- 每个季度做一次边界审计,防止系统重新长回单体
最后补一句很现实的话:
微服务不是银弹,边界也不是一次定终身。好的架构,往往不是“最优拆分”,而是在业务变化中还能稳住秩序的拆分。这才是真正能落到生产环境里的微服务能力。