文章目录
- 一、核心概念:什么是后端架构
- 二、三种常见架构模式
-
- [1. 单体架构(Monolith)](#1. 单体架构(Monolith))
- [2. 分层架构(Layered)](#2. 分层架构(Layered))
- [3. 微服务架构(Microservices)](#3. 微服务架构(Microservices))
- [三、代码示例:Go 分层架构最小实现](#三、代码示例:Go 分层架构最小实现)
- 四、与前端类比对照表
- 五、思考题:电商系统
-
- [1. 什么情况下选模块化单体](#1. 什么情况下选模块化单体)
- [2. 为什么不建议一开始就拆微服务](#2. 为什么不建议一开始就拆微服务)
- [3. 什么时候值得拆微服务](#3. 什么时候值得拆微服务)
- [4. 如果选微服务,怎么拆](#4. 如果选微服务,怎么拆)
-
- [1. 用户服务](#1. 用户服务)
- [2. 商品服务](#2. 商品服务)
- [3. 库存服务](#3. 库存服务)
- [4. 订单服务](#4. 订单服务)
- [5. 支付服务](#5. 支付服务)
- [5. 一次下单如何跨服务完成](#5. 一次下单如何跨服务完成)
- [6. 和前端 monorepo 的类比](#6. 和前端 monorepo 的类比)
- [进阶 架构设计的 3 个核心原则](#[进阶] 架构设计的 3 个核心原则)
从前端组件化思维,到后端分层与服务拆分------今天我们站在更高的视角,看后端系统是怎么"搭积木"的。
一、核心概念:什么是后端架构
前端架构你一定不陌生:组件化、状态管理(Redux/Pinia)、路由、构建工具链......这些都是前端架构的范畴。
后端架构回答的是同一个问题的另一面:一个后端系统由哪些部分组成?它们怎么分工、怎么通信?
从前端视角看,你可以把后端架构理解成:
- 前端组件 = 后端模块/服务
- 组件间 props / 事件通信 = 服务间 API / 消息队列通信
- 前端状态管理 = 后端数据库 + 缓存
- 前端路由 = 后端 API 网关 + 负载均衡
二、三种常见架构模式
1. 单体架构(Monolith)
所有功能打包在一个应用里,就像你写一个 Vue/React 单页应用,所有组件都在同一个项目里。
- ✅ 简单、开发快、部署方便
- ❌ 代码越来越大后,改一处怕影响全局(和前端巨型组件一个道理)
2. 分层架构(Layered)
把单体按"职责"横向切成几层,最经典的是三层架构:
┌─────────────────────────┐
│ Controller 层(接口) │ ← 接收 HTTP 请求,就像前端的"页面组件"
├─────────────────────────┤
│ Service 层(业务逻辑) │ ← 核心业务处理,就像前端的"业务 hooks / store"
├─────────────────────────┤
│ DAO/Repository 层 │ ← 操作数据库,就像前端的"API 请求封装层"
└─────────────────────────┘
这和前端的"页面组件 → 业务逻辑 → API 封装"分层思路几乎一模一样!
3. 微服务架构(Microservices)
把大系统拆成多个小服务,每个服务独立部署、独立数据库,通过 HTTP/RPC/消息队列通信。
类比前端:就像微前端(Micro Frontends)------每个团队维护自己的子应用,最后拼在一起。
三、代码示例:Go 分层架构最小实现
下面是一个典型的三层架构 Go 项目结构示意(5-15 行核心代码):
go
// main.go - 入口,组装各层(类比前端 main.ts 里挂载 App)
func main() {
db := gorm.Open(mysql.Open(dsn), &gorm.Config{})
repo := repository.NewUserRepository(db) // DAO 层
svc := service.NewUserService(repo) // Service 层
handler := handler.NewUserHandler(svc) // Controller 层
r := gin.Default()
r.GET("/users/:id", handler.GetUser)
r.Run(":8080")
}
go
// handler/user.go - Controller 层:只管收请求、返响应
func (h *UserHandler) GetUser(c *gin.Context) {
id := c.Param("id")
user, err := h.service.GetByID(id) // 调 Service 层
if err != nil {
c.JSON(404, gin.H{"error": "not found"})
return
}
c.JSON(200, user)
}
go
// service/user.go - Service 层:业务逻辑在这里
func (s *UserService) GetByID(id string) (*User, error) {
if id == "" {
return nil, errors.New("invalid id") // 业务校验
}
return s.repo.FindByID(id) // 调 DAO 层
}
关键原则:每层只依赖下一层,不反向依赖。 就像前端组件只调用 hooks,hooks 只调 API------方向不能乱。
四、与前端类比对照表
| 后端概念 | 前端对应物 | 作用 |
|---|---|---|
| Controller / Handler | 页面组件 / 路由组件 | 接收输入、返回输出 |
| Service 层 | 业务 hooks / Pinia/Vuex actions | 核心业务逻辑 |
| Repository / DAO | API 请求封装层(axios 封装) | 数据存取 |
| 数据库 | 后端的"数据源" | 持久化存储 |
| 缓存(Redis) | 前端的 localStorage / 内存缓存 | 加速访问 |
| 消息队列 | EventBus / 事件总线 | 异步解耦 |
| API 网关 | 前端的 Nginx 反向代理 | 统一入口、路由分发 |
| 微服务 | 微前端(Micro Frontends) | 按业务拆分、独立部署 |
五、思考题:电商系统
你现在负责一个电商系统的后端设计:用户、商品、订单、支付四个模块。
你会选择单体架构还是微服务架构?为什么?如果选微服务,你会怎么拆?
提示:想想前端什么时候用单仓库(monorepo)、什么时候拆成多个独立项目------背后的权衡逻辑是一样的。
默认选择是:先做模块化单体,不直接上微服务。
原因不是系统只有四个模块,而是架构要匹配当前的团队规模、业务复杂度和运维能力。用户、商品、订单、支付可以有清晰边界,但"有边界"不等于"必须跨网络部署"。
1. 什么情况下选模块化单体
如果目前是这些条件:
- 一个小团队负责整个系统
- 产品还在快速试错
- 用户量和订单量暂时不确定
- 各模块经常一起修改、一起发布
- 尚未建设完善的监控、链路追踪、消息队列和自动化部署
建议先使用一个 Go 服务、一个仓库、一个部署单元:
text
ecommerce/
├── cmd/
│ └── api/
│ └── main.go
├── internal/
│ ├── user/
│ │ ├── handler.go
│ │ ├── service.go
│ │ └── repository.go
│ ├── product/
│ ├── inventory/
│ ├── order/
│ ├── payment/
│ └── platform/
│ ├── database/
│ ├── cache/
│ └── messaging/
└── migrations/
虽然是单体,但要遵守模块边界:
text
订单模块不能直接修改支付表
支付模块不能直接修改订单表
商品模块不能直接读取用户模块的内部对象
模块之间通过明确的接口调用:
go
type PaymentService interface {
CreatePayment(ctx context.Context, orderID string, amount int64) error
}
这样部署简单、事务简单、调试方便,同时保留了将来拆服务的可能。
2. 为什么不建议一开始就拆微服务
微服务把原来的函数调用:
go
paymentService.Pay(order)
变成跨网络调用:
text
Order Service
↓ HTTP/gRPC
Payment Service
随之而来的不只是多个 Go 项目,还包括:
- 服务发现
- API 网关
- 超时和重试
- 熔断、限流
- 分布式链路追踪
- 消息队列
- 配置中心
- 多套 CI/CD
- 数据一致性
- 接口版本兼容
- 局部失败处理
- 分布式事务或补偿机制
微软的架构指南也指出,微服务可以独立部署和扩容,但会引入服务通信、测试、数据一致性和运维等系统级复杂度。Azure 微服务架构指南
所以微服务实际上是:
用系统复杂度,换取团队、部署、扩容和故障隔离方面的独立性。
3. 什么时候值得拆微服务
出现以下信号时,才开始考虑拆分:
| 信号 | 说明 |
|---|---|
| 多个独立团队 | 每个团队需要独立开发和发布 |
| 发布互相阻塞 | 改支付代码却必须重新发布整个商城 |
| 流量差异明显 | 商品查询很大,支付流量小但安全要求高 |
| SLA 不同 | 商品暂时不可用还能降级,支付不能随便失败 |
| 安全要求不同 | 支付密钥、权限、审计需要单独隔离 |
| 技术需求不同 | 搜索可能用 Elasticsearch,订单使用 MySQL |
| 单体边界已经稳定 | 已经知道哪些业务总是一起变化 |
| 运维体系成熟 | 有监控、告警、追踪、自动部署和故障演练 |
微服务应该围绕"业务能力"或"限界上下文"拆分,而不是机械地按照表或者代码目录拆分。AWS 按业务子域拆分指南
4. 如果选微服务,怎么拆
电商系统不一定只拆成题目里的四个服务。考虑增加库存边界:
text
API Gateway
│
┌────────────────┼────────────────┐
↓ ↓ ↓
User Service Product Service Order Service
│ │
↓ ↓
Inventory Service Payment Service
1. 用户服务
负责:
- 注册、登录
- 用户资料
- 收货地址
- 用户状态
拥有自己的数据:
text
users
addresses
user_credentials
2. 商品服务
负责:
- SPU/SKU
- 商品名称、描述、分类
- 商品上下架
- 基础价格
拥有:
text
products
skus
categories
复杂促销场景下,可以以后再拆出定价和营销服务,不必一开始就拆。
3. 库存服务
负责:
- 可售库存
- 库存预占
- 确认扣减
- 释放库存
拥有:
text
stocks
stock_reservations
stock_records
库存最好从商品中独立出来,因为商品资料是"读多写少",库存却是高并发修改,而且涉及超卖问题。
简单系统中也可以先把库存保留在商品模块。
4. 订单服务
负责:
- 创建订单
- 订单项
- 订单状态机
- 取消和关闭订单
- 保存下单快照
拥有:
text
orders
order_items
order_status_logs
订单中应保存商品快照:
text
商品名称
SKU
成交价格
购买数量
收货地址
不能每次查看历史订单时都读取商品服务的当前价格,否则商品改价以后,历史订单也会跟着变化。
5. 支付服务
负责:
- 创建支付单
- 调用微信、支付宝等渠道
- 支付回调
- 退款
- 对账
- 支付幂等
拥有:
text
payments
payment_attempts
refunds
callback_records
支付服务只负责"钱是否支付成功",订单服务负责"订单应该变成什么状态"。
5. 一次下单如何跨服务完成
不能依赖一个跨五个数据库的大事务,一般使用状态机、消息和补偿机制:
text
1. 创建待支付订单
2. 库存服务预占库存
3. 支付服务创建支付单
4. 用户完成支付
5. 支付服务处理回调并发布 PaymentSucceeded
6. 订单服务将订单改成已支付
7. 库存服务确认扣减库存
如果支付超时:
text
订单关闭
→ 发布 OrderCancelled
→ 库存服务释放预占库存
这属于 Saga 思路:每个服务完成自己的本地事务,失败时执行对应的补偿操作。
还需要保证:
- 支付回调幂等
- 消息可能重复消费
- 消息发送失败可以重试
- 状态迁移不能倒退
- 服务调用必须有超时
- 不能让多个服务直接共享数据库
6. 和前端 monorepo 的类比
两者背后的判断逻辑确实相似:
text
模块经常一起变化
团队规模较小
统一发布更简单
↓
倾向 monorepo / 模块化单体
text
团队相互独立
发布节奏不同
技术和权限要求不同
需要独立扩容
↓
倾向独立项目 / 微服务
不过要注意:
monorepo 是代码仓库边界,微服务是运行和部署边界,两者不是同一个概念。
完全可以:
- 单体使用 monorepo
- 多个微服务仍然放在同一个 monorepo
- 多个微服务分别放在不同仓库
最终建议是:
新项目先做边界清晰的模块化单体;当某个模块产生明确的独立团队、独立发布、独立扩容、安全隔离或故障隔离需求时,再沿着已有模块边界逐步拆出微服务。
对于这个电商系统,通常优先考虑拆出的会是支付服务 和库存服务:支付有安全与故障隔离需求,库存有高并发和一致性需求。
进阶 架构设计的 3 个核心原则
- 单一职责:每个模块/服务只做一件事(和前端组件化一个道理)
- 高内聚、低耦合:相关的代码放一起,模块之间依赖越少越好
- 面向接口编程 :依赖抽象(接口),不依赖具体实现------就像前端依赖
axios实例而不是直接写死 fetch
好的后端架构和好的前端架构,底层逻辑完全一致------都是为了可维护、可扩展、好协作。