数据库选型深入 — 从原理到决策

目录

  • [一、两大阵营:OLTP vs OLAP](#一、两大阵营:OLTP vs OLAP "#%E4%B8%80%E4%B8%A4%E5%A4%A7%E9%98%B5%E8%90%A5oltp-vs-olap")
  • [二、行式存储 vs 列式存储(核心原理)](#二、行式存储 vs 列式存储(核心原理) "#%E4%BA%8C%E8%A1%8C%E5%BC%8F%E5%AD%98%E5%82%A8-vs-%E5%88%97%E5%BC%8F%E5%AD%98%E5%82%A8%E6%A0%B8%E5%BF%83%E5%8E%9F%E7%90%86")
  • 三、选型决策树(完整版)
  • 四、主流数据库深入拆解
    • [4.1 MySQL](#4.1 MySQL "#41-mysql")
    • [4.2 PostgreSQL](#4.2 PostgreSQL "#42-postgresql")
    • [4.3 Doris / StarRocks](#4.3 Doris / StarRocks "#43-doris--starrocks")
    • [4.4 ClickHouse](#4.4 ClickHouse "#44-clickhouse")
    • [4.5 Elasticsearch](#4.5 Elasticsearch "#45-elasticsearch")
    • [4.6 MongoDB](#4.6 MongoDB "#46-mongodb")
    • [4.7 Redis](#4.7 Redis "#47-redis")
    • [4.8 HBase / Cassandra](#4.8 HBase / Cassandra "#48-hbase--cassandra")
  • 五、组合使用模式
  • 六、你项目中的选型复盘
  • 七、面试高频问题

一、两大阵营:OLTP vs OLAP

所有数据库归根结底服务于两种工作负载:

维度 OLTP(事务处理) OLAP(分析处理)
核心操作 INSERT / UPDATE / DELETE 单行 SELECT ... GROUP BY ... 聚合百万行
单次数据量 几行 几百万~几亿行
响应时间 毫秒级(用户在等) 秒级可接受(看报表不急)
事务 必须(ACID) 不需要或弱事务
写入模式 高频小写入(一笔订单、一次点击) 低频批量写入(ETL、Stream Load)
存储方式 行式存储 列式存储
并发模式 高并发短查询(QPS 几千~几万) 低并发长查询(几个分析师同时查)
代表 MySQL、PostgreSQL Doris、ClickHouse、BigQuery

一句话判断: 你的核心操作是"增删改一条记录"还是"统计分析一大批数据"?前者 OLTP,后者 OLAP。

现实中大部分系统两者都需要: 业务操作用 OLTP,数据分析用 OLAP,中间通过 MQ 或 ETL 同步数据。


二、行式存储 vs 列式存储(核心原理)

这是理解 OLTP 和 OLAP 性能差异的关键,面试必考。

数据示例

假设有一张 1000 万行的用户行为表:

yaml 复制代码
| user_id | action  | amount | city   | timestamp           |
|---------|---------|--------|--------|---------------------|
| 1       | buy     | 99.9   | 北京   | 2024-01-01 10:00:00 |
| 2       | click   | 0      | 上海   | 2024-01-01 10:00:01 |
| 3       | buy     | 199.0  | 北京   | 2024-01-01 10:00:02 |
| ...     | ...     | ...    | ...    | ...                 |

行式存储(MySQL)

磁盘上的物理排列:

csharp 复制代码
[1, buy, 99.9, 北京, 2024-01-01 10:00:00]
[2, click, 0, 上海, 2024-01-01 10:00:01]
[3, buy, 199.0, 北京, 2024-01-01 10:00:02]
...

查单条记录 : SELECT * FROM t WHERE user_id = 1

  • 通过索引定位到这一行,一次 IO 读出所有字段 → 快

聚合查询 : SELECT city, SUM(amount) FROM t GROUP BY city

  • 需要读 1000 万行的所有字段(user_id、action、amount、city、timestamp)
  • 但实际只需要 city 和 amount 两列
  • 80% 的 IO 浪费在读不需要的列上 → 慢

列式存储(Doris / ClickHouse)

磁盘上的物理排列:

ini 复制代码
user_id 列:   [1, 2, 3, ...]           → 连续存放
action 列:    [buy, click, buy, ...]    → 连续存放
amount 列:    [99.9, 0, 199.0, ...]     → 连续存放
city 列:      [北京, 上海, 北京, ...]    → 连续存放
timestamp 列: [2024-01-01..., ...]      → 连续存放

聚合查询 : SELECT city, SUM(amount) FROM t GROUP BY city

  • 只需要读 city 列和 amount 列
  • 跳过 user_id、action、timestamp 三列
  • IO 量减少 60% → 快 10~100 倍

查单条记录 : SELECT * FROM t WHERE user_id = 1

  • 需要从 5 个不同位置分别读出这一行的 5 个字段
  • 5 次随机 IO → 比行式存储慢

列式存储的额外优势:压缩

同一列的数据类型相同、值域相似,压缩效果极好:

ini 复制代码
city 列: [北京, 上海, 北京, 北京, 上海, 北京, ...]
→ 字典编码: 北京=0, 上海=1 → [0, 1, 0, 0, 1, 0, ...]
→ 再用 RLE 压缩: [0×3, 1×1, 0×2, ...]

100GB 的原始数据,列式存储压缩后可能只占 15~25GB。

总结

操作 行式存储(MySQL) 列式存储(Doris)
查单条记录 快(一次 IO) 慢(多次随机 IO)
聚合分析 慢(读大量无用列) 快(只读需要的列)
写入单行 快(追加一行) 慢(要写多个列文件)
批量写入 一般 快(列批量追加)
压缩率 一般(混合类型) 高(同类型压缩)
存储成本

三、选型决策树(完整版)

第一层:数据的核心操作是什么?

css 复制代码
你的系统核心操作是什么?
│
├─ A. 增删改查单条记录(用户注册、下单、更新资料)
│   → 关系型数据库(OLTP)→ 进入决策树 A
│
├─ B. 大批量数据聚合分析(报表、统计、多维对比)
│   → 分析型数据库(OLAP)→ 进入决策树 B
│
├─ C. 全文搜索(商品搜索、日志检索、模糊匹配)
│   → 搜索引擎 → 进入决策树 C
│
├─ D. 高频简单读写(缓存、计数器、排行榜、分布式锁)
│   → 内存数据库 → Redis
│
├─ E. 存文件(图片、视频、ZIP、日志文件)
│   → 对象存储(S3/FDS/OSS)
│
└─ F. 以上都有
    → 组合使用(见第五节)

决策树 A:关系型数据库选哪个?

javascript 复制代码
需要关系型数据库
│
├─ 公司技术栈是 MySQL,DBA 团队熟悉 MySQL
│   └─ MySQL(生态和运维支持是最大优势)
│
├─ 需要 JSONB 灵活字段(自定义表单、CMS)
│   └─ PostgreSQL
│
├─ 需要内置全文搜索(数据量不大,不想引入 ES)
│   └─ PostgreSQL
│
├─ 需要复杂分析查询(窗口函数、CTE、递归)
│   └─ PostgreSQL
│
├─ 简单 CRUD,没有特殊需求
│   └─ MySQL(最通用,招人容易,资料多)
│
└─ 数据结构经常变化,不想每次改表结构
    ├─ 变化频繁但仍有关系 → PostgreSQL(JSONB)
    └─ 完全无固定结构 → MongoDB

决策树 B:分析型数据库选哪个?

markdown 复制代码
需要 OLAP 分析数据库
│
├─ 需要多表 JOIN + 兼容 MySQL 协议
│   └─ Doris / StarRocks
│       优势: JdbcTemplate 直接用,学习成本低
│       适用: 业务分析、多维报表
│
├─ 单表查询为主,追求极致性能
│   └─ ClickHouse
│       优势: 单表聚合查询最快
│       适用: 日志分析、时序数据、监控指标
│
├─ 数据在 Hadoop/S3 上,不想搬数据
│   └─ Presto / Trino
│       优势: 联邦查询,直接查多个数据源
│       适用: 数据湖场景
│
└─ 云上托管,不想运维
    └─ BigQuery(GCP)/ Redshift(AWS)/ Serverless Doris

决策树 C:搜索场景怎么选?

sql 复制代码
需要搜索能力
│
├─ 简单的精确匹配或前缀匹配
│   └─ MySQL / PostgreSQL 加索引就够了
│       例: WHERE name = '张三' 或 WHERE name LIKE '张%'
│
├─ 中文分词搜索,数据量 < 几百万
│   └─ PostgreSQL 全文搜索(不需要引入 ES)
│
├─ 中文分词搜索,数据量 > 几百万,需要搜索建议/纠错/同义词
│   └─ Elasticsearch
│       注意: ES 不做主存储,MySQL 做主存储,Canal 同步到 ES
│
└─ 日志检索(GB~TB 级日志)
    └─ Elasticsearch(ELK 栈)或 ClickHouse

决策树 D:缓存/高频读写怎么选?

vbnet 复制代码
需要高频读写
│
├─ KV 缓存(查询结果缓存、会话存储)
│   └─ Redis String / Hash
│
├─ 计数器(点赞数、浏览量)
│   └─ Redis INCR(原子操作)
│
├─ 排行榜(Top N)
│   └─ Redis Sorted Set(ZADD + ZREVRANGE)
│
├─ 分布式锁(防止重复处理)
│   └─ Redis SET NX + TTL
│
├─ 消息队列(简单场景)
│   └─ Redis List(LPUSH + BRPOP)
│       注意: 复杂场景用专业 MQ(Kafka/RabbitMQ)
│
├─ 集合操作(共同好友、标签交集)
│   └─ Redis Set(SINTER)
│
└─ 需要持久化 + 复杂查询
    └─ 不要用 Redis 做主存储,用 MySQL + Redis 缓存

四、主流数据库深入拆解

4.1 MySQL

定位: 最通用的关系型数据库,大部分场景的默认选择。

核心架构:

css 复制代码
客户端 → 连接池 → SQL 解析器 → 查询优化器 → 执行引擎 → 存储引擎(InnoDB)
                                                              ↓
                                                         磁盘(B+ 树)

InnoDB 存储引擎的关键特性:

  • B+ 树索引:所有数据按主键组织成 B+ 树,叶子节点存完整行数据(聚簇索引)
  • 事务支持:ACID,通过 undo log(回滚)和 redo log(崩溃恢复)实现
  • MVCC:多版本并发控制,读不阻塞写,写不阻塞读
  • 行级锁:并发更新不同行互不影响

索引原理(面试必考):

less 复制代码
B+ 树索引(以 user_id 为例):

         [50]                    ← 根节点
        /    \
    [20,30]  [70,80]             ← 中间节点
    / | \    / | \
  [10,15] [25,28] [55,60] ...   ← 叶子节点(存实际数据,叶子间有链表连接)
  • 查找 user_id=25:根 → 左子树 → 中间叶子,3 次 IO
  • 范围查询 user_id BETWEEN 20 AND 60:定位到 20,沿叶子链表扫描到 60
  • 没有索引的查询:全表扫描,1000 万行逐行比较 → 极慢

什么时候加索引:

  • WHERE 条件里经常出现的字段
  • JOIN 的关联字段
  • ORDER BY / GROUP BY 的字段

什么时候不该加索引:

  • 写多读少的表(索引会减慢写入)
  • 区分度低的字段(性别只有男/女,索引没意义)
  • 很少查询的字段

MySQL 的扩展方案:

方案 解决什么问题 怎么做
读写分离 读 QPS 太高 主库写,从库读,通过 binlog 同步
分库分表 单表数据量太大(> 2000 万行) 按 userId 取模分到多张表
连接池 连接数不够 HikariCP / Druid,复用连接
慢查询优化 查询太慢 EXPLAIN 分析执行计划,加索引

MySQL 的天花板:

  • 单表 > 2000 万行:查询开始变慢,考虑分表
  • 单实例 QPS > 1 万:考虑读写分离
  • 复杂聚合查询:不适合,用 OLAP 数据库
  • 全文搜索:LIKE '%keyword%' 不走索引,用 ES

4.2 PostgreSQL

定位: 功能最强的开源关系型数据库。MySQL 能做的它都能做,还多了很多。

比 MySQL 多的关键能力(详细版):

JSONB 字段:

sql 复制代码
-- 创建表时用 JSONB 字段
CREATE TABLE issues (
  id SERIAL PRIMARY KEY,
  title TEXT,
  custom_fields JSONB  -- 灵活字段,不同 issue 可以有不同字段
);

-- 插入
INSERT INTO issues (title, custom_fields) VALUES
  ('Bug #1', '{"priority": "high", "story_points": 5}'),
  ('Feature #2', '{"priority": "low", "due_date": "2024-12-31"}');

-- 查询 JSONB 内部字段
SELECT * FROM issues WHERE custom_fields->>'priority' = 'high';

-- 给 JSONB 字段建索引(GIN 索引)
CREATE INDEX idx_custom ON issues USING GIN (custom_fields);

这就是 Jira 这类工具的数据模型------每个项目的自定义字段不同,用 JSONB 完美解决。MySQL 做不到(MySQL 8.0 有 JSON 类型但索引支持弱)。

全文搜索:

sql 复制代码
-- PostgreSQL 内置全文搜索
SELECT * FROM articles
WHERE to_tsvector('chinese', content) @@ to_tsquery('chinese', '数据库 & 选型');

数据量 < 几百万时够用,不需要引入 ES。

窗口函数:

sql 复制代码
-- 每个部门薪资排名(MySQL 8.0 也支持,但 PG 更早更成熟)
SELECT name, department, salary,
       RANK() OVER (PARTITION BY department ORDER BY salary DESC) as rank
FROM employees;

什么时候选 PostgreSQL 而不是 MySQL:

  1. 需要 JSONB 灵活字段 → PG 明显优势
  2. 需要轻量全文搜索 → PG 内置,不用引入 ES
  3. 需要复杂分析查询 → PG 的窗口函数、CTE 更强
  4. 新项目没有历史包袱 → PG 功能更全面

什么时候还是选 MySQL:

  1. 公司 DBA 团队只熟悉 MySQL → 运维支持是最大优势
  2. 需要和已有 MySQL 系统集成
  3. 简单 CRUD,不需要高级特性 → MySQL 够用且生态更大

4.3 Doris / StarRocks

定位: 实时 OLAP 分析数据库,面向多维聚合查询场景。

架构详解:

sql 复制代码
                    ┌─────────┐
客户端 ──SQL──→    │   FE    │  Frontend: 接收 SQL,做查询规划和元数据管理
                    │ (Master) │
                    └────┬────┘
                         │ 分发查询计划
              ┌──────────┼──────────┐
              ▼          ▼          ▼
         ┌────────┐ ┌────────┐ ┌────────┐
         │  BE 1  │ │  BE 2  │ │  BE 3  │  Backend: 存储数据,执行查询
         │ (数据1) │ │ (数据2) │ │ (数据3) │
         └────────┘ └────────┘ └────────┘
  • FE 负责 SQL 解析、查询优化、元数据管理
  • BE 负责数据存储和查询执行
  • 水平扩展:加 BE 节点就能提升存储和查询能力
  • MPP 架构:查询被拆分到多个 BE 并行执行,最后合并结果

写入方式详解:

方式 原理 适用场景 性能
Stream Load HTTP PUT 批量灌入 实时/准实时写入 高(几十万行/秒)
INSERT INTO 兼容 MySQL 语法 少量数据、调试 低(不推荐大批量)
Broker Load 从 HDFS/S3 导入 离线批量导入
Routine Load 从 Kafka 持续消费 实时流式写入

你项目用的是 Stream Load:Python consumer 解析完数据后,通过 HTTP PUT 批量灌入 Doris。

Doris vs ClickHouse 详细对比:

维度 Doris ClickHouse
协议 兼容 MySQL(JdbcTemplate 直接用) 自有协议 + HTTP 接口
JOIN 能力 强(MPP 分布式 JOIN) 弱(大表 JOIN 性能差)
实时写入 Stream Load,写入即可查 写入后有合并延迟(MergeTree)
运维复杂度 中(FE+BE,自带管理) 高(依赖 ZooKeeper,手动分片)
单表查询速度 更快(向量化引擎极致优化)
并发查询 好(MPP 架构) 差(单查询吃满资源)
生态 国内活跃(百度/小米/美团) 全球活跃(Yandex 开源)
学习成本 低(会 MySQL 就会 Doris) 中(需要学习 MergeTree 引擎)

选 Doris 的场景 : 需要 JOIN、团队熟悉 MySQL、需要实时写入即查 选 ClickHouse 的场景: 单表极致性能、日志/时序分析、不需要 JOIN


4.4 ClickHouse

定位: 单表聚合查询性能最强的 OLAP 数据库。

为什么这么快:

  1. 向量化执行: 不是一行一行处理,而是一批一批处理(类似 GPU 的 SIMD)

    yaml 复制代码
    传统: for each row: sum += row.amount    → 1000 万次循环
    向量化: 一次取 8192 行的 amount 列,CPU SIMD 指令一次加 8 个 → 快 10 倍
  2. 稀疏索引: 不是每行都有索引,而是每 8192 行一个索引条目

    yaml 复制代码
    传统 B+ 树: 1000 万行 → 1000 万个索引条目 → 索引本身就很大
    稀疏索引: 1000 万行 → 1220 个索引条目 → 索引极小,全部放内存
  3. 列式存储 + 压缩: 同类型数据压缩率极高,IO 量小

  4. MergeTree 引擎: 写入时先写到小文件,后台异步合并成大文件

    dart 复制代码
    写入: 数据 → 内存 buffer → 小文件(part)
    后台: 小 part + 小 part → 合并成大 part(类似 LSM Tree)

    这就是为什么 ClickHouse 写入后不能立即查到最新数据(有合并延迟)。

ClickHouse 的坑:

  • JOIN 性能差:大表 JOIN 可能 OOM
  • 不支持事务:没有 UPDATE/DELETE(只有 ALTER TABLE DELETE,异步执行)
  • 并发查询差:一个复杂查询可能吃满所有 CPU
  • 运维复杂:集群模式依赖 ZooKeeper

4.5 Elasticsearch

定位: 不是传统数据库,是搜索引擎。核心能力是全文搜索。

倒排索引原理(面试必考):

传统数据库(正排索引):

arduino 复制代码
文档1 → "小米手机性能很好"
文档2 → "华为手机拍照好"
文档3 → "小米电视价格低"

搜索"小米手机":需要逐个文档扫描,看是否包含"小米"和"手机" → O(N)

倒排索引:

erlang 复制代码
"小米" → [文档1, 文档3]
"手机" → [文档1, 文档2]
"性能" → [文档1]
"华为" → [文档2]
"电视" → [文档3]
...

搜索"小米手机":

  1. 查"小米"的倒排列表 → [文档1, 文档3]
  2. 查"手机"的倒排列表 → [文档1, 文档2]
  3. 取交集 → [文档1]
  4. O(1) 级别,毫秒返回

分词器:

  • 英文:按空格分词("hello world" → ["hello", "world"])
  • 中文:需要专门的分词器(IK 分词)
    • "小米手机性能很好" → ["小米", "手机", "性能", "很好"]
    • 不分词的话"小米手机"是一个整体,搜"小米"搜不到

ES 的标准用法:

makefile 复制代码
写入链路: 业务数据 → MySQL(主存储)→ Canal(监听 binlog)→ ES(搜索副本)
搜索链路: 用户搜索 → ES → 返回文档 ID 列表 → 用 ID 去 MySQL 查完整数据

ES 不做主存储的原因:

  • 没有事务
  • 更新操作实际上是"删除旧文档 + 创建新文档",开销大
  • 数据可能丢失(默认 1 秒刷盘一次)

ES 的核心概念:

概念 类比 MySQL 说明
Index Database 一类数据的集合
Document Row 一条数据(JSON 格式)
Field Column 字段
Mapping Schema 字段类型定义
Shard 分表 数据水平拆分
Replica 从库 数据副本,提高可用性和读性能

4.6 MongoDB

定位: Schema-free 的文档数据库,数据以 JSON(BSON)格式存储。

核心优势:灵活的数据模型

MySQL 的方式(需要 JOIN):

sql 复制代码
-- 用户表
users: id, name, age
-- 地址表(一对多)
addresses: id, user_id, city, street
-- 查询: SELECT * FROM users JOIN addresses ON users.id = addresses.user_id

MongoDB 的方式(嵌套文档,不需要 JOIN):

json 复制代码
{
  "name": "张三",
  "age": 25,
  "addresses": [
    { "city": "北京", "street": "xx路" },
    { "city": "上海", "street": "yy路" }
  ]
}

适用场景:

  • CMS / 博客系统:每篇文章的字段不同
  • 游戏:玩家数据结构复杂且经常变化
  • IoT:不同设备上报的数据格式不同
  • 快速原型:不想提前定义表结构

不适用场景:

  • 需要复杂 JOIN(MongoDB 的 $lookup 性能差)
  • 需要强事务(虽然 4.0+ 支持,但不如 MySQL 成熟)
  • 数据关系复杂(多对多关系用关系型更自然)

MongoDB vs PostgreSQL JSONB:

  • 如果你的数据大部分是结构化的,少部分需要灵活字段 → PostgreSQL + JSONB
  • 如果你的数据大部分是非结构化的 → MongoDB

4.7 Redis

定位: 内存 KV 存储,微秒级读写。是缓存层,不是主存储。

五大数据结构详解:

数据结构 底层实现 命令示例 典型场景
String SDS(简单动态字符串) SET key value / INCR key 缓存、计数器、分布式锁
Hash 哈希表 HSET user:1 name "张三" 对象缓存(用户信息、购物车)
List 双向链表 / 压缩列表 LPUSH queue task1 / BRPOP queue 0 消息队列(简单场景)、最新列表
Set 哈希表 SADD post:1:likes user1 / SISMEMBER 点赞集合、标签、去重、共同好友
Sorted Set 跳表 + 哈希表 ZADD rank 100 user1 / ZREVRANGE rank 0 9 排行榜、Feed 流时间线、延迟队列

Redis 作为缓存的完整模式(Cache Aside):

less 复制代码
读取流程:
  1. 查 Redis → 命中 → 返回(99% 的请求到这里就结束了)
  2. 未命中 → 查 MySQL → 写入 Redis(设置 TTL)→ 返回

写入流程:
  1. 更新 MySQL
  2. 删除 Redis 缓存(注意:是删除,不是更新)
  3. 下次读取时自动从 MySQL 重新加载

为什么删除而不是更新?
  并发场景:线程 A 更新 MySQL 为 10,线程 B 更新 MySQL 为 20
  如果更新缓存:A 更新缓存为 10,B 更新缓存为 20,但 MySQL 最终是 20 → 一致
  但如果顺序是:A 更新 MySQL,B 更新 MySQL,B 更新缓存,A 更新缓存 → 缓存是 10,MySQL 是 20 → 不一致!
  删除缓存就没这个问题:不管谁先删,下次读取都会从 MySQL 重新加载最新值。

Redis 的三大缓存问题(面试必考):

问题 现象 解决方案
缓存穿透 查不存在的数据,每次都打到 MySQL 布隆过滤器(快速判断 key 是否存在);缓存空值(SET key NULL TTL 60)
缓存击穿 热 key 过期瞬间,大量请求打到 MySQL 互斥锁(只有一个请求去查 MySQL,其他等待);热 key 永不过期
缓存雪崩 大量 key 同时过期,MySQL 被打爆 TTL 加随机值(避免同时过期);多级缓存(本地缓存 + Redis)

分布式锁详解:

vbnet 复制代码
加锁: SET lock_key unique_value NX EX 600
  NX: 只有 key 不存在时才设置(互斥)
  EX 600: 600 秒后自动过期(防死锁)
  unique_value: 用 UUID,释放时验证是自己的锁

释放: 
  if (GET lock_key == my_unique_value) {
    DEL lock_key
  }
  注意: GET + DEL 不是原子操作,要用 Lua 脚本保证原子性

4.8 HBase / Cassandra

定位: 海量数据的分布式宽列存储。

适用场景:

  • 数据量 > 几十亿行(MySQL 和 Doris 都扛不住的量级)
  • 写入 QPS > 几十万(日志、IoT、用户行为)
  • 按 key 查询为主(不需要复杂 SQL)
  • IM 消息历史(按会话 ID + 时间戳查询)

HBase vs Cassandra:

维度 HBase Cassandra
架构 主从(依赖 ZooKeeper + HDFS) 去中心化(P2P)
一致性 强一致 最终一致(可调)
运维 复杂(Hadoop 生态) 相对简单
适用 和 Hadoop 生态集成 独立部署,跨数据中心

一般项目用不到,了解概念即可。当你的数据量到了 MySQL 分库分表都搞不定的时候,再考虑。

五、组合使用模式

实际项目中很少只用一种数据库,通常是组合使用。

模式 1: MySQL + Redis(最基础,几乎所有 Web 应用)

makefile 复制代码
写入: 客户端 → API → MySQL(持久化)→ 删除 Redis 缓存
读取: 客户端 → API → Redis(缓存命中直接返回)→ 未命中 → MySQL → 回填 Redis

适用:所有需要数据库的 Web 应用。Redis 解决读性能问题。

模式 2: MySQL + Elasticsearch(需要搜索)

makefile 复制代码
写入: API → MySQL → Canal(binlog 监听)→ ES(异步同步)
搜索: 客户端 → ES(返回 ID 列表)→ MySQL(查完整数据)
CRUD: 客户端 → MySQL(ES 只做搜索,不做主存储)

适用:电商商品搜索、内容平台文章搜索、日志检索。

模式 3: MySQL(OLTP) + Doris(OLAP)(业务 + 分析)

makefile 复制代码
业务操作: 客户端 → API → MySQL(订单、用户)
数据同步: MySQL → MQ / ETL → Doris
分析查询: 报表系统 → Doris(多维聚合)

适用:既有业务操作又有数据分析的场景。你的项目就是这个模式(虽然没用 MySQL,但 Doris 承担了分析角色)。

模式 4: 全家桶(大型系统)

makefile 复制代码
MySQL: 业务主存储(用户、订单、商品)
Redis: 缓存 + 分布式锁 + 计数器 + 排行榜
ES: 商品搜索 + 日志检索
Doris/ClickHouse: 数据分析 + 报表
MQ: 数据同步管道(MySQL → ES、MySQL → Doris)
S3: 文件存储
MongoDB: 灵活 Schema 数据(可选)

适用:电商、社交平台等复杂系统。

数据同步的关键工具

工具 原理 用途
Canal 监听 MySQL binlog MySQL → ES / Redis / MQ
Debezium CDC(Change Data Capture) 任意数据库 → Kafka
Flink CDC 流式 CDC 实时数据同步和转换
DataX / Sqoop 批量 ETL 离线数据同步

六、你项目中的选型复盘

数据类型 选型 为什么这么选 有没有更好的选择
分析数据 Doris 多维聚合、Stream Load 批量写入、兼容 MySQL 协议 ClickHouse 单表更快,但不兼容 MySQL,学习成本高
原始文件 FDS/S3 大文件存储,presigned URL 直传 合理,对象存储是标准选择
缓存+锁 Redis 多进程协调,解析结果缓存 合理,Redis 是标准选择
diff_csv FDS/S3 文件型数据,按 key 存取 合理

没有用 MySQL 的原因: 项目没有传统业务数据(没有用户表、订单表),核心数据全是分析型的。

没有用 ES 的原因: 不需要全文搜索,数据查询都是结构化的 SQL 聚合。

没有用 MongoDB 的原因: 数据结构固定(每个分析模块的表结构是确定的),不需要灵活 Schema。

这个选型是合理的------用最少的组件解决问题,不过度引入复杂度。


七、面试高频问题

Q: MySQL 和 Redis 怎么保证数据一致性?

Cache Aside 模式:写入时先更新 MySQL 再删除 Redis 缓存(不是更新)。读取时先查 Redis,未命中再查 MySQL 并回填。

极端情况下可能有短暂不一致(缓存刚被删,另一个请求读到旧值并回填了缓存),但窗口极小(毫秒级)。如果业务不能容忍,可以用延迟双删(删缓存 → 等 500ms → 再删一次)。

Q: 什么时候该加 Elasticsearch?

当你发现 MySQL 的 LIKE '%关键词%' 查询越来越慢(不走索引),或者需要中文分词搜索、模糊匹配、搜索建议、拼写纠错时。如果只是精确匹配(WHERE name = '张三')或前缀匹配(WHERE name LIKE '张%'),MySQL 加索引就够了。

Q: Doris 和 ClickHouse 怎么选?

一句话:需要 JOIN 选 Doris,不需要 JOIN 选 ClickHouse。 补充:团队熟悉 MySQL 选 Doris(协议兼容),追求单表极致性能选 ClickHouse。

Q: Redis 挂了怎么办?

看 Redis 在系统里的角色:

  • 缓存 → 降级直接查 MySQL(慢但正确)
  • 分布式锁 → 降级为不加锁(可能重复计算但不影响正确性)
  • 计数器 → 降级为数据库计数(慢但正确)
  • 主存储 → 故障(不推荐用 Redis 做主存储)

关键原则:Redis 挂了系统应该降级而不是崩溃。

Q: 数据量大了怎么办?

数据库 扩展方式
MySQL 读写分离 → 分库分表(按 userId 分)→ 最终考虑 TiDB(分布式 MySQL)
Doris 加 BE 节点(水平扩展)
Redis Redis Cluster(自动分片)
ES 增加分片数 + 副本数
通用 冷热分离(热数据在线,冷数据归档到低成本存储)

Q: 怎么判断该不该引入新的数据库组件?

三个条件同时满足才引入:

  1. 现有组件确实解决不了(不是"用起来不方便",是"真的做不到或性能不可接受")
  2. 新组件的运维成本团队能承受
  3. 数据同步方案想清楚了(新组件和现有系统怎么保持数据一致)

不满足就先用现有组件凑合。过早引入组件 = 过早优化 = 万恶之源。

相关推荐
码农BookSea2 小时前
RAG详解:让大模型看见你的私有知识
人工智能·后端
rannn_1112 小时前
【Redis|实战篇7】黑马点评|附近商铺、用户签到、UV签到
java·数据库·redis·后端·uv
来一斤小鲜肉2 小时前
一文搞懂:如何用 Spring AI 搭建 MCP Server 和 Client
后端·langchain
极客沐森2 小时前
面试提问:在电商秒杀活动中,如何防止“超卖”现象的发生
后端
青柠代码录2 小时前
【Redis】缓存击穿
后端
uzong2 小时前
《企业IT架构转型之道:阿里巴巴中台战略思想与架构实战》从业务痛点到架构革命,企业转型的底层逻辑(精华解读)
后端·架构
计算机学姐3 小时前
基于SpringBoot的在线学习网站平台【个性化推荐+数据可视化+课程章节学习】
java·vue.js·spring boot·后端·学习·mysql·信息可视化
南囝coding3 小时前
Claude Code 多 Agent 协作:Subagents 和 Agent Teams 怎么选?
前端·后端
uzong3 小时前
《大型网站技术架构》-大型网站技术架构背后的系统性思维(精华解读)
后端·架构