docs: 重构 README 附录展示 & 新增多个附录交互组件

README 更新:
- 移除顶部 header.png 横幅图片
- 新增「附录知识库」板块,以 3×3 网格展示 9 大知识领域精选内容
- 附录链接指向部署版网站 (datawhalechina.github.io)
- 阶段表格新增「附录」行,突出 80+ 交互式专题
- 章节标题「新手入门 & PM」简化为「零基础入门」
- News 新增 2026-02-25 附录知识库更新条目

新增交互组件:
- 异步任务队列 (async-task-queues) 演示组件
- 文件存储 (file-storage) 演示组件
- 项目架构 (project-architecture) 演示组件
- 限流与背压 (rate-limiting) 演示组件
- 搜索引擎 (search-engines) 演示组件
- 计算机基础: AppLaunch/BiosUefi/OSBoot 等启动流程演示组件

新增附录文档:
- 前端项目架构 (frontend-project-architecture.md)
- 后端项目架构 (backend-project-architecture.md)

内容优化:
- 算法思维、数据结构、编程语言、调试艺术等多篇附录内容更新
- HTML/CSS 布局、请求旅程等前后端文档完善
- 附录索引页 (index.md) 同步更新
This commit is contained in:
sanbuphy
2026-02-25 12:22:49 +08:00
parent f44c842fe7
commit df51f84ab5
36 changed files with 8535 additions and 393 deletions
@@ -1,3 +1,151 @@
# 异步任务队列与生产消费模型
> 待实现
::: tip 前言
**用户点了"导出报表"按钮,然后盯着转圈的加载动画等了 30 秒——这合理吗?** 当一个操作需要几秒甚至几分钟才能完成时,让用户干等着显然不是好体验。异步任务队列就是解决这个问题的核心架构模式——把耗时操作丢到后台去处理,让用户立刻得到响应。
:::
**这篇文章会带你学什么?**
学完这章后,你将获得:
- **同步异步对比**:理解为什么某些操作必须异步化,以及异步化带来的用户体验提升
- **生产消费模型**:掌握 Producer-Consumer 模式的核心思想和工作流程
- **Worker 池机制**:了解任务如何被分发到多个 Worker 并行处理
- **可靠性保障**:掌握任务重试、幂等性、死信队列等保障机制
- **技术选型能力**:了解主流异步任务框架的特点和适用场景
| 章节 | 内容 | 核心概念 |
|-----|------|---------|
| **第 1 章** | 为什么需要异步 | 同步阻塞 vs 异步非阻塞 |
| **第 2 章** | 生产消费模型 | Producer、Queue、Consumer |
| **第 3 章** | Worker 工作池 | 并发处理、任务分发 |
| **第 4 章** | 可靠性保障 | 重试策略、幂等性、死信队列 |
| **第 5 章** | 框架选型 | Celery、Sidekiq、Bull、RQ |
---
## 0. 全景图:为什么不能让用户"干等着"?
想象你去餐厅点餐。好的餐厅会在你点完餐后立刻给你一个取餐号,然后你可以去找座位、玩手机,等餐好了再来取。而不是让你站在柜台前,盯着厨师做完整道菜。
Web 应用中有很多类似的"做菜"操作:
- **发送邮件/短信**:调用第三方 API,可能需要几秒
- **生成报表/PDF**:大量数据计算,可能需要几十秒
- **图片/视频处理**:压缩、转码、加水印,可能需要几分钟
- **数据同步**:跨系统数据同步,耗时不确定
::: tip 异步任务的核心思想
把耗时操作从"请求-响应"的主流程中剥离出来,放到后台队列中异步处理。用户提交请求后立刻得到"已收到,正在处理"的响应,处理完成后通过通知、轮询或 WebSocket 告知结果。
:::
---
## 1. 同步 vs 异步:一个订单的故事
当用户提交一个订单时,后端需要做很多事情:扣减库存、创建订单记录、发送确认邮件、更新推荐系统、记录审计日志……
在同步模式下,这些操作串行执行,用户必须等所有操作完成才能看到结果。在异步模式下,只需要完成核心操作(扣减库存、创建订单),其余操作丢到队列里后台处理。
<AsyncTaskFlowDemo />
| 对比维度 | 同步处理 | 异步处理 |
|---------|---------|---------|
| 用户等待时间 | 所有操作总耗时 | 仅核心操作耗时 |
| 系统吞吐量 | 低(线程被阻塞) | 高(快速释放线程) |
| 失败影响 | 非核心失败导致整体失败 | 非核心失败不影响主流程 |
| 实现复杂度 | 简单 | 需要额外的队列基础设施 |
| 数据一致性 | 强一致 | 最终一致 |
::: tip 什么时候该用异步?
三个判断标准:**耗时长**(超过 1-2 秒)、**非核心**(失败不应影响主流程)、**可延迟**(不需要立刻得到结果)。满足其中任意两个,就应该考虑异步化。
:::
---
## 2. 生产消费模型:任务的"流水线"
异步任务队列的核心是经典的 **生产者-消费者模式(Producer-Consumer Pattern**。这个模式有三个角色:
- **生产者(Producer)**:产生任务的一方,通常是 Web 服务器处理用户请求时
- **队列(Queue)**:存储待处理任务的缓冲区,通常用 Redis、RabbitMQ 等实现
- **消费者(Consumer/Worker)**:从队列中取出任务并执行的工作进程
<TaskWorkerDemo />
::: tip 队列的三大价值
1. **解耦**:生产者不需要知道谁来处理任务,消费者不需要知道任务从哪来
2. **削峰填谷**:突发流量时任务先堆积在队列中,消费者按自己的节奏处理
3. **可靠性**:任务持久化在队列中,即使消费者崩溃也不会丢失
:::
| 组件 | 职责 | 常见实现 |
|------|------|---------|
| 消息中间件 | 存储和转发任务消息 | Redis、RabbitMQ、Kafka |
| 序列化器 | 将任务参数序列化/反序列化 | JSON、MessagePack、Pickle |
| 调度器 | 管理定时任务和延迟任务 | Cron、APScheduler、node-cron |
| 结果存储 | 保存任务执行结果 | Redis、数据库、S3 |
---
## 3. 可靠性保障:任务不能"丢了"也不能"重复"
在分布式环境中,网络抖动、服务重启、资源不足等问题随时可能发生。异步任务系统必须具备完善的可靠性保障机制。
最核心的两个问题:**任务丢失**(消费者处理到一半崩溃了)和**重复执行**(任务被投递了两次)。
<TaskRetryDemo />
::: tip 可靠性三板斧
1. **ACK 机制**:消费者处理完任务后才发送确认(ACK),未确认的任务会被重新投递
2. **重试策略**:任务失败后按策略重试,指数退避 + 抖动是最佳实践
3. **幂等性设计**:同一个任务执行多次和执行一次的效果相同,通过唯一 ID 去重实现
:::
| 机制 | 解决的问题 | 实现方式 |
|------|-----------|---------|
| ACK 确认 | 任务丢失 | 处理完成后手动确认,超时未确认则重新投递 |
| 死信队列(DLQ) | 反复失败的"毒消息" | 重试超过上限后转入死信队列,人工介入处理 |
| 幂等性 | 重复执行 | 用任务唯一 ID 做去重,数据库唯一约束 |
| 优先级队列 | 任务饥饿 | 高优先级任务优先处理,避免被低优先级任务阻塞 |
| 超时控制 | 任务卡死 | 设置最大执行时间,超时自动终止并重试 |
---
## 4. 框架选型:选择适合你的工具
不同语言生态有不同的异步任务框架,它们在功能丰富度、性能、易用性上各有侧重。选择框架时,首先考虑你的技术栈,然后根据项目规模和需求做决定。
<AsyncComparisonDemo />
::: tip 选型建议
- **Python 项目**:中大型用 Celery,小型用 RQ
- **Node.js 项目**:首选 BullMQBull 的下一代)
- **Ruby 项目**Sidekiq 几乎是唯一选择
- **Java 项目**Spring 生态用 Spring Batch,高吞吐用 Kafka Streams
- **Go 项目**Asynq(基于 Redis)或 Machinery
如果你的项目已经在用 Redis,那么基于 Redis 的方案(Celery+Redis、BullMQ、Sidekiq)是最简单的起步方式。
:::
---
## 总结
异步任务队列是后端架构中不可或缺的基础设施。它让系统能够优雅地处理耗时操作,提升用户体验的同时提高系统吞吐量。
回顾本章的关键要点:
1. **异步化的判断标准**:耗时长、非核心、可延迟,满足两个就该异步化
2. **生产消费模型**Producer → Queue → Consumer,三者解耦协作
3. **Worker 池**:多个 Worker 并行消费,提高处理能力
4. **可靠性保障**:ACK 确认 + 重试策略 + 幂等性,三者缺一不可
5. **框架选型**:根据技术栈和项目规模选择,Redis 是最常见的消息中间件
## 延伸阅读
- [Celery 官方文档](https://docs.celeryq.dev/) - Python 最流行的分布式任务队列
- [BullMQ 文档](https://docs.bullmq.io/) - Node.js 高性能任务队列
- [Sidekiq Wiki](https://github.com/sidekiq/sidekiq/wiki) - Ruby 生态的任务处理标杆
- [RabbitMQ Tutorials](https://www.rabbitmq.com/tutorials) - 消息中间件入门教程
- [异步任务最佳实践](https://brandur.org/job-drain) - 任务队列的设计模式与陷阱
@@ -0,0 +1,751 @@
# 后端项目架构设计
::: tip 🎯 核心问题
**API 越写越多,代码越来越乱,如何设计一个清晰、可维护的后端项目结构?** 这就像问:你是把所有工具都扔进一个抽屉,还是按功能分类整理?好的项目架构能让团队协作更高效,让系统扩展更轻松。
:::
---
## 1. 为什么要关注后端项目架构?
### 1.1 从小脚本到大系统的演变
很多初学者刚开始写后端时,代码结构非常简单:
```python
# app.py - 所有代码在一个文件
from flask import Flask, request, jsonify
import sqlite3
app = Flask(__name__)
@app.route('/users', methods=['GET'])
def get_users():
conn = sqlite3.connect('db.sqlite')
users = conn.execute('SELECT * FROM users').fetchall()
return jsonify(users)
@app.route('/users', methods=['POST'])
def create_user():
data = request.json
conn = sqlite3.connect('db.sqlite')
conn.execute('INSERT INTO users (name, email) VALUES (?, ?)',
(data['name'], data['email']))
conn.commit()
return jsonify({'message': 'User created'})
# 还有订单、商品、支付...所有接口都在这个文件
```
几百行代码搞定一切,简单直接。但随着业务发展,问题开始出现:
- **接口多了**:一个文件几千行,找代码像"考古"
- **逻辑复杂了**:业务规则散落在各处,修改容易遗漏
- **数据库操作重复**:到处写 SQL,改表结构要改几十处
- **测试困难**:代码耦合严重,单元测试难以编写
**问题的本质**:没有"章法",所有的逻辑都堆在一起,就像把所有的工具、零件、说明书都扔进一个抽屉。
### 1.2 好的架构像整理好的车间
想象一个整理好的工厂车间:
| 区域 | 功能 | 特点 |
|------|------|------|
| **原料区** | 存放原材料 | 分类摆放,标签清晰 |
| **加工区** | 生产加工 | 流水线作业,工序明确 |
| **质检区** | 质量检查 | 统一标准,严格把关 |
| **成品区** | 存放成品 | 整齐有序,易于出库 |
| **工具室** | 存放工具 | 按需借用,用完归还 |
**好的后端架构**就是把代码也这样组织:每一层只关心自己的职责,数据像流水一样在各层之间传递。
::: tip 💡 通俗比喻:餐厅后厨的组织
把后端系统想象成一家餐厅的后厨:
- **`controllers/`(出餐口)** = 服务员接单:接收订单、核对信息、上菜
- **`services/`(厨师团队)** = 厨师做菜:按照菜谱加工、协调各工序
- **`repositories/`(仓库管理)** = 仓管取料:从仓库取食材、记录库存
- **`models/`(菜谱标准)** = 菜谱定义:宫保鸡丁需要什么料、什么口味
- **`utils/`(工具柜)** = 厨具存放:刀、勺、秤等通用工具
**关键点**:每个角色职责明确,不会越界。服务员不会自己炒菜,厨师不会擅自改菜谱。
:::
---
## 2. 经典分层架构详解
### 2.1 四层架构(Controller-Service-Repository-Model
最经典的后端分层架构如下:
```
my-backend-project/
├── src/
│ ├── controllers/ # 控制器层(Controller
│ │ ├── userController.js
│ │ ├── orderController.js
│ │ └── index.js
│ ├── services/ # 业务逻辑层(Service
│ │ ├── userService.js
│ │ ├── orderService.js
│ │ └── index.js
│ ├── repositories/ # 数据访问层(Repository/DAO
│ │ ├── userRepository.js
│ │ └── index.js
│ ├── models/ # 数据模型层(Model/Entity
│ │ ├── user.js
│ │ ├── order.js
│ │ └── index.js
│ ├── middlewares/ # 中间件
│ │ ├── auth.js
│ │ ├── errorHandler.js
│ │ └── validator.js
│ ├── utils/ # 工具函数
│ │ ├── logger.js
│ │ ├── response.js
│ │ └── validator.js
│ ├── config/ # 配置文件
│ │ ├── database.js
│ │ ├── redis.js
│ │ └── index.js
│ ├── routes/ # 路由定义
│ │ ├── userRoutes.js
│ │ ├── index.js
│ │ └── api.js
│ ├── jobs/ 或 workers/ # 定时任务/后台任务
│ │ └── emailWorker.js
│ ├── events/ 或 subscribers/ # 事件监听
│ │ └── userEvents.js
│ └── app.js # 应用入口
├── tests/ # 测试文件
│ ├── unit/
│ ├── integration/
│ └── e2e/
├── migrations/ # 数据库迁移
├── seeds/ # 种子数据
├── docs/ # 文档
├── .env # 环境变量
├── package.json
└── README.md
```
::: tip 📊 从图解中你能看到什么?
**分层逻辑**
```
┌─────────────────────────────────────────┐
│ Controller 层(控制器层) │ ← 接待员:接收请求,返回响应
│ - 接收 HTTP 请求 │
│ - 参数校验、权限检查 │
│ - 调用 Service │
│ - 格式化响应 │
├─────────────────────────────────────────┤
│ Service 层(业务逻辑层) │ ← 厨师:处理核心业务
│ - 业务逻辑编排 │
│ - 事务管理 │
│ - 调用 Repository │
│ - 跨模块协调 │
├─────────────────────────────────────────┤
│ Repository 层(数据访问层) │ ← 仓管员:管理数据存取
│ - 数据库操作 │
│ - ORM 封装 │
│ - 查询构建 │
├─────────────────────────────────────────┤
│ Model 层(数据模型层) │ ← 菜谱标准:定义数据结构
│ - 实体定义(Entity) │
│ - 类型定义 │
│ - 业务规则验证 │
└─────────────────────────────────────────┘
```
**依赖方向**
```
Controller → Service → Repository → Model
Middleware / Utils
```
上层依赖下层,下层不依赖上层。Model 是核心,所有层都可能依赖它。
:::
### 2.2 各层职责详解
#### Controller 层:请求的"接待员"
Controller 是系统的"门面",负责接收 HTTP 请求并返回响应。
**职责**
- 接收和解析请求参数
- 调用相应的 Service 处理业务
- 格式化响应数据
- 处理 HTTP 相关逻辑(状态码、Header 等)
**不应该做的事**
- 直接操作数据库
- 编写复杂业务逻辑
- 处理事务
::: details 📝 Controller 代码示例(Node.js/Express
```javascript
// controllers/userController.js
const userService = require('../services/userService')
const { success, error } = require('../utils/response')
class UserController {
// 获取用户列表
async list(req, res) {
try {
const { page = 1, limit = 10 } = req.query
const users = await userService.getUsers({ page, limit })
return success(res, users)
} catch (err) {
return error(res, err.message, 500)
}
}
// 获取单个用户
async getById(req, res) {
try {
const { id } = req.params
const user = await userService.getUserById(id)
if (!user) {
return error(res, 'User not found', 404)
}
return success(res, user)
} catch (err) {
return error(res, err.message, 500)
}
}
// 创建用户
async create(req, res) {
try {
const userData = req.body
const newUser = await userService.createUser(userData)
return success(res, newUser, 201)
} catch (err) {
return error(res, err.message, 400)
}
}
}
module.exports = new UserController()
```
:::
#### Service 层:业务的"厨师"
Service 是系统的"大脑",包含核心业务逻辑。
**职责**
- 实现业务规则和流程
- 协调多个 Repository 完成复杂操作
- 管理事务
- 数据转换和计算
**不应该做的事**
- 直接处理 HTTP 请求/响应
- 直接操作数据库(通过 Repository
::: details 📝 Service 代码示例
```javascript
// services/userService.js
const userRepository = require('../repositories/userRepository')
const orderRepository = require('../repositories/orderRepository')
const emailService = require('./emailService')
const { hashPassword } = require('../utils/crypto')
class UserService {
// 获取用户列表
async getUsers({ page, limit }) {
const offset = (page - 1) * limit
const [users, total] = await Promise.all([
userRepository.findAll({ limit, offset }),
userRepository.count()
])
return {
data: users,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit)
}
}
}
// 获取用户详情(包含订单信息)
async getUserById(id) {
const user = await userRepository.findById(id)
if (!user) return null
// 获取用户订单统计
const orderStats = await orderRepository.getStatsByUserId(id)
return {
...user,
orderStats
}
}
// 创建用户(包含事务和邮件通知)
async createUser(userData) {
// 检查邮箱是否已存在
const existingUser = await userRepository.findByEmail(userData.email)
if (existingUser) {
throw new Error('Email already exists')
}
// 密码加密
const hashedPassword = await hashPassword(userData.password)
// 创建用户
const newUser = await userRepository.create({
...userData,
password: hashedPassword
})
// 发送欢迎邮件(异步,不阻塞)
emailService.sendWelcomeEmail(newUser.email).catch(console.error)
return newUser
}
}
module.exports = new UserService()
```
:::
#### Repository 层:数据的"仓管员"
Repository 负责所有与数据存储相关的操作。
**职责**
- 数据库的增删改查
- ORM 映射
- 查询优化
**不应该做的事**
- 包含业务逻辑
- 处理事务(由 Service 控制)
::: details 📝 Repository 代码示例
```javascript
// repositories/userRepository.js
const { User } = require('../models')
class UserRepository {
// 查询所有用户
async findAll({ limit, offset }) {
return await User.findAll({
limit,
offset,
attributes: { exclude: ['password'] } // 不返回密码
})
}
// 根据 ID 查询
async findById(id) {
return await User.findByPk(id, {
attributes: { exclude: ['password'] }
})
}
// 根据邮箱查询
async findByEmail(email) {
return await User.findOne({ where: { email } })
}
// 创建用户
async create(data) {
return await User.create(data)
}
// 更新用户
async update(id, data) {
const user = await User.findByPk(id)
if (!user) return null
return await user.update(data)
}
// 删除用户
async delete(id) {
const user = await User.findByPk(id)
if (!user) return null
await user.destroy()
return true
}
// 统计用户数量
async count() {
return await User.count()
}
}
module.exports = new UserRepository()
```
:::
#### Model 层:数据的"定义"
Model 定义数据结构和业务规则。
::: details 📝 Model 代码示例(Sequelize
```javascript
// models/user.js
const { DataTypes } = require('sequelize')
const { sequelize } = require('../config/database')
const User = sequelize.define('User', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
name: {
type: DataTypes.STRING(100),
allowNull: false,
validate: {
len: [2, 100]
}
},
email: {
type: DataTypes.STRING(255),
allowNull: false,
unique: true,
validate: {
isEmail: true
}
},
password: {
type: DataTypes.STRING(255),
allowNull: false
},
status: {
type: DataTypes.ENUM('active', 'inactive', 'banned'),
defaultValue: 'active'
}
}, {
tableName: 'users',
timestamps: true, // 自动添加 createdAt 和 updatedAt
indexes: [
{ fields: ['email'] },
{ fields: ['status'] }
]
})
module.exports = User
```
:::
---
## 3. 其他重要目录
### 3.1 `middlewares/` 中间件
中间件是请求处理流程中的"过滤器"。
```
middlewares/
├── auth.js # 认证中间件
├── errorHandler.js # 错误处理
├── validator.js # 参数校验
├── rateLimiter.js # 限流
├── logger.js # 请求日志
└── cors.js # 跨域处理
```
::: details 📝 中间件示例
```javascript
// middlewares/auth.js
const jwt = require('jsonwebtoken')
const authMiddleware = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1]
if (!token) {
return res.status(401).json({ message: 'No token provided' })
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET)
req.user = decoded
next()
} catch (err) {
return res.status(401).json({ message: 'Invalid token' })
}
}
module.exports = authMiddleware
```
:::
### 3.2 `routes/` 路由
集中管理所有 API 路由。
```javascript
// routes/userRoutes.js
const express = require('express')
const router = express.Router()
const userController = require('../controllers/userController')
const authMiddleware = require('../middlewares/auth')
// 公开路由
router.get('/', userController.list)
router.get('/:id', userController.getById)
// 需要认证的路由
router.post('/', authMiddleware, userController.create)
router.put('/:id', authMiddleware, userController.update)
router.delete('/:id', authMiddleware, userController.delete)
module.exports = router
```
```javascript
// routes/index.js
const express = require('express')
const router = express.Router()
router.use('/users', require('./userRoutes'))
router.use('/orders', require('./orderRoutes'))
router.use('/products', require('./productRoutes'))
module.exports = router
```
### 3.3 `config/` 配置
集中管理所有配置,支持多环境。
```javascript
// config/index.js
const env = process.env.NODE_ENV || 'development'
const configs = {
development: {
port: 3000,
database: {
host: 'localhost',
port: 5432,
name: 'myapp_dev'
},
redis: {
host: 'localhost',
port: 6379
}
},
production: {
port: process.env.PORT || 80,
database: {
host: process.env.DB_HOST,
port: process.env.DB_PORT,
name: process.env.DB_NAME
}
}
}
module.exports = configs[env]
```
### 3.4 `utils/` 工具
```
utils/
├── logger.js # 日志工具
├── response.js # 响应封装
├── crypto.js # 加密解密
├── date.js # 日期处理
└── validator.js # 验证工具
```
---
## 4. 按功能组织(Feature-based
对于中大型项目,可以采用按功能组织的方式:
```
src/
├── features/
│ ├── users/
│ │ ├── users.controller.js
│ │ ├── users.service.js
│ │ ├── users.repository.js
│ │ ├── users.model.js
│ │ ├── users.routes.js
│ │ ├── users.validator.js
│ │ └── index.js # 统一导出
│ ├── orders/
│ │ ├── orders.controller.js
│ │ ├── orders.service.js
│ │ └── ...
│ └── products/
│ ├── products.controller.js
│ └── ...
├── shared/ # 共享资源
│ ├── middlewares/
│ ├── utils/
│ └── config/
└── app.js
```
**优点**
- 高内聚,一个功能的所有代码在一起
- 便于团队协作,不同人负责不同 feature
- 易于删除或重构
---
## 5. 知名开源项目的架构参考
### 5.1 Express.js 官方示例
```
express-example/
├── bin/ # 启动脚本
├── public/ # 静态资源
├── routes/ # 路由
├── views/ # 视图模板
├── app.js # 应用配置
└── package.json
```
**特点**:简单直接,适合小型项目。
### 5.2 NestJS(企业级 Node.js 框架)
```
nestjs-project/
├── src/
│ ├── modules/ # 功能模块
│ │ ├── users/
│ │ │ ├── users.controller.ts
│ │ │ ├── users.service.ts
│ │ │ ├── users.module.ts
│ │ │ └── dto/
│ │ └── orders/
│ ├── common/ # 共享模块
│ ├── config/ # 配置
│ └── main.ts # 入口
```
**特点**
- 强制模块化结构
- 内置依赖注入
- 适合大型项目
### 5.3 DjangoPython
```
django-project/
├── project_name/ # 项目配置
├── apps/
│ ├── users/ # 用户应用
│ │ ├── models.py
│ │ ├── views.py
│ │ ├── serializers.py
│ │ └── urls.py
│ └── orders/ # 订单应用
├── templates/
├── static/
└── manage.py
```
**特点**
- 约定优于配置
- MTVModel-Template-View)模式
- 应用可复用
### 5.4 Spring BootJava
```
spring-boot-project/
├── src/main/java/
│ └── com/example/
│ ├── controller/
│ ├── service/
│ ├── repository/
│ ├── entity/
│ ├── dto/
│ ├── config/
│ └── Application.java
├── src/main/resources/
│ ├── application.yml
│ └── mapper/
└── src/test/
```
**特点**
- 严格的分层架构
- 注解驱动开发
- 强大的生态
---
## 6. 架构设计原则与检查清单
### 6.1 核心原则
| 原则 | 说明 | 实践建议 |
|------|------|----------|
| **单一职责** | 一个模块只做一件事 | Controller 只处理 HTTPService 只处理业务 |
| **依赖倒置** | 依赖抽象而非具体实现 | 使用接口/抽象类 |
| **开闭原则** | 对扩展开放,对修改关闭 | 新增功能不修改原有代码 |
| **DRY** | 不要重复自己 | 提取公共逻辑到 utils 或基类 |
| **KISS** | 保持简单 | 不要过度设计 |
### 6.2 检查清单
**分层检查**
- [ ] Controller 是否只处理 HTTP 相关逻辑?
- [ ] Service 是否包含核心业务逻辑?
- [ ] Repository 是否只负责数据访问?
- [ ] 层与层之间是否通过明确的接口交互?
**代码质量**
- [ ] 是否有统一的错误处理机制?
- [ ] 是否使用环境变量管理配置?
- [ ] 是否有日志记录?
- [ ] 是否编写了单元测试?
**安全**
- [ ] 敏感配置是否放入环境变量?
- [ ] 是否有输入验证?
- [ ] 是否有认证和授权?
- [ ] 密码是否加密存储?
---
## 7. 总结
::: tip 💡 核心思想
好的后端架构应该像一家组织良好的餐厅:
- **分工明确**:每个角色知道自己的职责
- **流程清晰**:数据像流水一样在各层之间传递
- **易于扩展**:新增功能不会破坏现有结构
- **便于测试**:各层可以独立测试
**记住这几点**
1. **分层是手段,不是目的**:不要为了分层而分层
2. **按功能组织**:中大型项目推荐 Feature-based
3. **统一约定**:命名、结构、错误处理保持一致
4. **持续重构**:定期审视架构,及时调整
**最终目标**:让代码像整理好的车间一样,想找什么立刻能找到,新功能容易添加,旧代码容易维护。
:::
---
## 参考资源
- [NestJS 文档](https://docs.nestjs.com/)
- [Express 最佳实践](https://expressjs.com/en/advanced/best-practice-security.html)
- [Bulletproof Node.js](https://github.com/santiq/bulletproof-nodejs)
- [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
@@ -1,3 +1,162 @@
# 文件存储与对象存储
> 待实现
::: tip 前言
**用户上传了一张头像,你把它存在服务器的 `/uploads` 目录下——然后服务器磁盘满了,或者你加了第二台服务器,用户发现头像时有时无。** 文件存储看似简单,但在分布式环境下却是一个需要认真对待的架构问题。对象存储就是互联网时代解决这个问题的标准答案。
:::
**这篇文章会带你学什么?**
学完这章后,你将获得:
- **存储类型认知**:理解块存储、文件存储、对象存储的区别和适用场景
- **对象存储核心概念**:掌握 Bucket、Object、Key、Pre-signed URL 等核心概念
- **上传方案设计**:学会客户端直传 vs 服务端中转的方案选型
- **CDN 加速原理**:理解 CDN 如何加速静态资源的全球分发
- **最佳实践**:掌握文件命名、权限控制、生命周期管理等实战技巧
| 章节 | 内容 | 核心概念 |
|-----|------|---------|
| **第 1 章** | 存储类型对比 | 块存储、文件存储、对象存储 |
| **第 2 章** | 对象存储核心概念 | Bucket、Object、Key、元数据 |
| **第 3 章** | 文件上传方案 | 客户端直传、Pre-signed URL |
| **第 4 章** | CDN 加速 | 边缘节点、缓存策略、回源 |
| **第 5 章** | 最佳实践 | 命名规范、权限、生命周期 |
---
## 0. 全景图:为什么不能把文件存在服务器本地?
刚开始做项目时,把用户上传的文件存在服务器本地目录是最直觉的做法。但随着项目发展,你会遇到一系列问题:
- **磁盘空间有限**:服务器磁盘总会满,扩容麻烦
- **多服务器不共享**:负载均衡后,用户请求可能打到不同服务器,文件找不到
- **没有备份**:服务器挂了,文件就丢了
- **没有 CDN**:全球用户访问同一台服务器,速度慢
::: tip 对象存储的核心价值
对象存储(如 AWS S3、阿里云 OSS)解决了所有这些问题:**容量无限、全球可访问、自动备份、天然支持 CDN**。它已经成为互联网应用存储文件的事实标准。
:::
---
## 1. 存储类型对比:块、文件、对象
计算机世界有三种主要的存储方式,它们解决不同层次的问题。
<FileStorageTypeDemo />
| 维度 | 块存储 | 文件存储 | 对象存储 |
|------|--------|---------|---------|
| 数据单位 | 固定大小的块 | 文件 + 目录 | 对象(Key-Value |
| 访问协议 | iSCSI/FC | NFS/SMB | HTTP REST API |
| 性能 | 最高(毫秒级) | 中等 | 较低(但够用) |
| 扩展性 | 有限 | 中等 | 近乎无限 |
| 成本 | 最高 | 中等 | 最低 |
| 典型场景 | 数据库 | 共享文件 | 图片/视频/备份 |
::: tip 简单记忆
- **块存储**像硬盘——给数据库用
- **文件存储**像网络共享文件夹——给多台服务器共享配置用
- **对象存储**像网盘——给用户上传的图片、视频用
:::
---
## 2. 对象存储核心概念
对象存储的数据模型非常简单:**Bucket(桶)** 是容器,**Object(对象)** 是文件,每个对象通过唯一的 **Key(键)** 来标识。
```
my-app-bucket/ ← Bucket(桶)
├── avatars/user-123.jpg ← Object Key
├── avatars/user-456.png ← Object Key
├── reports/2024/q1-report.pdf ← Object Key"目录"只是 Key 的前缀)
└── uploads/temp/file.zip ← Object Key
```
| 概念 | 说明 | 示例 |
|------|------|------|
| Bucket | 存储容器,全局唯一命名 | `my-app-prod``company-assets` |
| Object | 存储的文件本体 + 元数据 | 一张图片、一个 PDF |
| Key | 对象的唯一标识符 | `avatars/user-123.jpg` |
| 元数据 | 对象的附加信息 | Content-Type、自定义标签 |
| ACL | 访问控制列表 | public-read、private |
| Pre-signed URL | 临时授权访问链接 | 有效期 15 分钟的上传/下载链接 |
::: tip 对象存储没有真正的"目录"
`avatars/user-123.jpg` 中的 `avatars/` 不是目录,只是 Key 的前缀。对象存储是扁平结构,所有对象在同一层级。控制台显示的"文件夹"只是按前缀分组的视觉效果。
:::
---
## 3. 文件上传方案:谁来传文件?
文件上传有两种主流方案:服务端中转和客户端直传。对于大多数场景,**客户端直传**是更优的选择。
<FileUploadFlowDemo />
::: tip 客户端直传的优势
1. **节省服务器带宽**:文件不经过你的服务器,直接到 OSS
2. **避免超时**:大文件上传不会触发 Nginx/网关的超时限制
3. **降低服务器负载**:服务器只需要签发凭证,不需要处理文件流
4. **支持断点续传**:OSS 原生支持分片上传,前端可以实现断点续传
实现步骤:前端请求后端获取 Pre-signed URL → 前端用这个 URL 直接上传到 OSS → OSS 回调通知后端
:::
---
## 4. CDN 加速:让全球用户都快
当你的用户遍布全球时,从单一源站下载文件会很慢。CDNContent Delivery Network)通过在全球部署边缘节点,将文件缓存到离用户最近的节点,大幅降低访问延迟。
<CDNAccelerationDemo />
| CDN 概念 | 说明 |
|---------|------|
| 边缘节点 | 分布在全球各地的缓存服务器 |
| 回源 | 边缘节点没有缓存时,向源站请求文件 |
| 缓存命中率 | 请求被边缘节点直接响应的比例,越高越好 |
| TTL | 缓存有效期,过期后需要重新回源 |
| 缓存刷新 | 主动清除边缘节点的缓存,让新文件生效 |
::: tip CDN 最佳实践
- **文件名加 hash**`logo.a3f2b1.png` 而不是 `logo.png`,这样更新文件时不需要刷新缓存
- **设置合理的 TTL**:静态资源(JS/CSS/图片)设长 TTL1年),HTML 设短 TTL5分钟)
- **开启 Gzip/Brotli 压缩**:文本类资源压缩后体积减少 60-80%
:::
---
## 5. 最佳实践
| 实践 | 说明 | 示例 |
|------|------|------|
| Key 命名规范 | 用有意义的前缀组织文件 | `{type}/{date}/{uuid}.{ext}` |
| 避免热点 Key | 不要用递增数字开头 | 用 UUID 或 hash 前缀 |
| 权限最小化 | Bucket 默认 private | 只对需要公开的文件设置 public-read |
| 生命周期规则 | 自动清理过期文件 | 临时文件 7 天后自动删除 |
| 跨域配置 | 前端直传需要配置 CORS | 允许你的域名 PUT/POST |
| 服务端加密 | 敏感文件开启 SSE | SSE-S3 或 SSE-KMS |
---
## 总结
文件存储是每个 Web 应用都会遇到的基础问题。对象存储以其无限容量、低成本、高可用的特性,成为了互联网应用的标准选择。
回顾本章的关键要点:
1. **三种存储类型**:块存储给数据库、文件存储给共享、对象存储给用户文件
2. **对象存储模型**Bucket + Key + Object,扁平结构,HTTP API 访问
3. **客户端直传**Pre-signed URL 方案,文件不经过服务器,高效省资源
4. **CDN 加速**:边缘节点缓存 + 文件名 hash,让全球用户都快
5. **安全与管理**:权限最小化、生命周期规则、服务端加密
## 延伸阅读
- [AWS S3 开发者指南](https://docs.aws.amazon.com/s3/) - 对象存储的标杆文档
- [阿里云 OSS 最佳实践](https://help.aliyun.com/document_detail/31853.html) - 国内最常用的对象存储
- [MinIO 文档](https://min.io/docs/minio/linux/index.html) - 开源的 S3 兼容对象存储
- [Cloudflare R2](https://developers.cloudflare.com/r2/) - 零出口费用的对象存储
- [Pre-signed URL 详解](https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html) - 客户端直传的核心机制
@@ -1,3 +1,131 @@
# 限流与背压控制
> 待实现
::: tip 前言
**双十一零点,几亿用户同时涌入——服务器扛得住吗?** 任何系统都有处理能力的上限。当请求量超过系统承载能力时,如果不加控制,结果就是所有人都用不了。限流和背压就是保护系统不被"压垮"的两道防线。
:::
**这篇文章会带你学什么?**
学完这章后,你将获得:
- **限流必要性**:理解为什么需要主动拒绝部分请求来保护系统
- **限流算法**:掌握令牌桶、漏桶、滑动窗口三种核心算法的原理和差异
- **背压机制**:理解当上游速度超过下游时的处理策略
- **多层限流**:了解从客户端到网关到服务的多层限流架构
- **实战能力**:知道在什么场景下选择什么限流策略
| 章节 | 内容 | 核心概念 |
|-----|------|---------|
| **第 1 章** | 为什么需要限流 | 雪崩效应、服务保护 |
| **第 2 章** | 限流算法 | 令牌桶、漏桶、滑动窗口 |
| **第 3 章** | 背压控制 | 缓冲区、丢弃策略、弹性扩容 |
| **第 4 章** | 多层限流架构 | 客户端、网关、服务端 |
| **第 5 章** | 实战与选型 | Nginx、Redis、Sentinel |
---
## 0. 全景图:为什么要"拒绝"用户?
这听起来很反直觉——我们不是应该服务好每一个用户吗?但现实是:**不拒绝一部分请求,所有请求都会失败**。
想象一个只能坐 100 人的餐厅,突然涌进来 1000 人。如果不限流,结果不是 1000 人都能吃上饭,而是厨房崩溃、服务员瘫痪,1000 人谁都吃不上。正确的做法是在门口排队限流,让 100 人先进去,其余人等候。
::: tip 限流的核心目标
- **保护系统**:防止过载导致服务完全不可用
- **公平分配**:确保已接受的请求能正常处理
- **优雅降级**:被限流的请求收到明确的 429 状态码,而不是超时或 500 错误
:::
---
## 1. 限流算法:三种经典方案
限流的核心问题是:**在单位时间内,最多允许多少个请求通过?** 不同的算法在精确度、突发流量处理、实现复杂度上各有取舍。
<RateLimitAlgorithmDemo />
| 算法 | 原理 | 突发流量 | 精确度 | 实现复杂度 |
|------|------|---------|--------|-----------|
| 令牌桶 | 固定速率放令牌,请求消耗令牌 | 允许(桶中有存量) | 高 | 中 |
| 漏桶 | 请求排队,固定速率处理 | 不允许(完全平滑) | 高 | 中 |
| 滑动窗口 | 统计窗口内请求数 | 部分允许 | 较高 | 低 |
| 固定窗口 | 按时间窗口计数 | 边界处可能突发 | 低 | 最低 |
::: tip 选哪个算法?
- **API 限流**:令牌桶最常用,允许合理的突发流量
- **流量整形**:漏桶适合需要恒定输出速率的场景
- **简单计数**:滑动窗口实现简单,适合大多数 Web 应用
:::
---
## 2. 背压控制:当上游比下游快
限流解决的是"外部请求太多"的问题,而**背压(Backpressure**解决的是"内部组件速度不匹配"的问题。
当生产者产生数据的速度持续超过消费者处理数据的速度时,中间的缓冲区会不断膨胀,最终导致内存溢出或数据丢失。背压机制就是让消费者能够"反向通知"生产者减速。
<BackpressureDemo />
::: tip 背压的四种策略
1. **丢弃(Drop**:缓冲区满时丢弃新数据或旧数据,适合实时性要求高但允许丢失的场景
2. **阻塞(Block**:让生产者暂停,等消费者处理完再继续,适合数据不能丢失的场景
3. **采样(Sample**:只处理部分数据,适合高频数据流
4. **弹性扩容(Scale**:动态增加消费者数量,适合云原生环境
:::
---
## 3. 多层限流架构
生产环境中,限流不是在某一个点做就够了,而是需要**多层防护**,每一层解决不同粒度的问题。
| 层级 | 位置 | 限流粒度 | 工具 |
|------|------|---------|------|
| 客户端 | 前端/App | 按钮防抖、请求节流 | lodash.throttle、debounce |
| CDN/WAF | 边缘节点 | IP 级别、地域级别 | Cloudflare Rate Limiting |
| API 网关 | 入口网关 | 路由级别、用户级别 | Nginx limit_req、Kong |
| 服务端 | 应用内部 | 接口级别、资源级别 | Sentinel、Resilience4j |
| 数据库 | 存储层 | 连接数、QPS | 连接池配置、慢查询熔断 |
::: tip 限流的 HTTP 规范
被限流的请求应该返回 `429 Too Many Requests` 状态码,并在响应头中包含:
- `Retry-After`: 建议客户端多久后重试(秒数或日期)
- `X-RateLimit-Limit`: 限流上限
- `X-RateLimit-Remaining`: 剩余配额
- `X-RateLimit-Reset`: 配额重置时间
:::
---
## 4. 实战选型
| 场景 | 推荐方案 | 说明 |
|------|---------|------|
| Nginx 入口限流 | `limit_req_zone` | 基于漏桶算法,配置简单 |
| 分布式限流 | Redis + Lua 脚本 | 令牌桶或滑动窗口,多实例共享计数 |
| Java 微服务 | Sentinel / Resilience4j | 支持熔断、降级、热点限流 |
| Node.js API | express-rate-limit | 简单易用,支持 Redis 存储 |
| Go 服务 | golang.org/x/time/rate | 标准库令牌桶实现 |
---
## 总结
限流和背压是保护系统稳定性的两道关键防线。限流控制外部流量的涌入速度,背压协调内部组件的处理速度。
回顾本章的关键要点:
1. **限流的必要性**:不拒绝部分请求,所有请求都会失败
2. **三种核心算法**:令牌桶(允许突发)、漏桶(完全平滑)、滑动窗口(简单精确)
3. **背压机制**:丢弃、阻塞、采样、扩容四种策略
4. **多层防护**:从客户端到数据库,每层解决不同粒度的问题
5. **429 规范**:被限流时返回标准状态码和限流头信息
## 延伸阅读
- [Stripe 的限流实践](https://stripe.com/blog/rate-limiters) - 支付系统的限流设计
- [Nginx limit_req 文档](https://nginx.org/en/docs/http/ngx_http_limit_req_module.html) - Nginx 限流模块
- [Alibaba Sentinel](https://sentinelguard.io/) - 面向分布式服务的流量控制组件
- [Resilience4j](https://resilience4j.readme.io/) - Java 轻量级容错库
- [Token Bucket 算法详解](https://en.wikipedia.org/wiki/Token_bucket) - 令牌桶算法的数学原理
@@ -1,3 +1,295 @@
# 一个请求的完整旅程
> 待实现
::: tip 前言
**当你在浏览器里输入一个网址按下回车,到页面显示出来,中间到底发生了什么?** 这个问题是面试经典题,更是理解整个 Web 架构的钥匙。搞懂这条链路,你就能理解前端、后端、网络、数据库是怎么协作的。
:::
**这篇文章会带你学什么?**
学完这章后,你将获得:
- **全链路视角**:理解一个 HTTP 请求从发出到返回的完整过程
- **各层职责认知**:DNS、TCP、负载均衡、Web 服务器、应用服务器、数据库各自做什么
- **问题定位能力**:请求慢或失败时,知道从哪一层开始排查
- **性能优化思路**:每一层都有优化空间,知道优化点在哪里
| 章节 | 内容 | 核心概念 |
|-----|------|---------|
| **第 1 章** | 浏览器发起请求 | DNS 解析、TCP 连接、HTTP 请求 |
| **第 2 章** | 网络传输 | 路由、CDN、负载均衡 |
| **第 3 章** | 服务器处理 | Web 服务器、应用逻辑、数据库查询 |
| **第 4 章** | 响应返回 | 序列化、压缩、渲染 |
| **第 5 章** | 全链路优化 | 缓存、连接复用、异步处理 |
---
## 0. 全景图:一个请求经历了什么?
用一个比喻来理解:你在网上下单买书,这个过程和 HTTP 请求惊人地相似。
| 请求阶段 | 买书类比 | 技术对应 |
|---------|---------|---------|
| 输入网址 | 你说"我要去某某书店" | 浏览器解析 URL |
| DNS 解析 | 查地图找到书店地址 | 域名 → IP 地址 |
| TCP 连接 | 走到书店门口,推门进去 | 三次握手建立连接 |
| 发送请求 | 告诉店员"我要《xxx》这本书" | HTTP 请求报文 |
| 服务器处理 | 店员去仓库找书、查库存、算价格 | 应用逻辑 + 数据库查询 |
| 返回响应 | 店员把书递给你 | HTTP 响应报文 |
| 浏览器渲染 | 你打开书开始阅读 | HTML/CSS/JS 解析渲染 |
<RequestJourneyFlow />
---
## 1. 浏览器发起请求
### 1.1 URL 解析
当你输入 `https://api.example.com/books?id=123` 时,浏览器会把它拆解成几个部分:
| 部分 | 值 | 含义 |
|-----|-----|------|
| 协议 | `https` | 用加密方式通信 |
| 域名 | `api.example.com` | 服务器的"名字" |
| 路径 | `/books` | 要访问的资源 |
| 查询参数 | `id=123` | 附加条件 |
### 1.2 DNS 解析:域名 → IP 地址
计算机不认识域名,只认识 IP 地址(如 `93.184.216.34`)。DNS 就是互联网的"电话簿"。
```
浏览器缓存 → 系统缓存 → 路由器缓存 → ISP DNS → 根域名服务器
↓ 命中就直接用,不命中就往下查
```
::: tip DNS 缓存的意义
如果每次请求都从根域名服务器查起,全球互联网会被 DNS 查询压垮。所以每一层都有缓存,大部分请求在浏览器或系统层就能解析完成。
:::
### 1.3 TCP 三次握手
找到 IP 地址后,浏览器需要和服务器"建立连接"。TCP 用三次握手确保双方都准备好了:
```
客户端 → 服务器:你好,我想连接(SYN)
服务器 → 客户端:好的,我准备好了(SYN + ACK)
客户端 → 服务器:收到,开始通信(ACK)
```
如果是 HTTPS,还需要额外的 TLS 握手来协商加密方式。
### 1.4 发送 HTTP 请求
连接建立后,浏览器发送 HTTP 请求报文:
```http
GET /books?id=123 HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer eyJhbGci...
User-Agent: Chrome/120.0
```
| 组成部分 | 内容 |
|---------|------|
| 请求行 | 方法(GET)+ 路径 + 协议版本 |
| 请求头 | 元信息:身份认证、期望的数据格式等 |
| 请求体 | POST/PUT 请求才有,携带要提交的数据 |
---
## 2. 网络传输:请求在路上
### 2.1 路由转发
请求离开你的电脑后,会经过多个路由器的转发,就像快递经过多个中转站:
```
你的电脑 → 家庭路由器 → 运营商网络 → 骨干网 → 目标机房
```
每个路由器根据 IP 地址决定"下一跳"往哪里转发。可以用 `traceroute` 命令查看请求经过了哪些节点。
### 2.2 CDN 加速
如果目标网站使用了 CDN(内容分发网络),请求可能不需要到达源服务器:
| 场景 | 走向 |
|-----|------|
| 请求静态资源(图片、CSS、JS) | CDN 边缘节点直接返回 |
| 请求动态数据(API) | 穿透 CDN,到达源服务器 |
CDN 的本质是"把内容提前放到离用户最近的地方"。
### 2.3 负载均衡
大型网站不会只有一台服务器。负载均衡器负责把请求分配到多台服务器上:
```
用户请求 → 负载均衡器 → 服务器 A(30% 流量)
→ 服务器 B(30% 流量)
→ 服务器 C(40% 流量)
```
常见的分配策略:
| 策略 | 原理 | 适用场景 |
|-----|------|---------|
| 轮询 | 依次分配 | 服务器配置相同 |
| 加权轮询 | 按权重分配 | 服务器配置不同 |
| IP 哈希 | 同一用户固定到同一台 | 需要会话保持 |
| 最少连接 | 分给当前连接最少的 | 请求处理时间差异大 |
---
## 3. 服务器处理:厨房里发生了什么
请求到达服务器后,会经过多层处理。
### 3.1 Web 服务器(Nginx / Apache
第一个接收请求的通常是 Web 服务器,它负责:
| 职责 | 说明 |
|-----|------|
| 静态文件服务 | 直接返回 HTML、CSS、JS、图片 |
| 反向代理 | 把 API 请求转发给后端应用 |
| SSL 终止 | 处理 HTTPS 加密解密 |
| 请求过滤 | 拦截恶意请求、限流 |
### 3.2 应用服务器处理
Web 服务器把请求转发给应用服务器(Node.js、Spring、Django 等),处理流程:
```
请求进入 → 中间件链 → 路由匹配 → 控制器 → 服务层 → 数据访问层
```
**中间件**做的事情:
1. 解析请求体(JSON、表单数据)
2. 验证身份(检查 Token
3. 检查权限(这个用户能访问这个接口吗?)
4. 记录日志(谁在什么时候访问了什么)
### 3.3 数据库查询
大部分请求最终都要和数据库打交道:
```
应用代码:SELECT * FROM books WHERE id = 123
数据库引擎:解析 SQL → 查询优化 → 执行计划 → 读取数据
返回结果:{ id: 123, title: "xxx", price: 59.9 }
```
::: tip 数据库是最常见的性能瓶颈
网络传输通常是毫秒级,应用逻辑也很快,但一个没有索引的数据库查询可能要几秒甚至几十秒。所以"慢请求"大概率是数据库查询慢。
:::
---
## 4. 响应返回:数据的归途
### 4.1 构造 HTTP 响应
服务器处理完后,构造响应报文:
```http
HTTP/1.1 200 OK
Content-Type: application/json
Content-Encoding: gzip
Cache-Control: max-age=3600
```
| 组成部分 | 内容 |
|---------|------|
| 状态行 | 协议版本 + 状态码(200 成功、404 未找到、500 服务器错误) |
| 响应头 | 数据格式、缓存策略、压缩方式等 |
| 响应体 | 实际的数据内容(JSON、HTML 等) |
### 4.2 数据压缩
服务器通常会用 gzip 或 brotli 压缩响应体,减少传输量:
| 压缩算法 | 压缩率 | 速度 |
|---------|--------|------|
| gzip | 约 70% | 快 |
| brotli | 约 80% | 较慢但压缩更好 |
一个 100KB 的 JSON,压缩后可能只有 20-30KB。
### 4.3 浏览器渲染
浏览器收到响应后:
1. **解析 HTML** → 构建 DOM 树
2. **解析 CSS** → 构建样式树
3. **合并** → 生成渲染树
4. **布局** → 计算每个元素的位置和大小
5. **绘制** → 把像素画到屏幕上
<RequestTimeline />
---
## 5. 全链路优化:每一层都能更快
### 5.1 各层优化手段
| 层级 | 优化手段 | 效果 |
|-----|---------|------|
| DNS | DNS 预解析、使用快速 DNS 服务 | 减少 DNS 查询时间 |
| 网络 | CDN、HTTP/2、连接复用 | 减少传输延迟 |
| 服务器 | 缓存(Redis)、异步处理 | 减少处理时间 |
| 数据库 | 索引、查询优化、读写分离 | 减少查询时间 |
| 前端 | 懒加载、代码分割、资源压缩 | 减少渲染时间 |
### 5.2 缓存:最有效的优化
缓存存在于请求链路的每一层:
```
浏览器缓存 → CDN 缓存 → 反向代理缓存 → 应用缓存(Redis)→ 数据库缓存
```
::: tip 缓存的本质
用空间换时间。把计算过的结果存起来,下次直接用,不用重新算。缓存命中率每提高 10%,系统性能可能提升数倍。
:::
### 5.3 请求失败时的排查思路
| 现象 | 可能的问题层 | 排查方法 |
|-----|------------|---------|
| 完全无响应 | DNS / 网络 | ping、nslookup |
| 连接超时 | 网络 / 服务器宕机 | telnet、curl |
| 返回 4xx | 客户端请求有误 | 检查 URL、参数、Token |
| 返回 5xx | 服务器内部错误 | 查看服务器日志 |
| 响应很慢 | 数据库 / 应用逻辑 | 查看慢查询日志、APM 工具 |
---
## 6. 总结
一个 HTTP 请求的完整旅程:
1. **浏览器**:解析 URL → DNS 查询 → TCP 连接 → 发送请求
2. **网络**:路由转发 → CDN 判断 → 负载均衡分发
3. **服务器**:Web 服务器接收 → 中间件处理 → 业务逻辑 → 数据库查询
4. **返回**:构造响应 → 压缩 → 网络传输 → 浏览器渲染
::: tip 理解全链路的价值
当你能在脑中画出请求的完整链路时,遇到任何问题都能快速定位到是哪一层出了问题。这是从"初级开发"到"能独立排查问题"的关键跨越。
:::
---
## 延伸阅读
- [HTTP 权威指南](https://developer.mozilla.org/zh-CN/docs/Web/HTTP) — MDN 的 HTTP 文档
- [High Performance Browser Networking](https://hpbn.co/) — 浏览器网络性能优化
- [What happens when...](https://github.com/alex/what-happens-when) — 经典的"输入 URL 后发生了什么"详解
- [What happens when...](https://github.com/alex/what-happens-when) — 经典的"输入 URL 后发生了什么"详解
@@ -1,3 +1,153 @@
# 搜索引擎原理
> 待实现
::: tip 前言
**你在淘宝搜"红色连衣裙",0.1 秒内从几十亿商品中找到了最相关的结果——这背后是怎么做到的?** 搜索引擎是互联网最核心的基础设施之一,从 Google 到电商站内搜索,它的核心原理都是一样的:倒排索引 + 相关性排序。
:::
**这篇文章会带你学什么?**
学完这章后,你将获得:
- **倒排索引**:理解搜索引擎最核心的数据结构
- **分词技术**:了解中文分词的挑战和常见方案
- **相关性排序**:掌握 TF-IDF 和 BM25 的基本原理
- **Elasticsearch**:了解最流行的搜索引擎的架构和使用场景
- **搜索优化**:掌握同义词、纠错、高亮等实用搜索功能
| 章节 | 内容 | 核心概念 |
|-----|------|---------|
| **第 1 章** | 倒排索引 | 正排索引 vs 倒排索引 |
| **第 2 章** | 分词与分析 | 中文分词、停用词、词干提取 |
| **第 3 章** | 相关性排序 | TF-IDF、BM25 |
| **第 4 章** | Elasticsearch | 分布式架构、分片、副本 |
| **第 5 章** | 搜索优化 | 同义词、纠错、自动补全 |
---
## 0. 全景图:搜索的本质是什么?
搜索的本质是一个**信息检索(Information Retrieval**问题:给定一个查询,从海量文档中找到最相关的结果,并按相关性排序返回。
这个过程分为两个阶段:
- **索引阶段(离线)**:提前把所有文档处理好,建立高效的查找结构
- **查询阶段(在线)**:用户输入关键词时,快速找到匹配的文档并排序
::: tip 为什么不能用数据库 LIKE 查询?
`SELECT * FROM products WHERE name LIKE '%红色连衣裙%'` 看起来能搜索,但它需要**全表扫描**——逐行检查每条记录。当数据量达到百万级时,这种查询会慢到不可用。倒排索引把这个 O(n) 的操作变成了 O(1) 的查找。
:::
---
## 1. 倒排索引:搜索引擎的"心脏"
传统数据库用的是**正排索引**:从文档 ID 找到文档内容。而搜索引擎用的是**倒排索引**:从关键词找到包含它的文档列表。
<InvertedIndexDemo />
| 索引类型 | 方向 | 查找方式 | 适用场景 |
|---------|------|---------|---------|
| 正排索引 | 文档 → 内容 | 知道 ID,查内容 | 数据库主键查询 |
| 倒排索引 | 关键词 → 文档列表 | 知道关键词,查文档 | 全文搜索 |
::: tip 倒排索引的构建过程
1. **文档收集**:获取所有需要被搜索的文档
2. **分词(Tokenization**:将文档拆分为一个个词语
3. **建立映射**:记录每个词语出现在哪些文档中(以及出现位置、频率等)
4. **持久化存储**:将索引写入磁盘,支持快速查找
:::
---
## 2. 分词与文本分析
分词是搜索引擎的第一步,也是中文搜索的最大挑战。英文天然以空格分词,但中文没有分隔符——"乒乓球拍卖了"可以分成"乒乓球/拍卖/了"或"乒乓/球拍/卖/了"。
| 分词方式 | 说明 | 示例 |
|---------|------|------|
| 标准分词 | 按空格和标点切分(英文) | "hello world" → ["hello", "world"] |
| 中文分词 | 基于词典或模型切分 | "搜索引擎" → ["搜索", "引擎"] |
| N-gram | 按固定长度滑动窗口切分 | "搜索" → ["搜索", "索引"] |
| 自定义词典 | 添加业务专有词汇 | "iPhone16ProMax" 作为一个词 |
::: tip 文本分析管道
分词只是文本分析的一步,完整的管道包括:
1. **字符过滤**:去除 HTML 标签、特殊字符
2. **分词**:将文本拆分为词语(Token
3. **停用词过滤**:去除"的"、"了"、"是"等无意义的高频词
4. **同义词扩展**:将"手机"扩展为"手机、电话、移动电话"
5. **词干提取**:将 "running" 还原为 "run"(英文)
:::
---
## 3. 相关性排序:哪个结果最"相关"?
找到匹配的文档只是第一步,更重要的是**排序**——把最相关的结果排在最前面。
| 算法 | 原理 | 特点 |
|------|------|------|
| TF-IDF | 词频(TF) × 逆文档频率(IDF) | 经典算法,简单有效 |
| BM25 | TF-IDF 的改进版,加入文档长度归一化 | Elasticsearch 默认算法 |
| 向量检索 | 将文档和查询转为向量,计算余弦相似度 | 支持语义搜索 |
::: tip TF-IDF 直觉理解
- **TF(词频)**:一个词在文档中出现越多次,这个文档越可能与该词相关
- **IDF(逆文档频率)**:一个词在越少的文档中出现,它的区分度越高
- "的"在所有文档中都出现(IDF 低),所以搜索"的"没有意义
- "Elasticsearch"只在少数文档中出现(IDF 高),搜索它能精确定位
:::
---
## 4. Elasticsearch:最流行的搜索引擎
Elasticsearch 是目前最流行的开源搜索引擎,基于 Apache Lucene 构建,提供分布式、RESTful API 的全文搜索能力。
| 概念 | 说明 |
|------|------|
| Index | 类似数据库的"表",存储同类文档 |
| Document | 一条记录,JSON 格式 |
| Shard | 分片,将索引拆分到多个节点 |
| Replica | 副本,提供高可用和读扩展 |
| Mapping | 字段类型定义,类似数据库 Schema |
| Analyzer | 文本分析器,定义分词规则 |
::: tip ES vs 数据库
Elasticsearch 不是用来替代数据库的,而是作为搜索层与数据库配合使用。典型架构:数据写入数据库 → 同步到 ES → 搜索请求走 ES → 详情请求走数据库。
:::
---
## 5. 搜索优化:让搜索更"聪明"
| 优化手段 | 说明 | 效果 |
|---------|------|------|
| 同义词 | "手机"也能搜到"电话" | 提高召回率 |
| 拼写纠错 | "iphoen" 自动纠正为 "iphone" | 容错性 |
| 自动补全 | 输入"苹"提示"苹果手机" | 提升体验 |
| 高亮 | 搜索结果中标红匹配词 | 直观展示 |
| 权重调整 | 标题匹配权重 > 内容匹配 | 提高精确度 |
| 过滤与聚合 | 按价格区间、品牌筛选 | 缩小范围 |
---
## 总结
搜索引擎是互联网应用的核心基础设施。理解倒排索引、分词、相关性排序这三个核心概念,就掌握了搜索引擎的本质。
回顾本章的关键要点:
1. **倒排索引**:从关键词到文档的反向映射,是搜索引擎的核心数据结构
2. **分词是基础**:中文分词是搜索质量的关键,需要选择合适的分词器
3. **BM25 排序**:基于词频和文档频率的相关性评分,是 ES 的默认算法
4. **ES 架构**:分片 + 副本实现分布式和高可用
5. **搜索优化**:同义词、纠错、补全让搜索更智能
## 延伸阅读
- [Elasticsearch 官方文档](https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html) - 最权威的 ES 参考
- [Elasticsearch 权威指南](https://www.elastic.co/guide/cn/elasticsearch/guide/current/index.html) - 中文入门指南
- [Apache Lucene](https://lucene.apache.org/) - ES 底层的搜索引擎库
- [MeiliSearch](https://www.meilisearch.com/) - 轻量级搜索引擎,适合中小项目
- [Typesense](https://typesense.org/) - 开源的即时搜索引擎