预计阅读 26 分钟
07 | 存储系统设计 —— 关系型 vs NoSQL、对象存储、时序数据库、搜索引擎选型
选对了存储,系统设计就成功了一半;选错了存储,后面所有的优化都是在给错误的决策打补丁。
开篇自测
- 面对一个新业务场景,你如何在 MySQL、MongoDB、Redis、Elasticsearch 之间做选型决策?
- 对象存储与文件系统的核心区别在哪里?为什么说对象存储是”写一次,读百万次”的最佳选择?
- 时序数据库为什么不用通用关系型数据库来代替?它的存储引擎做了哪些针对性优化?
一、存储选型的架构思维
1.1 从数据特征出发,而非从技术出发
初学者常犯的错误是”我会什么用什么”。正确的思路是先分析数据特征,再匹配存储引擎:
数据特征分析框架
+------------------+------------------+
| 结构化程度 | 访问模式 |
| | |
| 强结构 -> RDBMS | 点查 -> KV 存储 |
| 半结构 -> 文档DB | 范围 -> B+Tree |
| 非结构 -> 对象存储 | 全文 -> 倒排索引 |
| | 时序 -> LSM-Tree |
+------------------+------------------+
| 一致性要求 | 数据规模 |
| | |
| 强一致 -> RDBMS | < 1TB -> 单机 |
| 最终一致 -> NoSQL | 1~50TB -> 分片 |
| 无要求 -> 缓存 | > 50TB -> 分布式 |
+------------------+------------------+
1.2 存储系统全景图
+----------------------------------------------------------------+
| 存储系统选型全景 |
+----------------------------------------------------------------+
| 结构化数据 MySQL / PostgreSQL / TiDB |
| 适用:事务、复杂查询、强一致性 |
| |
| 半结构化数据 MongoDB / DynamoDB |
| 适用:灵活 Schema、文档嵌套、快速迭代 |
| |
| 键值数据 Redis / Memcached / etcd |
| 适用:缓存、会话、配置、分布式锁 |
| |
| 宽列数据 HBase / Cassandra / ScyllaDB |
| 适用:海量数据、高写入、稀疏列 |
| |
| 时序数据 InfluxDB / TimescaleDB / Prometheus |
| 适用:监控指标、IoT 传感器、金融行情 |
| |
| 搜索数据 Elasticsearch / Solr / Meilisearch |
| 适用:全文检索、日志分析、聚合统计 |
| |
| 非结构化数据 S3 / OSS / MinIO / Ceph |
| 适用:图片、视频、文档、备份 |
+----------------------------------------------------------------+
二、关系型数据库深度剖析
2.1 MySQL 与 PostgreSQL 的架构选型
| 维度 | MySQL (InnoDB) | PostgreSQL |
|---|---|---|
| 存储引擎 | B+ Tree(聚簇索引) | Heap Table + B-Tree 索引 |
| MVCC 实现 | Undo Log 回滚段 | UPDATE 标记旧行 dead 并插入新行,需 VACUUM 回收 dead tuples,否则表膨胀 |
| 复制方案 | 半同步/组复制 | 流复制 + 逻辑复制 |
| JSON 支持 | 基础支持 | 原生 JSONB,支持索引 |
| 适用场景 | 互联网 OLTP | 复杂查询、地理信息、分析 |
2.2 何时突破单机关系型数据库
当你遇到以下信号时,就该考虑分布式方案了:
信号 1: 单表数据量 > 5000 万行
-> 方案: 分库分表 或 NewSQL (TiDB / CockroachDB)
信号 2: 写入 QPS > 1 万
-> 方案: 读写分离 + 分片
信号 3: 数据总量 > 1TB
-> 方案: 水平拆分,每个分片控制在 200GB 以内
import hashlib
# 分片键选择示例 —— 以物流跟踪系统为例
# 错误: 按 create_time 分片 -> 热点集中在最新分片
# 正确: 按 shipment_id 哈希分片 -> 写入均匀分散
def resolve_shard(shipment_id: str, total_shards: int = 16) -> int:
"""根据运单号计算分片编号
注意:必须使用 hashlib 等确定性哈希,不能用内置 hash()——
Python 3.3+ 默认启用哈希随机化(PYTHONHASHSEED),
同一字符串在不同进程中 hash() 返回值不同,会导致路由不一致。"""
hash_val = int(hashlib.md5(shipment_id.encode()).hexdigest(), 16) & 0x7FFFFFFF
return hash_val % total_shards
shard_index = resolve_shard("SF2025031800042")
db_name = f"logistics_db_{shard_index:02d}"
print(f"路由到 {db_name}.tracking_{shard_index:02d}")
三、NoSQL 存储选型
3.1 文档数据库:MongoDB
MongoDB 的核心优势是灵活的 Schema 设计,在业务快速迭代阶段尤为适用:
适合 MongoDB: 不适合 MongoDB:
[✓] 数据结构经常变化 [✗] 频繁的多表 JOIN
[✓] 嵌套文档 (商品 SKU) [✗] 复杂事务 (转账/库存)
[✓] 读多写少,查询模式固定 [✗] 严格数据完整性约束
// 嵌入式设计 —— 旅行行程管理 (子文档与父文档生命周期绑定)
{
_id: ObjectId("..."),
traveler: "赵云",
trip_name: "西南自驾",
segments: [
{ city: "成都", days: 3, budget: 2500, highlights: ["锦里", "武侯祠"] },
{ city: "大理", days: 2, budget: 1800, highlights: ["洱海", "古城"] }
]
}
// 引用式设计 —— 独立评价 (子文档需独立查询和更新)
{ _id: ObjectId("..."), trip_id: ObjectId("..."),
reviewer: "张飞", rating: 4.5, comment: "行程安排紧凑但不赶" }
3.2 宽列数据库:Cassandra / HBase
宽列数据库底层基于 LSM-Tree,写入只做追加,性能极高:
LSM-Tree 写入流程:
写请求 ──> MemTable (内存)
| (满了)
v
Flush 到磁盘 ──> SSTable 文件
| (过多)
v
后台 Compaction 合并
优势: 顺序写,吞吐 10万+ TPS
劣势: 读取需合并多个 SSTable,延迟较高
| 维度 | Cassandra | HBase |
|---|---|---|
| 架构 | 去中心化 P2P | 主从(依赖 ZooKeeper) |
| 一致性 | 可调(ONE/QUORUM/ALL) | 强一致性 |
| 运维 | 简单,无单点 | 复杂,依赖 Hadoop 生态 |
| 适用 | 写多读少、跨数据中心 | Hadoop 生态内的实时查询 |
四、对象存储设计
4.1 对象存储的本质
对象存储把数据当作不可变的对象管理,每个对象由 Key、Data、Metadata 三部分组成:
文件系统 (POSIX): 对象存储 (S3-like):
/home/videos/ Bucket: media-assets
├── stage-01.mp4 ├── concert/stage-01.mp4
└── stage-02.mp4 └── interview/guest-wang.mp4
- 树形目录,支持随机读写 - 扁平 KV,整体读写不支持修改
- 单机或网络文件系统 - 天然分布式
- 适合小文件、频繁修改 - 适合大文件、一次写入多次读取
4.2 大文件分片上传
import hashlib
from dataclasses import dataclass
CHUNK_SIZE = 8 * 1024 * 1024 # 8MB 分片
@dataclass
class ChunkMeta:
chunk_index: int
etag: str
uploaded: bool = False
def prepare_multipart_upload(file_path: str) -> list[ChunkMeta]:
"""将文件拆分为多个分片,计算每个分片的指纹"""
chunks = []
with open(file_path, "rb") as f:
index = 0
while block := f.read(CHUNK_SIZE):
etag = hashlib.md5(block).hexdigest()
chunks.append(ChunkMeta(chunk_index=index, etag=etag))
index += 1
return chunks
def resume_upload(chunks: list[ChunkMeta]) -> list[ChunkMeta]:
"""断点续传: 跳过已上传的分片"""
return [c for c in chunks if not c.uploaded]
五、时序数据库
5.1 时序数据的独特性
特征 1: 写入密集 + 时间递增
传感器每秒上报温度 -> 写入量巨大且总是追加
特征 2: 近期高频访问,历史低频访问
最近 1 小时数据查看 1000 次/天,3 个月前数据 1 次/月
特征 3: 查询模式固定
总是 "时间范围 + 标签 + 聚合"
SELECT avg(cpu_usage) WHERE host='web-07' AND time > now()-1h
5.2 时序数据库的存储优化
优化 1: 按时间分块 (Time-based Sharding)
+----------+----------+----------+----------+
| Block | Block | Block | Block |
| 03/15 | 03/16 | 03/17 | 03/18 |
+----------+----------+----------+----------+
冷数据 ──────────────────────> 热数据
可压缩归档 内存中加速
优化 2: 列式存储 + 高压缩
行存: (时间, 主机, CPU, 内存) 每行一条完整记录
列存: CPU列: [45, 47, 46, ...] 同类数据紧邻,delta/RLE 压缩
优化 3: 自动降采样 (Downsampling)
原始(每秒) -> 保留 7 天 | 5分钟均值 -> 30 天 | 1小时均值 -> 1 年
| 维度 | InfluxDB | TimescaleDB | Prometheus |
|---|---|---|---|
| 底层 | 自研 TSM | PostgreSQL 扩展 | 自研 TSDB |
| 查询语言 | Flux / InfluxQL | SQL | PromQL |
| SQL 兼容 | 否 | 完全兼容 | 否 |
| 适用 | IoT、独立监控 | 已有 PG 生态 | K8s 监控 |
六、搜索引擎选型
6.1 倒排索引原理
原始文档:
Doc1: "分布式消息队列的设计与实践"
Doc2: "消息队列在电商系统中的应用"
Doc3: "分布式系统的一致性设计"
倒排索引 (Term -> Posting List):
分布式 -> [Doc1, Doc3]
消息队列 -> [Doc1, Doc2]
设计 -> [Doc1, Doc3]
系统 -> [Doc2, Doc3]
查询 "分布式 设计":
分布式 ∩ 设计 = [Doc1, Doc3] -> 命中两篇文档
6.2 Elasticsearch 架构与常见陷阱
ES 集群架构:
客户端 -> 协调节点 -> 数据节点 1/2/3 (分片 + 副本)
四大陷阱:
1. 把 ES 当主数据库 -> 不支持事务,数据丢失风险高
正确: MySQL 主库 + ES 查询副本
2. Mapping 爆炸 -> 动态字段无限增长,性能骤降
正确: 关闭 dynamic mapping,严格定义 Schema
3. 深度分页 -> from=10000 每个分片取 10020 条再合并
正确: 使用 search_after + Point-in-Time (PIT) API
4. 频繁更新 -> 实质是"删旧写新",大量 segment merge
正确: 批量更新,控制更新频率
七、存储选型实战决策
7.1 综合选型决策树
你的数据需要什么?
|
+------------+------------+
| |
需要事务? 不需要事务?
| |
+---+---+ +------+------+
| | | |
复杂查询 简单KV 结构固定? 结构灵活?
| | | |
RDBMS Redis +--+--+ MongoDB
(MySQL/PG) | |
时序? 全文搜索?
| |
InfluxDB ES
7.2 混合存储架构示例
以智能停车场管理系统为例:
+---------------------------------------------------------------+
| 智能停车场管理系统存储架构 |
+---------------------------------------------------------------+
| MySQL 车位信息、用户账户、订单 |
| (事务: 入场扣费 + 车位状态变更) |
| |
| Redis 车位实时状态缓存、限流计数 |
| (高性能: 入口闸机毫秒级响应) |
| |
| MongoDB 车辆识别日志 (OCR 结果 + 原始参数) |
| (灵活 Schema: 不同摄像头返回不同字段) |
| |
| InfluxDB 车流量统计、设备温度监控 |
| |
| Elasticsearch 历史停车记录搜索 (车牌/时间/区域) |
| |
| MinIO (S3) 车牌照片、监控录像、发票 PDF |
+---------------------------------------------------------------+
思考题
-
假设你正在设计一个”全国天气预报系统”,需要存储来自 5 万个气象站每 10 秒一次的温度、湿度、风速数据,同时支持用户按城市查询最近 7 天趋势。你会怎么设计存储方案?
-
有人说”Elasticsearch 可以替代 MySQL 做主存储”,你怎么看?请从数据安全性、一致性、查询能力三个角度分析。
结尾自测
-
选择存储引擎时,最核心的四个数据特征维度是什么?
- 答:结构化程度、访问模式、一致性要求、数据规模。
-
MongoDB 的嵌入式文档和引用式文档分别适用于什么场景?
- 答:嵌入式适用于子文档与父文档生命周期绑定、总是一起查询的场景。引用式适用于子文档独立增长、需要独立查询和更新的场景。
-
时序数据库相比通用关系型数据库做了哪三项核心优化?
- 答:按时间分块存储(冷热分离)、列式存储加高压缩比(delta/RLE 编码)、自动降采样(原始数据短期保留,聚合数据长期保留)。
-
使用 Elasticsearch 时,为什么深度分页会造成性能问题?
- 答:from+size 分页需要每个分片都返回 from+size 条数据到协调节点合并排序,深度分页时数据量巨大。替代方案是 search_after 配合 Point-in-Time (PIT) API。
-
在混合存储架构中,如何保证 MySQL 与 Elasticsearch 之间的数据一致性?
- 答:推荐监听 MySQL Binlog 通过 Canal/Debezium 同步到 ES,延迟秒级。也可双写(一致性弱)或定时全量同步(延迟高)。
下一章预告:理论终归要落地。下一章我们将进入实战篇,用短链服务、Feed 流、即时通讯、抢红包四个经典案例,手把手带你完成从零到一的系统设计。
购买课程解锁全部内容
面试晋升必学:11 章掌握系统设计
¥29.90