必须先验证单体真实瓶颈再拆分,按限界上下文而非名词划分服务,通信层封装client禁止硬编码,数据迁移接受最终一致,事务改用saga模式。

不能直接“重构单体为微服务”,必须先判断是否真需要拆——如果单体跑得稳、改得快、扩得灵,别碰它。
先验证单体瓶颈是不是真瓶颈
很多团队一上来就画服务边界、搭注册中心,结果发现压根没到那个量级。真实瓶颈往往藏在日志里:
-
go build耗时超过 90 秒?说明代码耦合太深,模块化比拆服务更急 - 每次发版都要全量回滚?说明部署单元太大,该用
make build-user-svc这类按模块构建脚本了 - 数据库慢查询集中在
user_order_join这种跨域表?说明数据模型已成枷锁,不是加索引能解决的 - 新人改一个“修改收货地址”要 grep 17 个文件?说明领域语义没收敛,该用
internal/address包统一收口了
只有当上述现象持续出现、且影响交付节奏时,才值得进入下一步。
按限界上下文切,而不是按名词建 service
把单体代码库里的 user 目录直接挪出来叫 user-service,90% 的概率会失败。真正该做的,是问清楚:
- 谁拥有“用户手机号”的写权限?是注册流程生成,还是实名认证更新?这个主权归属决定服务边界
- “登录成功后发欢迎短信”这个动作,触发方是 auth 模块,但短信内容拼装、渠道选择、发送重试逻辑,全在
notification领域里——它不该塞进auth-service - 优惠券的“发放”“核销”“冻结”三个状态,在风控、营销、财务系统里含义完全不同,硬塞进一个
coupon-service,最后只能开放UPDATE coupon SET status = ? WHERE id = ?这种裸 SQL
拆出来的每个服务,必须能独立完成一个完整业务闭环,比如 internal/withdrawal 包里包含校验规则、风控拦截、资金扣减、异步记账——而不是只提供 Withdraw(ctx, req) 接口然后甩锅给别处。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
通信层必须封装 client,禁止硬编码地址
这是老项目迁移中最容易翻车的地方。常见错误写法:
http.Get("http://order-svc:8080/v1/order/" + orderID)
这种写法会让服务失去重试、超时、熔断能力,本地联调直接瘫痪。正确做法是:
- 定义接口:
type OrderClient interface { GetOrder(ctx context.Context, id string) (*Order, error) } - 实现里用
http.Client或 gRPC stub,但地址从配置或服务发现(如etcd)读取 - client 初始化交给 DI 容器(
dig或构造函数),handler 层只依赖接口 - 测试时注入
mockOrderClient,完全隔离网络依赖
注意:初期可先用 DNS SRV 记录模拟服务发现,不用一上来就上 etcd 或 consul——避免把基础设施问题和业务拆分混在一起排查。
数据迁移别追求强一致,接受短暂最终一致
单体数据库里一张 users 表,拆完变成 auth_users、profile_users、billing_users 三张表,字段还不同。想一步到位同步所有历史数据?不可能。
- 新写操作走新服务,旧读操作仍走单体——用
data migration tool做单向双写 - 关键字段(如
user_id、status)优先同步,非核心字段(如last_login_ip)允许延迟几小时 - 所有跨服务查询,用 API 组合(如网关层聚合
/v1/user/{id}),而不是 JOIN 多库 - 监控
user_sync_delay_seconds指标,超过阈值自动告警并降级为单体兜底
最常被忽略的一点:拆服务后,事务边界变了。原来单体里一个 HTTP 请求内完成“创建订单+扣库存+发消息”,现在至少三次网络调用——必须用 Saga 模式或本地消息表,而不是指望分布式事务。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










