背景与问题
很多团队做微服务时,真正难的不是“把服务拆开”,而是“拆成什么样才不会越拆越乱”。
我见过两种很典型的失败案例:
- 按技术层拆分:用户服务、订单服务、DAO 服务、公共服务……看起来很规整,实际上调用链特别长,一个下单动作能串 6 个服务。
- 按数据库表拆分:一张表一个服务,边界清晰得像教科书,但业务一变,跨服务事务和联表查询立刻把系统拖垮。
所以,服务拆分的核心从来不是“把代码切文件夹”,而是围绕业务能力和领域边界重组系统。
如果边界划分不对,微服务会带来这些问题:
- 服务之间强依赖,发布必须一起发
- 一个业务需求要改 4~5 个仓库
- 数据冗余失控,口径不一致
- 链路变长,性能和排障都更难
- 团队职责模糊,出现“这个需求到底谁改”的扯皮
这篇文章我会从单体系统出发,给出一套偏实战的方法:如何识别领域、划定边界、拆分服务、设计通信方式,并通过一段可运行代码演示落地思路。
背景案例:从单体电商系统说起
假设我们有一个典型单体系统,功能包含:
- 用户注册、登录
- 商品管理
- 下单
- 支付
- 库存扣减
- 订单查询
- 营销优惠券
单体阶段通常没什么问题,代码都在一个仓库里,事务也好做。但当业务增长后,会出现几个信号:
- 订单相关需求迭代非常快,发布频率高
- 商品和搜索流量大,但订单逻辑更重
- 支付对稳定性要求高,不想被营销功能拖着一起发
- 不同团队开始独立负责不同业务域
这时如果还维持一个单体,就会越来越吃力。
核心原理
1. 服务拆分不是目的,边界稳定才是目的
微服务的目标不是“多”,而是让变化被局部吸收。
一个好的服务边界,应该满足:
- 大多数业务变更只落在一个服务内
- 服务内部高内聚,外部低耦合
- 团队可以独立开发、测试、部署
- 数据 ownership 明确,一个核心数据只由一个服务主写
一句更接地气的话:别让一个正常需求改遍全场。
2. 用领域模型而不是菜单结构来拆服务
很多系统初看会按页面菜单拆,比如“订单管理”“商品管理”“会员中心”。
但菜单是产品视角,不一定是领域视角。
更靠谱的方法是从领域模型出发:
- 核心域:最能体现业务竞争力的部分
- 支撑域:支持核心业务运转
- 通用域:相对标准化、可复用能力
以电商为例,可以这样理解:
- 核心域:订单、履约、定价
- 支撑域:库存、支付、营销
- 通用域:用户、通知、认证
这一步不是为了画图好看,而是决定资源投入和边界优先级。
3. 识别限界上下文(Bounded Context)
服务边界最常见的落地方式,是先找限界上下文。
简单理解:同一个词,在不同上下文里语义可能不同。
例如“订单”:
- 在交易域里,订单关心金额、状态、支付信息
- 在履约域里,订单关心发货、签收、退货
- 在客服域里,订单关心售后、投诉、工单
如果把这些都塞在一个“订单中心”里,最后一定变成超级服务。
更合理的方式是把“订单”放在不同上下文里建模,各自只关心自己的职责。
flowchart LR
A[单体系统] --> B[识别业务能力]
B --> C[提取领域对象]
C --> D[识别上下文边界]
D --> E[定义服务职责]
E --> F[确定数据归属]
F --> G[设计同步/异步通信]
G --> H[按业务增量拆分]
4. 四个实战判断标准:什么时候该拆,怎么拆
判断标准一:变更是否同频
如果两个模块总是一起改,说明它们可能属于同一边界。
如果一个模块高频变化,另一个非常稳定,拆分价值就高。
例子:
- 营销规则经常变,支付流程很稳
→ 不应该强耦合在一个服务里
判断标准二:是否需要独立扩缩容
- 商品查询、搜索:读多写少,需要高并发
- 订单创建:事务重、逻辑复杂
- 支付:外部依赖多,稳定性优先
这三者负载模型完全不同,通常适合拆开。
判断标准三:数据是否天然聚合
如果一个业务动作需要强一致事务、频繁联表、必须同时提交,先别急着拆。
拆早了,只是把数据库事务问题变成分布式事务问题。
判断标准四:团队职责是否清晰
服务边界最好和团队责任边界对齐。
如果“订单服务”需要三个团队共同维护,那这个边界大概率有问题。
方案对比:常见拆分方式的优劣
1. 按技术层拆分
示例
- Controller 服务
- Service 服务
- Repository 服务
问题
这是我最不推荐的一种。它把一个完整业务流程人为切碎,导致每次请求都穿越多个网络调用。
本质上是把单体里的函数调用,替换成了更慢、更脆弱的 RPC。
2. 按实体对象拆分
示例
- 用户服务
- 订单服务
- 商品服务
- 库存服务
优点
- 好理解,容易入门
- 组织结构清晰
问题
如果只按实体名拆,不看业务职责,很容易出现“订单服务什么都管”的大胖服务。
3. 按业务能力拆分
示例
- 下单服务
- 支付服务
- 履约服务
- 营销服务
- 商品目录服务
优点
- 更贴近真实业务流
- 服务职责更聚焦
- 更容易独立演进
成本
- 建模门槛更高
- 初期需要较多业务梳理
如果团队已经过了“先跑起来”的阶段,优先考虑按业务能力和领域上下文拆分。
一个可落地的拆分路径
这里给出一个我比较认可的迁移路线,不求一步到位,但求每一步都能产生正向收益。
第一步:先识别单体中的模块边界
不要一上来就拆库拆服务,先做一张图:
- 主要业务能力有哪些
- 哪些模块变更最频繁
- 哪些模块有独立数据
- 哪些模块依赖外部系统
classDiagram
class Monolith {
+UserModule
+CatalogModule
+OrderModule
+PaymentModule
+InventoryModule
+PromotionModule
}
class OrderModule {
+createOrder()
+payOrder()
+cancelOrder()
}
class PaymentModule {
+pay()
+refund()
}
class InventoryModule {
+reserve()
+deduct()
+release()
}
Monolith --> OrderModule
Monolith --> PaymentModule
Monolith --> InventoryModule
第二步:确定“先拆谁”
建议先拆满足以下特征的模块:
- 边界相对清晰
- 与核心链路耦合没那么深
- 独立收益明显
- 失败影响可控
通常是:
- 通知服务
- 营销服务
- 搜索服务
- 报表服务
而不是一上来就拆订单核心交易链路。
第三步:定义数据归属
这是微服务成败的关键点。
要建立一个原则:一个业务主数据只能有一个服务负责写入。
例如:
- 订单主状态:订单服务主写
- 支付流水:支付服务主写
- 库存锁定与扣减:库存服务主写
其他服务如果需要这些数据:
- 查询副本
- 缓存镜像
- 事件订阅构建读模型
而不是直接连别人的数据库。
第四步:区分同步调用和异步事件
经验上可以这样判断:
用同步调用的场景
- 用户请求链路中必须立即得到结果
- 强依赖下游返回值才能继续
- 失败需要立刻感知
例如:
- 下单时校验商品是否可售
- 支付时实时确认支付结果
用异步事件的场景
- 最终一致即可
- 下游动作可以延迟
- 希望降低链路耦合
例如:
- 订单已创建后通知库存锁定
- 支付成功后通知积分发放
- 订单完成后发送短信
sequenceDiagram
participant U as 用户
participant O as 订单服务
participant P as 支付服务
participant I as 库存服务
participant M as 消息总线
U->>O: 创建订单
O->>I: 同步校验/预占库存
I-->>O: 返回结果
O-->>U: 订单创建成功
U->>P: 发起支付
P-->>U: 支付受理
P->>M: 发布 PaymentSucceeded
M->>O: 更新订单为已支付
M->>I: 确认扣减库存
实战代码(可运行)
下面用一个简化示例演示:
我们用 Python 和 Flask 模拟一个“订单服务”,展示如何把核心交易逻辑留在订单边界内,并通过事件驱动通知其他上下游。
这不是完整生产代码,但可以运行,能帮助你理解边界落地方式。
目录结构
microservice-demo/
├── app.py
├── requirements.txt
requirements.txt
flask==2.3.3
app.py
from flask import Flask, request, jsonify
from dataclasses import dataclass, asdict
from typing import Dict, List
from uuid import uuid4
app = Flask(__name__)
# -------------------------
# 领域模型
# -------------------------
@dataclass
class OrderItem:
product_id: str
quantity: int
price: float
@dataclass
class Order:
order_id: str
user_id: str
items: List[OrderItem]
status: str
total_amount: float
# -------------------------
# 仓储(内存实现)
# -------------------------
orders: Dict[str, Order] = {}
# 简化库存数据:真实系统里应该由库存服务负责主写
inventory_read_model = {
"sku-1": 10,
"sku-2": 5
}
event_bus = []
# -------------------------
# 应用服务
# -------------------------
def calculate_total(items: List[OrderItem]) -> float:
return sum(item.price * item.quantity for item in items)
def validate_inventory(items: List[OrderItem]) -> None:
for item in items:
stock = inventory_read_model.get(item.product_id, 0)
if stock < item.quantity:
raise ValueError(f"商品 {item.product_id} 库存不足,剩余 {stock}")
def create_order(user_id: str, items_payload: List[dict]) -> Order:
items = [OrderItem(**item) for item in items_payload]
validate_inventory(items)
total_amount = calculate_total(items)
order = Order(
order_id=str(uuid4()),
user_id=user_id,
items=items,
status="CREATED",
total_amount=total_amount
)
orders[order.order_id] = order
# 发布领域事件,通知其他服务
event_bus.append({
"event_type": "OrderCreated",
"order_id": order.order_id,
"user_id": user_id,
"total_amount": total_amount
})
return order
def mark_order_paid(order_id: str) -> Order:
order = orders.get(order_id)
if not order:
raise ValueError("订单不存在")
if order.status != "CREATED":
raise ValueError(f"当前状态 {order.status} 不允许支付")
order.status = "PAID"
event_bus.append({
"event_type": "OrderPaid",
"order_id": order.order_id
})
return order
# -------------------------
# 接口层
# -------------------------
@app.route("/orders", methods=["POST"])
def http_create_order():
payload = request.get_json()
try:
order = create_order(
user_id=payload["user_id"],
items_payload=payload["items"]
)
return jsonify({
"success": True,
"data": serialize_order(order)
}), 201
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 400
@app.route("/orders/<order_id>/pay", methods=["POST"])
def http_pay_order(order_id):
try:
order = mark_order_paid(order_id)
return jsonify({
"success": True,
"data": serialize_order(order)
})
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 400
@app.route("/orders/<order_id>", methods=["GET"])
def http_get_order(order_id):
order = orders.get(order_id)
if not order:
return jsonify({"success": False, "error": "订单不存在"}), 404
return jsonify({"success": True, "data": serialize_order(order)})
@app.route("/events", methods=["GET"])
def http_get_events():
return jsonify({"success": True, "data": event_bus})
def serialize_order(order: Order):
result = asdict(order)
result["items"] = [asdict(item) for item in order.items]
return result
if __name__ == "__main__":
app.run(debug=True)
运行方式
python -m venv venv
source venv/bin/activate # Windows 可改用 venv\Scripts\activate
pip install -r requirements.txt
python app.py
服务默认运行在 http://127.0.0.1:5000。
创建订单
curl -X POST http://127.0.0.1:5000/orders \
-H "Content-Type: application/json" \
-d '{
"user_id": "u-1001",
"items": [
{"product_id": "sku-1", "quantity": 2, "price": 99.0}
]
}'
支付订单
curl -X POST http://127.0.0.1:5000/orders/{order_id}/pay
查看事件
curl http://127.0.0.1:5000/events
代码里体现了哪些边界原则
上面这个例子虽然简化,但有几个关键点值得注意。
1. 订单状态由订单服务主写
CREATED -> PAID 的状态流转,只在订单服务内完成。
别的服务如果想知道订单是否已支付,不应直接改订单表。
2. 库存只作为读模型参与校验
示例里用了 inventory_read_model,是为了表达一个思想:
订单服务可以读取库存视图做决策,但库存扣减的真实写入职责应归库存服务。
3. 通过领域事件传播事实,而不是共享数据库
OrderCreated、OrderPaid 这种事件,本质上是在说:
- “我这里发生了什么”
- 而不是“你们应该怎么做”
这会让服务之间的耦合低很多。
常见坑与排查
坑一:拆成了“分布式单体”
现象
- 服务很多,但必须一起发布
- 接口改一个字段,多个服务同时改
- 一个请求要同步串行调很多下游
原因
服务拆开了,边界没拆开。
表面上是微服务,实际上依赖关系还是单体式的。
排查方法
看这几个指标:
- 一个用户请求平均跨几个服务
- 一个需求平均修改几个仓库
- 发布失败是否会影响多个服务回滚
如果三个指标都偏高,就要反思边界设计。
坑二:把数据库当集成方式
现象
- A 服务直接查 B 服务数据库
- 为了“方便联查”,多个服务共享一套表
- 数据改动无法追踪来源
后果
这类系统表面拆了服务,实际数据仍然强耦合。
最后数据库 schema 成了最大的共享契约,谁都不敢动。
排查建议
- 检查数据库账号权限是否做了隔离
- 检查是否存在跨服务 SQL
- 看报表需求是否总是逼业务库开只读权限
坑三:过早追求“绝对正确”的边界
现象
项目还没进入稳定期,就投入很大成本做领域建模和大规模拆分。
问题
边界不是会议里一次性设计出来的,它往往是在持续迭代中被验证出来的。
早期业务不稳定时,过度精细拆分只会增加沟通和维护成本。
我的建议
先从明显高收益边界开始拆,保留演进空间,不要求一开始就“教科书级正确”。
坑四:同步调用链过长
现象
下单接口依赖:
- 用户服务
- 商品服务
- 库存服务
- 营销服务
- 支付服务
- 风控服务
任何一个超时,整体就失败。
排查思路
- 画出真实链路图
- 找出“必须同步”的环节
- 把通知类、衍生类动作改成异步
- 加超时、熔断、降级和幂等控制
坑五:事件驱动用了,但没有幂等
现象
消息重复投递后:
- 库存被重复扣减
- 订单被重复更新
- 积分发了两次
解决方法
消费端必须做幂等校验,常见方法:
- 事件 ID 去重
- 业务主键去重
- 状态机校验
- 唯一索引约束
安全/性能最佳实践
安全最佳实践
1. 服务间认证不要省
内部服务不是“天然可信”。
至少要做到:
- 服务间鉴权
- 请求签名或 mTLS
- 最小权限访问数据库和消息中间件
2. 敏感数据边界明确
像支付账号、身份证、手机号等,不要因为“方便联调”就在多个服务复制全量明文数据。
建议:
- 存储脱敏字段
- 关键字段加密
- 日志避免打印敏感信息
3. 接口契约版本化
微服务多了后,接口升级是高频动作。
如果不做版本治理,很容易一改字段就引发线上兼容问题。
性能最佳实践
1. 优先减少跨服务同步调用
性能优化第一步通常不是加机器,而是缩短调用链。
一个本地函数调用变成 5 次网络调用,性能损耗非常明显。
2. 读写分离思维
交易链路关注正确性,查询链路关注效率。
可以考虑:
- 写模型归属业务服务
- 读模型按查询场景构建
- 报表、搜索、列表页使用投影数据
3. 为热点能力独立扩容
例如:
- 商品详情
- 搜索建议
- 订单列表查询
这些往往比交易写入更容易成为热点,适合单独扩展。
4. 合理设置超时和重试
超时不是越长越稳,重试也不是越多越好。
建议:
- 明确调用超时时间
- 区分可重试和不可重试错误
- 配合熔断、限流
- 避免重试风暴
容量估算与演进建议
做架构时,别只谈“优雅”,还要谈“值不值得”。
什么时候不建议拆
如果你的系统符合下面这些条件,单体往往更合适:
- 团队规模很小,只有 3~5 人
- 业务还在快速试错
- 发布频率不高
- 性能瓶颈主要在数据库或少数热点接口
- 团队还没有成熟的监控、CI/CD、链路追踪体系
这时强上微服务,大概率是在增加复杂度。
什么时候可以开始拆
- 某些模块发布节奏明显不同
- 团队按业务域分工已经形成
- 单体代码理解成本显著上升
- 某些模块有明确独立扩容需求
- 已有基础设施支撑:日志、监控、告警、注册发现、配置中心、消息中间件
一条比较稳的演进路线
- 单体内先模块化
- 明确领域边界和数据归属
- 先拆边界清晰的外围能力
- 再拆高价值核心链路
- 用事件驱动降低耦合
- 持续根据变更频率调整边界
一个实用判断清单
如果你正在评估一个服务是否值得拆,可以用这张清单快速过一遍:
- 这个模块是否有独立业务目标?
- 是否能拥有自己的主数据?
- 是否能由一个团队独立负责?
- 它的变更节奏是否与其他模块明显不同?
- 它是否有独立扩缩容诉求?
- 拆出去后,同步调用是否会显著增加?
- 如果失败,能否局部隔离影响?
- 是否有清晰的接口契约和事件模型?
如果大多数答案是否定的,先别拆。
总结
从单体到微服务,真正决定成败的不是框架,也不是中间件,而是服务边界是否贴合业务变化规律。
可以把整件事记成三句话:
- 先建模,再拆分:围绕领域和业务能力识别边界,而不是按技术层或数据库表拆。
- 先明确数据归属,再谈协作:每个核心数据只能有一个主写者。
- 能同步就少同步,能异步就别强耦合:把变化留在服务内部,把事实通过事件传播出去。
如果你现在手上就是一个单体系统,我的建议很务实:
- 先别急着全量微服务化
- 先在单体内部做模块边界梳理
- 挑一个收益高、风险低的模块试拆
- 用一次真实业务迭代验证边界是否稳定
边界不是设计文档里“想出来”的,更多是业务和团队在实践中“磨出来”的。
只要方向对,哪怕拆得慢一点,也比一次性拆成一地鸡毛要强得多。