请求与存储最优实践#
一、关于请求方式#
1. POST vs PUT 特性对比#
| 特性 |
POST |
PUT |
| 语义 |
创建子资源,或提交数据供处理 |
替换整个目标资源(幂等) |
| 幂等性 |
❌ 不保证幂等(多次请求可能创建多个资源) |
✅ 幂等(多次请求结果相同) |
| URI |
通常由服务器决定最终 URI(如 /posts → /posts/123) |
客户端明确指定完整 URI(如 /posts/123) |
| 常见用途 |
创建新资源、表单提交、部分更新(PATCH 之前常用) |
全量更新已知资源、创建资源(若客户端指定 ID) |
2. 示例对比#
| 操作 |
使用 POST |
使用 PUT |
| 创建新文章 |
✅ 常见做法 |
也可以(客户端需自己生成 ID,如 /articles/uuid-123) |
| 全量更新文章 123 |
❌ 可以但不推荐(不幂等) |
✅ 标准做法 |
| 提交表单(购物车、联系表单) |
✅ |
❌ |
| 上传大文件并覆盖已有文件 |
❌(用 PUT) |
✅ |
3. 误区与补充#
- POST 也能更新:REST 没有禁止 POST 做更新,但 PUT/PATCH 更语义明确
- 部分更新:标准做法是用 PATCH,因为 PUT 要求全量替换(缺失字段会被删除)
- PUT 创建资源:允许,但条件是客户端能提供唯一且最终不变的 URI
- 部分 API 实践时,只开放 POST + PATCH,忽略 PUT
- 幂等性对网络重试很重要(PUT 安全,POST 不安全)
快速判断#
需要客户端指定完整 URI + 幂等(可安全重试)+ 全量替换 → PUT
URI 由服务器决定 + 可能改变状态/创建资源 → POST
部分更新且幂等 → PATCH
二、后端存储#
1. 核心原则#
| 原则 |
说明 |
| 数据安全优先 |
丢失比慢更致命 |
| 按需选择 |
没有万能存储,只有合适场景 |
| 分层存储 |
热/温/冷数据分离 |
| 不可变原则 |
能追加不修改,能软删不硬删 |
| 可观测性 |
慢查询、连接池、容量必须可监控 |
2. 数据库选型决策#
数据关系复杂?联查多?
├─ 是 → 关系型数据库(PostgreSQL / MySQL)
│ ├─ 强一致性 + 复杂事务 → PostgreSQL
│ └─ 读多写少 + 生态成熟 → MySQL
数据量大(TB级)?写入压力大?
├─ 是 → 分布式数据库
│ ├─ 强事务 + 水平扩展 → TiDB / CockroachDB
│ └─ 最终一致性 + 超高吞吐 → Cassandra / HBase
数据结构多变?无 schema?
├─ 是 → 文档数据库(MongoDB)
│ └─ 适合:日志、用户画像、CMS
需要全文搜索?
├─ 是 → Elasticsearch(配合主存储双写)
需要缓存 + 高性能?
├─ 是 → Redis
│ └─ 适合:会话、计数器、排行榜、分布式锁
需要图关系(社交、推荐)?
├─ 是 → 图数据库(Neo4j / Amazon Neptune)
时间序列数据(IoT、监控)?
├─ 是 → 时序数据库(InfluxDB / TimescaleDB / Prometheus)
3. 分层存储架构#
请求路径
│
▼
┌──────────────────────────────────────────────────────┐
│ L1: 缓存层 │
│ Redis / Memcached │
│ - TTL 5-15分钟 │
│ - 缓存穿透/击穿/雪崩防护 │
└──────────────────────────────────────────────────────┘
│ 未命中
▼
┌──────────────────────────────────────────────────────┐
│ L2: 热存储 │
│ PostgreSQL / MySQL(SSD,主从集群) │
│ - 最近 7-30 天数据 │
│ - 在线服务直接访问 │
└──────────────────────────────────────────────────────┘
│ 归档策略
▼
┌──────────────────────────────────────────────────────┐
│ L3: 温存储 │
│ 归档表 / 分区表 / 对象存储(OSS/S3) │
│ - 30 天 ~ 1 年数据 │
│ - 可离线分析 │
└──────────────────────────────────────────────────────┘
│ 长期保存
▼
┌──────────────────────────────────────────────────────┐
│ L4: 冷存储 │
│ 对象存储(Glacier Deep Archive) │
│ - 1 年以上数据 │
│ - 取回时间 12-48 小时 │
└──────────────────────────────────────────────────────┘
4. 关系型最优实践#
表设计#
-- ✅ 推荐:软删除 + 审计字段
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
status SMALLINT DEFAULT 1, -- 1:正常 2:禁用 3:已删
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ, -- NULL = 未删除
version INT DEFAULT 1 -- 乐观锁
);
-- ✅ 使用 UUID 作为业务 ID(防遍历/分库兼容)
-- ❌ 避免用自增 ID 暴露业务量
索引策略#
| 场景 |
建议 |
等值查询(WHERE email = ?) |
B-Tree 索引 |
范围查询(WHERE created_at > ?) |
B-Tree 索引 + 排序方向一致 |
模糊查询(LIKE 'abc%') |
普通索引(前置通配符 %abc 用不到索引) |
| 全文搜索 |
倒排索引(PostgreSQL tsvector / Elasticsearch) |
| 多条件排序 |
联合索引(覆盖排序字段) |
| JSON 字段查询 |
GIN 索引(PostgreSQL) |
索引黄金法则:
- 每个表 ≤ 5 个索引
- 重复/冗余索引果断删除
- 用
EXPLAIN 验证执行计划
- 监控索引命中率(未使用索引 > 30 天 → 删除)
防慢查询#
-- 设置语句超时
SET statement_timeout = '5s';
-- ❌ 避免
SELECT * FROM orders; -- 全表扫描
SELECT COUNT(*) FROM huge_table; -- 大数据量计数
WHERE name LIKE '%keyword%'; -- 左通配符
WHERE YEAR(created_at) = 2025; -- 函数破坏索引
-- ✅ 推荐
SELECT id, email, status FROM orders LIMIT 100; -- 限制字段 + 分页
WHERE created_at BETWEEN '2025-01-01' AND '2025-12-31';
5. Redis 缓存最优实践#
数据结构选择#
| 业务场景 |
推荐结构 |
示例 |
| 缓存单值 |
String |
user:123 |
| 对象缓存 |
Hash |
user:123 多个字段 |
| 排行榜 |
Sorted Set |
积分榜 |
| 去重集合 |
Set |
今日访问 IP |
| 消息队列 |
Stream / List |
轻量级队列 |
| 计数器 |
String + INCR |
PV/UV |
| 分布式锁 |
String + SET NX EX |
Redlock |
缓存三大问题(穿透、击穿、雪崩)#
# 1. 缓存穿透(查不存在的数据)
# 解决:布隆过滤器 或 缓存空值(TTL 短)
if not bloom_filter.might_contain(key):
return None
value = cache.get(key)
if value == "NULL":
return None
# 2. 缓存击穿(热点 key 失效)
# 解决:互斥锁 / 逻辑过期
value = cache.get(key)
if value is None:
with redis_lock(f"lock:{key}"):
value = db.query()
cache.set(key, value, ttl=300)
# 3. 缓存雪崩(大量 key 同时失效)
# 解决:TTL 加随机值
ttl = 3600 + random.randint(0, 300)
内存控制#
# 设置最大内存
maxmemory 4gb
# 淘汰策略(选其一)
maxmemory-policy allkeys-lru # 最常用
# volatile-lru # 仅对有过期时间的 key
# allkeys-random # 不推荐
# noeviction # 内存满时拒绝写入
6. 写入与事务#
事务边界#
# ✅ 推荐:短事务
@transaction.atomic
def create_order(user_id, items):
# 1. 创建订单(1条 INSERT)
order = Order.objects.create(user_id=user_id)
# 2. 批量插入订单项(N条 INSERT)
OrderItem.objects.bulk_create(items)
# 3. 扣减库存(1条 UPDATE)
# 事务总耗时 < 100ms
# ❌ 避免:长事务
@transaction.atomic
def bad_operation():
# 1. 调用外部 API(可能 5 秒)
# 2. 发送邮件(可能 2 秒)
# 3. 更新数据库(1 条 UPDATE)
# → 持有锁 7 秒,阻塞其他请求
写入优化#
| 场景 |
方案 |
| 批量写入 |
bulk_insert 分批(每批 500-1000 条) |
| 高并发扣减 |
Redis 计数 + 异步同步 DB |
| 日志/埋点 |
先写本地队列,异步批量写入 |
| 避免锁竞争 |
乐观锁(version 字段)优于悲观锁 |
| 大字段更新 |
分离到独立表(如 JSON 配置) |
7. 高可用与备份#
┌─────────────┐
│ 负载均衡 │
└──────┬──────┘
│
┌─────────────┼─────────────┐
│ │ │
┌───▼───┐ ┌───▼───┐ ┌───▼───┐
│ Master│ ◄─► │ Slave │ │Slave │ ← 读写分离
└───┬───┘ └───────┘ └───────┘
│
│ 异步复制 / 半同步
▼
┌───────────┐
│ Backup │ ← 每天全量 + WAL 归档
└───────────┘
备份策略#
| 备份类型 |
频率 |
保留期 |
恢复时间目标 |
| 全量备份 |
每天 1 次 |
30 天 |
2-4 小时 |
| 增量/WAL |
每 15-30 分钟 |
7 天 |
30 分钟 |
| 逻辑导出 |
每周 |
90 天 |
取决于大小 |
⚠️ 验收备份:每月至少一次恢复演练
监控指标#
| 维度 |
关键指标 |
告警阈值 |
| 连接数 |
活跃连接 / 最大连接 |
> 80% |
| 慢查询 |
慢查询数量/QPS |
每分钟 > 10 条 |
| 磁盘 |
使用率 |
> 80% |
| 复制延迟 |
主从延迟秒数 |
> 10 秒 |
| 缓存命中率 |
命中数 / 总请求 |
< 90%(业务相关) |
| 事务 |
平均时长 |
> 500ms |
8. 常见反模式#
| 反模式 |
正确做法 |
| 存储 JSON 大字段并频繁更新 |
拆表或用文档数据库 |
| 把所有存储放一个数据库 |
读写分离 + 分库分表 |
| 不用连接池(每次新建连接) |
连接池复用 |
| 索引滥加(10+ 索引/表) |
定期清理未使用索引 |
SELECT * |
只取需要的字段 |
| 在循环中查数据库(N+1 问题) |
批量查询或预加载 |
| 依赖自增 ID 做业务逻辑 |
用 UUID/雪花 ID |
| 生产环境直接修改表结构 |
在线 DDL 工具(gh-ost/pt-osc) |
9. 决策速查#
| 需求 |
方案 |
| 需要强 ACID 事务? |
→ PostgreSQL / MySQL |
| 需要极高读写(10w+ QPS)? |
→ Redis(缓存)+ 分库分表 |
| 数据量 > 1TB? |
→ 分布式(TiDB / Cassandra) |
| 查询模式不可预知? |
→ MongoDB |
| 需要全文搜索? |
→ Elasticsearch + 主存储 |
| 只有几个简单键值对? |
→ Redis 就够了 |
| 日志/监控指标? |
→ 时序数据库 + 对象存储归档 |