跳到主要内容
预计阅读 26 分钟

07 | 存储系统设计 —— 关系型 vs NoSQL、对象存储、时序数据库、搜索引擎选型

选对了存储,系统设计就成功了一半;选错了存储,后面所有的优化都是在给错误的决策打补丁。


开篇自测

  1. 面对一个新业务场景,你如何在 MySQL、MongoDB、Redis、Elasticsearch 之间做选型决策?
  2. 对象存储与文件系统的核心区别在哪里?为什么说对象存储是”写一次,读百万次”的最佳选择?
  3. 时序数据库为什么不用通用关系型数据库来代替?它的存储引擎做了哪些针对性优化?

一、存储选型的架构思维

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,延迟较高
维度CassandraHBase
架构去中心化 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 年
维度InfluxDBTimescaleDBPrometheus
底层自研 TSMPostgreSQL 扩展自研 TSDB
查询语言Flux / InfluxQLSQLPromQL
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                     |
+---------------------------------------------------------------+

思考题

  1. 假设你正在设计一个”全国天气预报系统”,需要存储来自 5 万个气象站每 10 秒一次的温度、湿度、风速数据,同时支持用户按城市查询最近 7 天趋势。你会怎么设计存储方案?

  2. 有人说”Elasticsearch 可以替代 MySQL 做主存储”,你怎么看?请从数据安全性、一致性、查询能力三个角度分析。


结尾自测

  1. 选择存储引擎时,最核心的四个数据特征维度是什么?

    • :结构化程度、访问模式、一致性要求、数据规模。
  2. MongoDB 的嵌入式文档和引用式文档分别适用于什么场景?

    • :嵌入式适用于子文档与父文档生命周期绑定、总是一起查询的场景。引用式适用于子文档独立增长、需要独立查询和更新的场景。
  3. 时序数据库相比通用关系型数据库做了哪三项核心优化?

    • :按时间分块存储(冷热分离)、列式存储加高压缩比(delta/RLE 编码)、自动降采样(原始数据短期保留,聚合数据长期保留)。
  4. 使用 Elasticsearch 时,为什么深度分页会造成性能问题?

    • :from+size 分页需要每个分片都返回 from+size 条数据到协调节点合并排序,深度分页时数据量巨大。替代方案是 search_after 配合 Point-in-Time (PIT) API。
  5. 在混合存储架构中,如何保证 MySQL 与 Elasticsearch 之间的数据一致性?

    • :推荐监听 MySQL Binlog 通过 Canal/Debezium 同步到 ES,延迟秒级。也可双写(一致性弱)或定时全量同步(延迟高)。

下一章预告:理论终归要落地。下一章我们将进入实战篇,用短链服务、Feed 流、即时通讯、抢红包四个经典案例,手把手带你完成从零到一的系统设计。

购买课程解锁全部内容

面试晋升必学:11 章掌握系统设计

¥29.90