目录
- [一、两大阵营: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:
- 需要 JSONB 灵活字段 → PG 明显优势
- 需要轻量全文搜索 → PG 内置,不用引入 ES
- 需要复杂分析查询 → PG 的窗口函数、CTE 更强
- 新项目没有历史包袱 → PG 功能更全面
什么时候还是选 MySQL:
- 公司 DBA 团队只熟悉 MySQL → 运维支持是最大优势
- 需要和已有 MySQL 系统集成
- 简单 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 数据库。
为什么这么快:
-
向量化执行: 不是一行一行处理,而是一批一批处理(类似 GPU 的 SIMD)
yaml传统: for each row: sum += row.amount → 1000 万次循环 向量化: 一次取 8192 行的 amount 列,CPU SIMD 指令一次加 8 个 → 快 10 倍 -
稀疏索引: 不是每行都有索引,而是每 8192 行一个索引条目
yaml传统 B+ 树: 1000 万行 → 1000 万个索引条目 → 索引本身就很大 稀疏索引: 1000 万行 → 1220 个索引条目 → 索引极小,全部放内存 -
列式存储 + 压缩: 同类型数据压缩率极高,IO 量小
-
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, 文档3]
- 查"手机"的倒排列表 → [文档1, 文档2]
- 取交集 → [文档1]
- 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: 怎么判断该不该引入新的数据库组件?
三个条件同时满足才引入:
- 现有组件确实解决不了(不是"用起来不方便",是"真的做不到或性能不可接受")
- 新组件的运维成本团队能承受
- 数据同步方案想清楚了(新组件和现有系统怎么保持数据一致)
不满足就先用现有组件凑合。过早引入组件 = 过早优化 = 万恶之源。