Backend
Outlinks (0)
No outlinks found
Backlinks (0)
No backlinks found
1 · Backend#
后端:一台监听 HTTP / WebSocket / gRPC 请求的服务器,负责提供静态内容、动态数据、业务逻辑和安全的数据访问。
浏览器为什么不能直接承担后端职责:原因包括浏览器沙箱限制、跨域限制、无法管理数据库连接池,以及客户端算力有限。
一个请求如何进入系统,如何通过认证和代码分层,如何读写数据、利用缓存、把慢任务异步化,最后形成一个可扩展的后端系统。
网络链路、HTTP 语义、认证授权、代码分层、数据库执行模型、缓存一致性、异步任务幂等
https://medium.com/@karthik.joshi103/backend-from-first-principles-036209a3049c https://medium.com/@karthik.joshi103/backend-from-first-principles-91eaf3720e38
1.1 · 总览:一个请求如何流经后端
一个典型请求大致会经过这条链路:
Client → DNS / Network → HTTP Router → Middleware → Controller → Service → Repository → DB / Cache / Queue
如果把后端拆开看,本质上是在解决 6 类问题:
- 请求怎么进入系统
- 请求怎么被安全地接受
- 请求怎么落到代码结构里
- 数据怎么存储和读取
- 热点数据怎么加速
- 慢任务怎么从主链路中剥离
1.2 · 1. 请求如何进入系统#
API 基础概念:
- 静态路由
GET /usersvs 动态路由GET /users/:id - query 参数用于过滤/分页:
GET /users?role=admin&page=2 - 嵌套路由表达从属关系:
GET /users/:id/orders - 版本化:
/v1/users、/v2/users - 序列化(对象→JSON)/ 反序列化(JSON→对象)
请求链路可以先粗略理解成:
- 域名解析到 IP
- 请求穿过网络到达服务器
- Web server / gateway 接收连接
- HTTP 路由层根据 method + path 找到对应处理逻辑
- 进入业务代码,最后返回响应
CORS:浏览器对跨域请求的安全机制。简单请求(GET/POST + 简单头)直接发送并检查响应头;非简单请求先发 OPTIONS 预检请求,服务端返回 Access-Control-Allow-* 头后才发送真正请求。
1.2.1 · REST#
- S: Web 服务需要一套通用的 API 风格
- C: SOAP 重 XML、强契约、僵硬,开发和调试成本高
- Q: 有没有更轻量、更符合 Web 语义的 API 风格?
- A: REST 的六个约束
| 约束 | 含义 |
|---|---|
| 客户端-服务器 | 前后端职责分离 |
| 无状态 | 每次请求携带完整信息,服务端不存会话 |
| 可缓存 | 响应需标明是否可缓存 |
| 统一接口 | 资源通过 URI 标识,用 HTTP 方法操作 |
| 分层系统 | 客户端不需要知道是否经过代理/网关 |
| 按需代码 | 可选,服务端可下发可执行代码 |
API 设计规范:资源名用复数、层级路径表达关系、单资源通过 ID 标识
GET /users # 列表
POST /users # 创建
GET /users/:id # 单个资源
PUT /users/:id # 全量更新
PATCH /users/:id # 部分更新
DELETE /users/:id # 删除
GET /users/:id/orders # 从属资源
GET /users?role=admin # 过滤条件放 query 参数
1.3 · 2. 请求如何被系统安全地接受#
- S: 系统需要知道”你是谁”(认证)和”你能做什么”(授权)
- C: 从单体到分布式,从自有用户到第三方登录,需求不断演进
- Q: 认证授权方案如何一步步演进,各自解决什么问题?
- A: Session → JWT → OAuth 2.0 → OIDC,内部权限用 RBAC
Session(有状态会话)
- 服务端存储会话数据,通过 Cookie 中的 Session ID 识别用户
- 问题:横向扩展时需要 sticky session 或集中式 session 存储(如 Redis),增加架构复杂度
JWT(无状态认证)
- 服务端签发 token,客户端每次请求携带,服务端只需验签不需存储
- 坑:token 一旦签发无法主动撤销(除非维护黑名单,又回到有状态);需要设计 access token + refresh token 双 token 刷新机制;payload 不要放敏感信息(Base64 编码不是加密)
OAuth 2.0(委托授权)
- 解决”授权别人代表用户访问资源”的问题
| 授权模式 | 适用场景 |
|---|---|
| Authorization Code | Web 应用,最安全(配合 PKCE 也用于 SPA/移动端) |
| Client Credentials | 服务间调用,无用户参与 |
OpenID Connect(身份认证)
- 在 OAuth 2.0 之上增加 ID Token,解决”用户是谁”的身份认证问题
RBAC(权限控制)
- 业务系统内部的权限控制适合单独用 RBAC 管理(用户→角色→权限),不应完全耦合在登录体系里
可以把这一节记成两句话:
- 认证回答”你是谁”
- 授权回答”你能做什么”
1.4 · 3. 请求如何落到代码结构里#
- S: 后端代码量增长后需要合理组织
- C: 不分层会导致业务逻辑、数据访问、请求处理混在一起,难以测试和维护
- Q: 如何划分职责边界?
- A: 按 Controller → Service → Repository 分层,依赖方向单向向下
| 层 | 职责 | 处理的数据类型 |
|---|---|---|
| Middleware | 校验、认证、日志、数据转换 | 原始请求/响应 |
| Controller | 接收请求、调用 Service、返回响应 | DTO(Data Transfer Object) |
| Service | 业务逻辑、事务编排 | Domain Model |
| Repository | 数据访问、SQL/ORM 操作 | Entity / DB Record |
关键原则:
- 依赖方向:Controller → Service → Repository,上层依赖下层,下层不感知上层
- DTO vs Domain Model:DTO 面向接口传输(字段可裁剪),Domain Model 面向业务逻辑(包含行为和校验)
- Controller 不写业务逻辑,Service 不操作 HTTP 对象,Repository 不包含业务规则
一条请求落到代码里时,常见流转是:
Middleware 做通用处理 → Controller 解析请求 → Service 编排业务 → Repository 读写数据
1.5 · 4. 数据如何存储与读取#
- S: 应用需要持久化和查询结构化数据
- C: 早期程序直接操作文件,数据格式与程序逻辑紧耦合,换程序就要重写解析
- Q: 如何让数据独立于程序存在并高效访问?
- A: 关系数据库:逻辑数据模型和物理存储分离
1.5.1 · RDBMS#
存储层:数据以固定大小的 page(通常 4KB/8KB)为单位落盘,page 组成 heap file;B-Tree 索引在 page 之上建立有序结构,避免全表扫描。
SQL 执行流程:
SQL 文本 → Parsing(语法树)→ Semantic Analysis(表/列是否存在)
→ Optimizer(选择执行计划)→ Execution(实际执行)
事务与并发:
- ACID:原子性、一致性、隔离性、持久性
- WAL(Write-Ahead Log):先写日志再改数据页,崩溃后可通过日志恢复;后台线程异步刷脏页到磁盘
- MVCC:每行数据维护多个版本,读操作看到事务开始时的快照,写操作创建新版本,读写互不阻塞
1.5.2 · NoSQL#
不同场景下权衡”人怎么理解数据”和”机器怎么存储数据”:
| 类型 | 代表 | 适用场景 | 数据模型 |
|---|---|---|---|
| 文档型 | MongoDB | 结构灵活、嵌套数据 | JSON/BSON 文档 |
| 列族 | Cassandra、HBase | 高写入吞吐、时间序列 | 行键 + 列族 |
| 图数据库 | Neo4j | 关系密集、多跳查询 | 节点 + 边 |
| 时序数据库 | InfluxDB、TimescaleDB | 监控、IoT、指标 | 时间戳 + 值 |
这里的核心不是”会不会写 SQL”,而是理解:
- 数据模型如何设计
- 查询为什么快或慢
- 并发读写时如何保证正确性
1.6 · 5. 热点数据如何加速#
- S: 部分数据被频繁读取,每次都查数据库性能浪费
- C: 计算昂贵、数据量大、读多写少的场景下,直接查源数据太慢
- Q: 如何用更快的存储加速热点数据访问?
- A: 把部分主数据放到更快的存储里,用空间和一致性换性能
缓存无处不在:DNS 缓存、CPU cache、浏览器缓存、CDN、API 缓存、Redis/Memcached 内存缓存。
核心难题不是”会用 Redis”,而是:
- 淘汰策略:LRU(最近最少使用)、LFU(最不频繁使用)、TTL(过期时间)
- 一致性问题:缓存与数据库数据不一致时如何处理
1.6.1 · 缓存模式
| 模式 | 写流程 | 读流程 | 一致性 | 适用场景 |
|---|---|---|---|---|
| Write-Through | 写缓存,缓存同步写 DB | 读缓存 | 强 | 读写均匀、一致性要求高 |
| Write-Back | 写缓存,缓存异步批量写 DB | 读缓存 | 弱(可能丢数据) | 写密集、可容忍短暂不一致 |
| Cache-Aside | 写 DB,删缓存 | 缓存未命中时查 DB 并回填 | 最终一致 | 大多数业务场景最常用 |
缓存不是替代数据库,而是放在数据库前面,解决”热点数据访问太慢”的问题。代价是:
- 需要接受一定程度的一致性复杂度
- 需要设计失效、淘汰、回填策略
1.7 · 6. 慢任务如何剥离出主链路#
- S: 某些逻辑(发邮件、生成报表、视频转码)耗时较长
- C: 放在主请求链路里同步执行会阻塞响应,影响用户体验和系统吞吐
- Q: 如何把慢任务从请求链路中剥离?
- A: 把任务扔进队列,由消费者异步处理,配合重试、ack、死信队列等机制
1.7.1 · 任务分类
| 类型 | 示例 |
|---|---|
| 一次性任务 | 发送欢迎邮件、生成发票 |
| 定时任务 | 每日报表、定期清理过期数据 |
| 链式/递归任务 | 视频上传 → 转码 → 生成缩略图 → 通知用户 |
| 批处理任务 | 批量导入用户、批量发送通知 |
1.7.2 · 设计原则
- 任务要小:单个任务职责单一,便于重试和调试
- 可观测:任务状态、耗时、失败原因需要可查
- 可重试:失败后能自动重试,配合退避策略
- 有限流:防止消费者被压垮
- 幂等:同一任务执行多次结果一致,重试不会产生副作用
1.7.3 · 常见组件与选型
异步系统里常见的不只是”有个队列”,而是一组配套组件:
- Producer:业务服务,负责投递任务/事件
- Broker / Queue:保存消息,负责投递和削峰
- Consumer / Worker:异步执行任务
- Scheduler:投递延迟任务、定时任务
- Retry / DLQ:失败重试、死信隔离
- Result Store / Status Store:保存任务状态、执行结果
- Monitoring:观察堆积长度、消费延迟、失败率
常见技术选型:
| 组件/技术 | 更像什么 | 常见场景 | 特点 |
|---|---|---|---|
| Redis + BullMQ / Celery | 任务队列 | 发邮件、生成报表、图片处理 | 上手快,适合业务任务调度 |
| RabbitMQ | 可靠消息队列 | 任务分发、工作队列、路由分发 | ack、重试、路由能力强 |
| SQS / 云消息服务 | 托管任务队列 | 云上业务异步化 | 运维成本低,和云生态集成强 |
| Kafka | 事件流平台 | 用户行为流、订单事件、日志采集、异步解耦 | 吞吐高、可回放、适合事件驱动和数据管道 |
Kafka 值得单独提一下:它当然也能承接异步处理,但它更偏向事件流(event streaming),不是传统意义上”取一个任务、做完就删掉”的任务队列。
- 如果需求是”某个慢操作后台执行掉”:更常见的是 Redis 队列、RabbitMQ、SQS
- 如果需求是”一个事件发生后,多个系统都要订阅处理,而且希望保留历史、支持重放”:Kafka 更合适
- 典型例子:
订单已创建事件发到 Kafka,库存、优惠券、推荐、数据平台都各自消费
1.7.4 · 死信队列(DLQ)#
死信队列(Dead Letter Queue)是专门用来存放无法被正常消费的消息/任务的队列。
常见进入死信的原因:
- 消费失败超过最大重试次数
- 消息过期仍未处理
- 队列已满或消息被 broker 拒收
- 消费者显式
reject/nack,且不再重新入队
它的目标不是立刻重试成功,而是先把异常消息从主流程中隔离出来,避免:
- 坏消息反复重试,持续占用资源
- 主队列被少数异常任务卡住
- 故障消息和正常消息混在一起,难以排查
一个典型流程:
- 任务先进入主队列
- 消费者处理失败,系统按策略自动重试
- 超过重试上限后,消息被转移到 DLQ
- 开发或运维再分析是数据问题、代码 bug,还是下游依赖故障
1.7.5 · 按环境的常见组件组合
| 环境 | 常见组件 | 目标 |
|---|---|---|
| 开发环境(dev) | 本地应用 + 本地 DB + 本地 Redis;必要时用 docker-compose 跑 RabbitMQ / Kafka | 快速启动、便于调试 |
| 测试/预发环境(test/staging) | 与生产同类组件,但规模更小;保留队列、缓存、定时任务、监控 | 尽量模拟真实链路,提前暴露集成问题 |
| 生产环境(prod) | 高可用 DB、Redis、消息队列/Kafka、Scheduler、DLQ、告警监控、日志系统 | 稳定性、可恢复性、可观测性 |
一个比较常见的落地方式:
- 小型项目/单体服务:Postgres + Redis + BullMQ/Celery
- 中型业务系统:Postgres/MySQL + Redis + RabbitMQ
- 事件驱动/数据量大:OLTP 数据库 + Redis + Kafka + 独立消费者集群
所以这节可以记成一句话:异步任务常用队列,跨系统事件流常提 Kafka;环境越往生产走,越强调高可用、死信、监控和调度。
1.8 · 7. 把整条链路串起来#
把前面的内容连起来,一个常见的后端请求是这样工作的:
- 浏览器发起
HTTP request - 请求经过网络到达服务,路由命中某个接口
- Middleware 做日志、鉴权、参数校验
- Controller 把请求转换成 DTO,交给 Service
- Service 执行业务逻辑,必要时读写数据库
- 如果数据是热点,先查缓存,未命中再查 DB 并回填
- 如果某些步骤很慢,就把任务投递到队列,由 worker 异步处理
- 最终系统返回响应,并通过监控、日志、告警观察整个过程
这也是后端系统设计的基本思路:
- 主链路尽量短
- 慢操作尽量异步
- 热点数据尽量缓存
- 权限边界尽量清晰
- 数据读写尽量可预测、可恢复、可观测