阶段2(上):后端专题 Go/Gin/PostgreSQL/MySQL/Redis/MQ/gRPC 的课程、企业场景、面试题和可运行示例
This commit is contained in:
1 parent
0bae9916a9
commit
5eb67c5d08
65 files changed
+6457
-77
No files matched your search
@@ -0,0 +1,113 @@
|
||||
---
|
||||
id: backend.grpc-microservices.scenario.monolith-split
|
||||
title: 单体拆分:从单体到“订单 + 用户”两个服务
|
||||
type: scenario
|
||||
domain: backend
|
||||
stack: [grpc-microservices, go, mq]
|
||||
tags: [单体拆分, 绞杀者模式, 数据拆分, gRPC, 灰度]
|
||||
level: advanced
|
||||
prerequisites: [backend.grpc-microservices.governance]
|
||||
est_hours: 2
|
||||
related: [backend.grpc-microservices.interview, backend.mq.reliability]
|
||||
week: [14]
|
||||
---
|
||||
|
||||
# 单体拆分:从单体到“订单 + 用户”两个服务
|
||||
|
||||
## 业务背景与问题
|
||||
|
||||
一个 Go 单体后台(用户、商品、订单、报表都在一个进程、一个数据库),团队从 3 人增加到 10 人后:多人改同一个仓库冲突多;报表的重查询拖慢下单;一个模块的 bug 导致整个服务重启。团队讨论“要不要全面改微服务”。
|
||||
|
||||
## 约束
|
||||
|
||||
- 业务不能停,拆分过程中要能随时回退。
|
||||
- 运维人力有限,没有专职的平台团队。
|
||||
- 用户数据被订单、报表等多处直接 JOIN 查询。
|
||||
|
||||
## 方案设计
|
||||
|
||||
1. **先判断是否真的需要拆**:很多问题不需要微服务就能解决——报表查询拖慢下单 → 报表走只读从库;代码冲突 → 按模块划分目录和负责人(模块化单体)。只有“独立部署、独立扩容、故障隔离”的需求明确的部分才拆出来。
|
||||
2. **选第一个拆分对象**:边界清晰、被依赖多、变化少的模块——用户服务(登录、资料)。
|
||||
3. **绞杀者模式(Strangler Fig)逐步迁移**:
|
||||
- 第一步:在单体内部把用户模块整理成接口(`UserService`),其他模块只通过接口访问,不再直接 JOIN 用户表;
|
||||
- 第二步:新建用户服务(gRPC),实现同样的接口;单体里的接口实现改为调用 gRPC 客户端,用开关控制走本地还是远程,灰度切换;
|
||||
- 第三步:数据拆分——用户表迁到用户服务的独立数据库;迁移期间双写或用 CDC 同步,校验一致后切读、再切写;
|
||||
- 第四步:订单需要的用户字段(如昵称)在订单里冗余一份或通过事件同步,避免每次查订单都远程调用。
|
||||
4. **配套治理**:调用超时和熔断、统一 trace ID、错误码映射、接口契约(proto)放在独立仓库管理。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph 单体
|
||||
O[订单模块] --> I{UserService 接口}
|
||||
I -->|开关=本地| L[本地实现]
|
||||
I -->|开关=远程| G[gRPC 客户端]
|
||||
end
|
||||
G -->|超时 + 熔断| U[用户服务] --> UDB[(用户库)]
|
||||
L --> MDB[(原单体库)]
|
||||
```
|
||||
|
||||
## 关键代码
|
||||
|
||||
接口先行,单体里的调用方不关心实现在本地还是远程:
|
||||
|
||||
```go
|
||||
type UserService interface {
|
||||
GetUser(ctx context.Context, id int64) (User, error)
|
||||
}
|
||||
|
||||
// 远程实现:gRPC 客户端 + 超时 + 错误码映射
|
||||
type remoteUsers struct{ cli userv1.UserServiceClient }
|
||||
|
||||
func (r remoteUsers) GetUser(ctx context.Context, id int64) (User, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, 300*time.Millisecond)
|
||||
defer cancel()
|
||||
u, err := r.cli.GetUser(ctx, &userv1.GetUserRequest{Id: id})
|
||||
switch status.Code(err) {
|
||||
case codes.OK:
|
||||
return User{ID: u.GetId(), Name: u.GetName()}, nil
|
||||
case codes.NotFound:
|
||||
return User{}, ErrUserNotFound
|
||||
default:
|
||||
return User{}, fmt.Errorf("用户服务调用失败:%w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 灰度开关:按比例把流量切到远程实现,出问题可以立即切回
|
||||
type switchUsers struct {
|
||||
local, remote UserService
|
||||
percent func() int // 从配置中心读取 0~100
|
||||
}
|
||||
|
||||
func (s switchUsers) GetUser(ctx context.Context, id int64) (User, error) {
|
||||
if int(id%100) < s.percent() {
|
||||
return s.remote.GetUser(ctx, id)
|
||||
}
|
||||
return s.local.GetUser(ctx, id)
|
||||
}
|
||||
```
|
||||
|
||||
gRPC 调用、错误码和超时的可运行演示见 [examples/grpc-user](../examples/grpc-user/);超时 / 重试 / 熔断见 [examples/resilience](../examples/resilience/)。
|
||||
|
||||
## 取舍
|
||||
|
||||
| 方案 | 适合 | 代价 |
|
||||
| --- | --- | --- |
|
||||
| 保持单体 + 模块化 | 团队小、业务变化快 | 部署和扩容不能独立 |
|
||||
| **按需拆出少数服务(本方案)** | 有明确的独立部署 / 扩容 / 隔离需求 | 引入网络调用、分布式数据一致性、更多运维 |
|
||||
| 一次性全面微服务 | 很少适合 | 风险高、周期长、运维成本陡增 |
|
||||
|
||||
## 易错点
|
||||
|
||||
- 拆了服务但还共用一个数据库:耦合没有解决,反而多了网络调用。
|
||||
- 一次性切换、没有开关:出问题无法快速回退。
|
||||
- 订单列表每行都远程查用户:N+1 远程调用。用批量接口或冗余字段。
|
||||
- 拆分后跨服务事务直接用分布式锁硬凑:应该用可靠消息 / Saga 做最终一致。
|
||||
- 拆得太细(每张表一个服务):调用链变长,问题更多。
|
||||
|
||||
## 面试怎么讲
|
||||
|
||||
> “我们没有一上来就全面微服务。先在单体里把用户模块抽成接口,让其他模块只通过接口访问;再把用户服务独立出来用 gRPC 提供,单体里加了按比例切流的开关,出问题可以立即切回;数据用双写加校验迁到独立库,订单里需要的用户昵称通过事件同步冗余。调用都加了超时、熔断和 trace ID。”
|
||||
|
||||
转岗求职时如果没有真实的拆分经历,可以讲“我在项目里做过模块化设计,并且了解拆分的步骤和风险”,不要编造经历。
|
||||
|
||||
追问准备:怎么判断一个模块该不该拆?数据迁移期间怎么保证一致?服务间调用失败怎么处理?
|
||||
Reference in new issue
Block a user