平滑拆分的关键是“双写+路由+灰度”,即先新老服务同时写入、再通过api网关或sdk控制读流量灰度、最后验证一致后下线旧路径,而非停机切流;双写需幂等,读路由应基于header或context而非url,proto版本须显式隔离,数据库schema从第一天起必须隔离,client依赖须抽象为interface并统一注入。

平滑拆分不是把单体代码复制粘贴到新服务里,而是让旧逻辑持续可用、新服务逐步接管流量、数据迁移可控——关键在于“双写+路由+灰度”,而不是“停机切流”。
先做双写,再切读,最后下线旧路径
直接停掉老 service、把所有调用切到新 service,等于在生产环境搞压力测试。真正能落地的节奏是:先让新 service 和老 service 同时写入(比如用户注册时,老 service 写 MySQL,新 service 同步写 PostgreSQL + 发送 Kafka 事件),再通过 API 网关或 client SDK 控制读请求路由(如 GetUser 请求按 user_id 哈希,90% 走老 service,10% 走新 service),最后等数据一致、监控稳定、日志无异常,才逐步关闭老 service 的写入口。
- 双写必须幂等:新 service 收到重复事件不能报错或重复发券,用
idempotency_key或 DB 唯一索引兜底 - 读路由别依赖 URL 路径(如
/v2/user),而要用 header(X-Service-Version: v2)或 context value,方便灰度开关动态控制 - 老 service 的读接口不能直接删,得保留至少一个发布周期,否则下游没升级的 client 会 404 或 panic
proto 接口版本要显式隔离,别靠注释或文档约定
gRPC 接口一旦上线,改字段名、删字段、变类型就是破坏性变更。userpb.GetUserRequest 和 userpbv2.GetUserRequest 必须是两个独立 package,生成的 Go 类型不兼容,编译期就能暴露问题。如果还混用同一个 proto 文件加 // v2: use this instead 注释,等于把契约责任推给开发者记忆——没人会每次写调用前去翻注释。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
- 版本号进 package 名,如
package userpbv2;,不是进 service 名(UserServiceV2) - 新旧 proto 文件放在不同目录:
api/user/v1/user.protovsapi/user/v2/user.proto,避免 protoc 生成冲突 - 禁止在 v1 接口里加 optional 字段“为 v2 预留”,这会让 v1 客户端误用未定义行为
数据库不能“先共库再拆库”,schema 隔离必须从第一天开始
初期共用 PostgreSQL 实例可以接受,但 user_service 和 order_service 必须用不同 schema(如 user_schema 和 order_schema),且禁止跨 schema JOIN 或直接查对方表。很多团队说“先共库省事,后面再拆”,结果半年后发现 order 表里硬编码了 user_name 字段,改结构就得全链路协调,彻底锁死演进。
- 每个 service 的
go.mod里只引入自己的数据库驱动,绝不出现github.com/xxx/user-service/internal/repository - 本地开发用 Docker Compose 启动多个独立 PostgreSQL 容器,而不是共享一个 pg 容器配多个 schema——后者掩盖权限和连接池问题
- 双写期间的数据一致性靠应用层对齐,不是靠 DB 触发器或物化视图,后者会把耦合从代码转移到数据库
client 接口必须抽象,且注入点统一收口
如果 order.Service 里直接 new 一个 http.Client 去调 user-service,那这个 service 就没法测、没法换实现、没法做熔断。所有下游依赖必须定义为 interface,比如 UserClient,并在 main 函数或 DI 容器里统一初始化——这样测试时传 mock,预发时传带重试的 http 实现,生产时传 gRPC stub。
- interface 定义放在调用方(order/service.go),不是被调方(user/client.go),避免反向依赖
- client 初始化不要分散在各 handler 里,统一在
cmd/order-service/main.go的init()或构造函数中完成 - 别用全局变量存 client 实例,Go 的
sync.Once或 DI 容器(如dig)更可控,避免并发初始化竞争
最常被忽略的不是技术方案,而是“谁来确认数据已对齐”。双写跑了一周,监控显示延迟
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










