微服务拆分应以解决协作卡点、发布风险和技术栈收敛为目标,按限界上下文而非模块名拆分,明确数据主权,接口需封装为client,数据库共享实例但schema隔离,数据迁移须渐进式推进。

拆微服务不是为了“看起来像微服务”,而是为了解决单体里团队协作卡点、发布风险高、技术栈无法收敛这些真实问题。如果当前单体跑得稳、改得快、扩得灵,别急着拆。
按限界上下文拆,而不是按模块名或表名拆
很多团队一上来就建 user-service、order-service,结果半年后发现登录要查用户基本信息+实名状态+风控等级+消息推送开关——四个字段来自四个不同业务规则,全堆在一个服务里,接口越加越多,数据库 schema 被迫开放。
真正该做的,是画出限界上下文(Bounded Context):
- 谁对“用户”这个概念有数据主权?是注册流程决定 ID 生成规则,还是会员系统决定等级升降逻辑?
- 一个需求变更是否只改一个服务?比如“增加手机号一键登录”要不要同时动
auth-service、user-service、sms-gateway?如果要,边界就错了 - 优惠券在发券、核销、风控中语义完全不同,强行塞进一个
coupon-service,必然妥协接口、共享数据库、绕过领域规则
下游调用必须封装成 client 接口,不能硬编码地址
常见错误现象:http.HandleFunc("/pay", func(w http.ResponseWriter, r *http.Request) { resp, _ := http.Get("http://order-svc:8080/v1/order/123") })——这种写法会让服务失去弹性,无法做重试、超时、熔断,且本地开发时根本没法联调。
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
实操建议:
- 定义接口:例如
type OrderClient interface { GetOrder(ctx context.Context, id string) (*Order, error) } - 实现里才用
http.Client或 gRPC stub;client 初始化交给 DI 容器(如dig或手写构造函数),不要在 handler 里new - 生产环境用服务发现(如 Consul +
go-micro/registry/consul)或 DNS SRV 记录,避免把order-svc地址写死
数据库共实例但 schema 隔离,核心是明确数据主权归属
单体拆分初期常犯的错:直接把原单体数据库按表拆成多个库,导致跨服务 JOIN、事务穿透、DDL 冲突频发。
更可行的做法:
- 所有服务共享同一套 PostgreSQL/MySQL 实例,但各自使用独立
schema(如user_schema、order_schema) - 每个 schema 的 owner 只能是对应服务,其他服务禁止直连该 schema 的表;读取需通过对方暴露的 API 或事件消费
- 聚合根内强一致性操作(如创建订单+扣库存)仍放在同一事务内,但仅限于同一限界上下文内部
最容易被忽略的点:数据迁移不是一次性动作,而是伴随每次服务拆出持续发生的渐进过程。旧单体里一个 SQL JOIN 查询,可能需要拆成 3 次 API 调用 + 缓存兜底 + 最终一致性补偿,这比写代码更考验对业务语义的理解深度。

















