四大数据库底层原理详解

四大数据库底层原理详解

MySQL · MongoDB · Redis · Elasticsearch 完整指南


目录

  1. 核心概念总览
  2. [MySQL ------ 关系型数据库](#MySQL —— 关系型数据库 "#%E4%BA%8Cmysql--%E5%85%B3%E7%B3%BB%E5%9E%8B%E6%95%B0%E6%8D%AE%E5%BA%93rdbms")
  3. [MongoDB ------ 文档型 NoSQL](#MongoDB —— 文档型 NoSQL "#%E4%B8%89mongodb--%E6%96%87%E6%A1%A3%E5%9E%8B-nosql")
  4. [Redis ------ 内存键值缓存/数据库](#Redis —— 内存键值缓存/数据库 "#%E5%9B%9Bredis--%E5%86%85%E5%AD%98%E9%94%AE%E5%80%BC%E7%BC%93%E5%AD%98%E6%95%B0%E6%8D%AE%E5%BA%93")
  5. [Elasticsearch ------ 全文检索引擎](#Elasticsearch —— 全文检索引擎 "#%E4%BA%94elasticsearch--%E5%85%A8%E6%96%87%E6%A3%80%E7%B4%A2%E5%BC%95%E6%93%8E")
  6. 横向对比与选型指南
  7. 进阶深入方向

一、核心概念总览

1.1 一句话对比

数据库 核心数据结构 存储介质 主要用途 一句话描述
MySQL 行存储 + B+树索引 磁盘 结构化事务数据 最成熟的关系型数据库,事务一致性强
MongoDB BSON文档 + B-Tree 磁盘 灵活文档型数据 灵活的文档存储,适合快速迭代
Redis 哈希表 + 多种数据结构 内存 高速缓存/临时数据 极速的内存数据库,适合缓存场景
Elasticsearch 倒排索引 + Segment 磁盘 全文搜索与分析 强大的搜索引擎,适合日志和检索

1.2 存储介质对比

存储介质 访问速度 容量 成本 典型应用
内存 ~100ns GB级 Redis、缓存
SSD磁盘 ~100μs TB级 MySQL、MongoDB、ES
HDD磁盘 ~10ms TB-PB级 冷数据存储、备份

二、MySQL ------ 关系型数据库(RDBMS)

2.1 基础架构:行存储

MySQL(InnoDB)按「行」把数据存到磁盘上:

  • 磁盘上最小管理单位是页(Page),默认 16KB
  • 一行记录(INSERT 的一条数据)会被放进某个页里
  • 一个页里通常放多行记录
scss 复制代码
┌──────────────────────────────────────┐
│         数据页 (16KB)                 │
│  ┌─────────┐ ┌─────────┐            │
│  │ row1    │ │ row2    │  ...       │
│  └─────────┘ └─────────┘            │
└──────────────────────────────────────┘

优缺点分析:

维度 优点 缺点
读取 SELECT * 整行一次性读出 只查某几列时也会读整行
写入 整行写入效率高 更新部分列需要锁定整行
压缩 适合行级压缩 列级压缩效率不如列存储

2.2 B+树索引

2.2.1 B+树结构特点

B+树是一种多路平衡查找树,核心特点:

css 复制代码
              [18, 35]
             /    |    \
       ┌─────┘    └─────┐
   [3,7,12]      [20,25]      [36,40]
    ↓  ← 链表连接 →   ↓   ← 链表 →  ↓
节点类型 存储内容 说明
非叶子节点 索引键 + 指针 不存真实数据,减少树高度
叶子节点 主键索引存整行数据 二级索引存主键值 所有叶子节点用双向链表连接
2.2.2 聚簇索引(Clustered Index)

InnoDB 中:主键索引 = 聚簇索引

B+树叶子节点就是这一行完整数据。

sql 复制代码
CREATE TABLE user(
  id INT PRIMARY KEY,
  name VARCHAR(20),
  age INT
);

查询过程示例:

sql 复制代码
SELECT * FROM user WHERE id = 18;

执行流程:
1. 从 B+树根节点向下查找
2. 到达叶子节点
3. 直接拿到整行数据(id, name, age)

✅ 快的原因:索引即数据,无需回表
2.2.3 二级索引(普通索引)
sql 复制代码
CREATE INDEX idx_age ON user(age);

B+树叶节点不存整行 ,只存:(age, id)

查询流程(需要回表):

sql 复制代码
SELECT * FROM user WHERE age = 25;

执行流程:
1. 走 idx_age B+树 → 找到 id
2. 回表:用 id 去主键聚簇索引再查一次

⚠️ 这叫"回表(Look-up)",比主键查询多一次 IO
2.2.4 为什么选择 B+树?
结构 问题 结论
Hash 不支持范围查询(>, BETWEEN) ❌ 不适合
红黑树 深度太大,磁盘 IO 多 ❌ 不适合
B+树 矮胖(减少磁盘 IO) + 叶子链表支持范围扫描 ✅ 最优选择

核心:数据库最怕磁盘 IO,B+树就是为"少读几次磁盘"设计的

2.3 事务与 ACID

InnoDB 通过以下机制实现 ACID:

特性 实现机制 说明
原子性(A) Undo Log 回滚未提交的事务
一致性(C) 约束 + 事务 保证数据符合规则
隔离性(I) 锁机制 + MVCC 行锁/间隙锁/临键锁
持久性(D) Redo Log 崩溃恢复

2.4 MVCC 多版本并发控制

2.4.1 什么是 MVCC?

MVCC(Multi-Version Concurrency Control) 是 InnoDB 实现高并发的核心机制。

核心思想:读不阻塞写,写不阻塞读

2.4.2 三大核心组件
scss 复制代码
┌─────────────────────────────────────────────────────────────┐
│                      MVCC 三剑客                             │
├─────────────────────────────────────────────────────────────┤
│  1. 隐藏字段(每行记录自带)                                    │
│     ├── DB_TRX_ID:最后修改该行的事务ID                         │
│     ├── DB_ROLL_PTR:回滚指针,指向 Undo Log                    │
│     └── DB_ROW_ID:行ID(如果没有主键)                         │
│                                                              │
│  2. Undo Log(回滚日志)                                       │
│     └── 存储数据的历史版本,形成版本链                            │
│                                                              │
│  3. Read View(读视图)                                        │
│     └── 决定当前事务能看到哪个版本的数据                          │
└─────────────────────────────────────────────────────────────┘
2.4.3 版本链示意

假设事务A(id=100)插入一条数据,然后事务B(id=200)更新了这条数据:

ini 复制代码
当前数据(B+树叶节点):
┌─────────────────────────────────────┐
│ id=1, name='李四'                    │
│ DB_TRX_ID=200                        │
│ DB_ROLL_PTR ─────────────────────┐   │
└──────────────────────────────────┼───┘
                                   ↓
                          Undo Log 版本链:
                    ┌─────────────────────────┐
                    │ name='张三'              │
                    │ DB_TRX_ID=100           │
                    │ DB_ROLL_PTR=NULL        │
                    └─────────────────────────┘
2.4.4 Read View 的判断逻辑

Read View 包含四个关键字段:

sql 复制代码
m_ids        -- 开启 Read View 时,当前活跃的事务ID列表(未提交)
min_trx_id   -- m_ids 中最小的事务ID
max_trx_id   -- 下一个将分配的事务ID(不是m_ids最大值)
creator_trx_id -- 创建该 Read View 的事务ID

判断规则(当前事务读取某行时):

sql 复制代码
该行的 DB_TRX_ID 与 Read View 对比:

① DB_TRX_ID == creator_trx_id
   → 是自己修改的,可见 ✅

② DB_TRX_ID < min_trx_id
   → 该版本在 Read View 创建前已提交,可见 ✅

③ DB_TRX_ID >= max_trx_id
   → 该版本在 Read View 创建后才开启的事务修改的,不可见 ❌

④ min_trx_id <= DB_TRX_ID < max_trx_id
   → 看 DB_TRX_ID 是否在 m_ids 中
     - 在 m_ids 中:说明该事务还未提交,不可见 ❌
     - 不在 m_ids 中:说明已提交,可见 ✅

如果不可见 → 沿着 DB_ROLL_PTR 去 Undo Log 找上一个版本,重复判断
2.4.5 RC vs RR 的区别
隔离级别 Read View 生成时机 特点
读已提交(RC) 每次 SELECT 都生成新的 Read View 可能出现不可重复读
可重复读(RR) 第一次 SELECT 生成 Read View,后续复用 保证可重复读
2.4.6 配合 B+树的工作流程
markdown 复制代码
SELECT 查询流程:

1. 从 B+树定位到叶子节点(按主键或二级索引)
2. 读取该行的 DB_TRX_ID
3. 用 Read View 判断该版本是否可见
   ├── 可见 → 直接返回
   └── 不可见 → 通过 DB_ROLL_PTR 去 Undo Log 找历史版本
       └── 继续判断,直到找到可见版本或版本链结束

核心:B+树存的是"最新版本",通过 Undo Log 版本链 + Read View 实现"看到历史版本"

2.5 COUNT(*) 原理与优化

2.5.1 为什么 InnoDB 的 COUNT(*) 慢?

InnoDB 的 B+树是聚簇索引,没有单独存储"总行数"的地方

scss 复制代码
┌─────────────────────────────────────────┐
│ MyISAM(有元数据存储)                     │
├─────────────────────────────────────────┤
│ 表元数据中存储:rows = 1000000            │
│ SELECT COUNT(*) → 直接读元数据,O(1)      │
└─────────────────────────────────────────┘

┌─────────────────────────────────────────┐
│ InnoDB(没有总行数存储)                   │
├─────────────────────────────────────────┤
│ 因为 MVCC:                               │
│   - 每个事务可能看到不同的行数              │
│   - 有些行被删除了但还没 purge             │
│   - 有些行是刚插入的还没提交                │
│ 所以无法存储一个"准确的"总行数              │
└─────────────────────────────────────────┘
2.5.2 COUNT(*) 执行过程
sql 复制代码
SELECT COUNT(*) FROM user;

执行流程:

markdown 复制代码
1. 选择最小的辅助索引(不是聚簇索引)
   → 辅助索引叶子节点只存 (索引列, 主键),比聚簇索引小

2. 遍历该索引的所有叶子节点
   → 统计符合条件的行数

3. 需要判断可见性(MVCC)
   → 不是简单的数节点个数
2.5.3 各种 COUNT 的区别
sql 复制代码
-- 推荐 ✅
SELECT COUNT(*) FROM user;
-- MySQL 优化:不读具体列,只数行数

-- 也行
SELECT COUNT(1) FROM user;
-- 等价于 COUNT(*),MySQL 内部会优化

-- 不推荐 ❌
SELECT COUNT(id) FROM user;
-- 需要读 id 列,判断是否为 NULL

-- 最慢 ❌
SELECT COUNT(name) FROM user;
-- 需要读 name 列,还要过滤 NULL 值
2.5.4 优化方案

方案1:使用计数表

sql 复制代码
CREATE TABLE table_counts (
  table_name VARCHAR(64) PRIMARY KEY,
  row_count BIGINT
);

-- 插入/删除时更新计数
INSERT INTO user ...;
UPDATE table_counts SET row_count = row_count + 1 WHERE table_name = 'user';

方案2:使用 Redis 缓存

sql 复制代码
SET user:count 1000000
INCR user:count
DECR user:count

方案3:估算(不需要精确值)

sql 复制代码
EXPLAIN SELECT * FROM user;
-- rows 字段是估算值

-- 或使用 information_schema
SELECT TABLE_ROWS FROM information_schema.TABLES
WHERE TABLE_NAME = 'user';

2.6 索引失效的常见场景

2.6.1 最左匹配原则

复合索引的匹配规则:从左到右依次匹配,不能跳过

sql 复制代码
-- 假设有索引:INDEX idx_name_age (name, age)

-- ✅ 走索引
SELECT * FROM user WHERE name = '张三';
SELECT * FROM user WHERE name = '张三' AND age = 20;

-- ❌ 不走索引(跳过了 name)
SELECT * FROM user WHERE age = 20;

-- ✅ 部分走索引(只用到 name)
SELECT * FROM user WHERE name = '张三' AND age > 20;
-- name 用索引,age 用索引下推

原理:B+树是按 (name, age) 组合排序的

ini 复制代码
叶子节点:
(张三, 18) → (张三, 20) → (张三, 25) → (李四, 18) → (李四, 22)

查询 name='张三':可以定位到张三的起始位置,范围扫描
查询 age=20:B+树先按 name 排序,age 是无序的,只能全表扫描

范围查询会中断后续索引使用:

sql 复制代码
-- 假设索引:(a, b, c)

-- ✅ 全部走索引
WHERE a = 1 AND b = 2 AND c = 3

-- ⚠️ a、b 走索引,c 不走
WHERE a = 1 AND b > 2 AND c = 3
2.6.2 隐式类型转换
sql 复制代码
-- 假设 phone 是 VARCHAR 类型,有索引

-- ❌ 不走索引(字符串和数字比较,MySQL 会把字符串转数字)
SELECT * FROM user WHERE phone = 13800138000;
-- 等价于:CAST(phone AS SIGNED) = 13800138000

-- ✅ 赋索引
SELECT * FROM user WHERE phone = '13800138000';
2.6.3 对索引列做运算/函数
sql 复制代码
-- ❌ 不走索引
SELECT * FROM user WHERE YEAR(create_time) = 2024;
SELECT * FROM user WHERE id + 1 = 100;
SELECT * FROM user WHERE SUBSTRING(name, 1, 1) = '张';

-- ✅ 赋索引
SELECT * FROM user WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01';
SELECT * FROM user WHERE id = 99;
SELECT * FROM user WHERE name LIKE '张%';
2.6.4 LIKE 以通配符开头
sql 复制代码
-- ✅ 赋索引(前缀匹配)
SELECT * FROM user WHERE name LIKE '张%';

-- ❌ 不走索引(后缀/中间匹配)
SELECT * FROM user WHERE name LIKE '%张';
SELECT * FROM user WHERE name LIKE '%张%';

原理:B+树按字符串排序,前缀有序,后缀无序

2.6.5 其他索引失效场景
场景 示例 是否走索引
OR 一侧无索引 WHERE a=1 OR b=1(b无索引)
NOT IN / != WHERE age != 20
IS NULL WHERE name IS NULL(NULL值比例高) ⚠️ 可能不走

2.7 适用场景

场景 是否推荐
银行交易、订单系统 ✅ 强推荐
CMS 内容管理 ✅ 推荐
电商核心业务 ✅ 推荐
需要复杂关联查询 ✅ 推荐
日志存储 ⚠️ 可用但不是最优
海量数据写入 ⚠️ 需要分库分表

三、MongoDB ------ 文档型 NoSQL

3.1 BSON 格式

3.1.1 JSON vs BSON

JSON 示例:

json 复制代码
{
  "name": "张三",
  "age": 28,
  "hobbies": ["篮球", "吉他"]
}

MongoDB 不存 JSON,存的是 BSON(Binary JSON)

BSON 相比 JSON 的优势:

特性 JSON BSON
数据类型 字符串、数字、布尔、数组、对象 额外支持 Date、Binary、Decimal128、ObjectId
解析速度 需逐字符扫描 长度前缀,可直接定位
空间效率 较低 较高(二进制编码)

JSON 是人看的,BSON 是机器快速读写用的二进制版 JSON

3.1.2 BSON 在磁盘上的样子(简化)

假设文档 {"_id": 1, "name": "张三", "age": 28}:

css 复制代码
┌─────────────────────────────────────────────┐
│ 总长度 │ _id: int32(1) │ name: string(张三) │ age: int32(28) │ 结束符 │
└─────────────────────────────────────────────┘

优缺点分析:

维度 优点 缺点
灵活性 schema-less,每条文档自带字段名和类型 比固定字段更占空间
性能 BSON 可直接定位字段值 需要解析二进制
类型 支持丰富的数据类型 -

3.2 WiredTiger 存储引擎

MongoDB 3.2 以后的默认存储引擎。

3.2.1 B-Tree vs MySQL 的 B+Tree
特性 MySQL B+Tree MongoDB WiredTiger B-Tree
叶子节点 存数据 存数据
非叶子节点 只存索引键 也存键值对
叶子链表 有(范围查询快) 没有专门的链表
本质 索引和数据分离两层 索引和数据在一棵树里
3.2.2 MVCC(多版本并发控制)
css 复制代码
时刻1:{_id:1, name:"张三", version:1}
时刻2:更新 name="李四" → 保留旧版本,新版本 version:2
时刻3:另一个读事务仍在读 version:1(不受影响)

特点:

  • 每个文档修改时,不覆盖旧数据,而是生成一个新版本
  • 读操作看到的是某个时间点的快照(snapshot)
  • 写操作之间通过**文档级锁(document-level lock)**隔离

好处:读写互不阻塞,高并发下表现好

3.2.3 写入流程
scss 复制代码
应用写入 {_id:1, name:"张三"}
        ↓
  内存中的 B-Tree 缓存(cache)
        ↓
  写入 WAL(预写日志,Write-Ahead Log) → 保证宕机不丢数据
        ↓
  定期刷盘(checkpoint):将内存中的 B-Tree 页面写入磁盘
  • WAL 先写,保证 crash-safe
  • 批量刷盘,提升写入吞吐

3.3 分片(Sharding)

3.3.1 为什么需要分片?
diff 复制代码
单机瓶颈:
- 存储容量:磁盘不够
- 读写性能:CPU/IO 成为瓶颈
- 内存限制:活跃数据放不下

分片 = 水平扩展(Scale Out)
把数据拆分到多个节点,每个节点存一部分数据
3.3.2 分片架构
scss 复制代码
┌─────────────────────────────────────────────────────────────┐
│                     MongoDB 分片集群                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   ┌─────────────┐  ┌─────────────┐  ┌─────────────┐        │
│   │ mongos 路由  │  │ mongos 路由  │  │ mongos 路由  │        │
│   └──────┬──────┘  └──────┬──────┘  └──────┬──────┘        │
│          │                │                │                │
│          └────────────────┼────────────────┘                │
│                           ↓                                  │
│              ┌────────────────────────┐                     │
│              │   Config Server 集群    │                     │
│              │   (存储集群元数据)      │                     │
│              └────────────┬───────────┘                     │
│                           ↓                                  │
│   ┌───────────────────────┼───────────────────────┐         │
│   ↓                       ↓                       ↓          │
│ ┌──────────┐         ┌──────────┐         ┌──────────┐      │
│ │ Shard 1  │         │ Shard 2  │         │ Shard 3  │      │
│ │ (Replica)│         │ (Replica)│         │ (Replica)│      │
│ │ 分片1数据 │         │ 分片2数据 │         │ 分片3数据 │      │
│ └──────────┘         └──────────┘         └──────────┘      │
└─────────────────────────────────────────────────────────────┘

三个角色:

角色 作用
mongos(路由) 接收客户端请求,路由到正确的分片
Config Server 存储集群元数据(分片范围、均衡状态)
Shard(分片) 存储实际数据,每个分片是一个复制集
3.3.3 分片键(Shard Key)

分片键决定了数据如何分布

javascript 复制代码
// 启用分片
sh.enableSharding("mydb")

// 选择分片键
sh.shardCollection("mydb.user", { "userId": 1 })  // 按 userId 分片
3.3.4 三种分片策略

1. 范围分片(Range Sharding)

yaml 复制代码
按分片键的值范围划分:

Shard 1: userId [0, 10000)
Shard 2: userId [10000, 20000)
Shard 3: userId [20000, ∞)
  • ✅ 优点:范围查询效率高
  • ❌ 缺点:数据分布可能不均匀

2. 哈希分片(Hash Sharding)

css 复制代码
按分片键的哈希值分布:

sh.shardCollection("mydb.user", { "userId": "hashed" })

计算 hash(userId) % 分片数 → 决定去哪个分片
  • ✅ 优点:数据分布均匀
  • ❌ 缺点:范围查询需要查询所有分片

3. 区域分片(Zones)

css 复制代码
给分片定义"区域",数据按规则路由到特定区域:

sh.addShardTag("shard1", "US")
sh.addShardTag("shard2", "EU")
3.3.5 分片键选择原则
原则 说明 好的示例 差的示例
高基数 分片键值域要大 { "userId": 1 } { "gender": 1 }
低频率 分片键值不要过于集中 { "userId": "hashed" } { "createTime": 1 }
查询模式 分片键应是常见查询条件 { "region": 1, "userId": 1 } { "randomId": 1 }

3.4 复制集(Replica Set)

3.4.1 复制集架构
scss 复制代码
┌─────────────────────────────────────────────────────────────┐
│                     MongoDB 复制集                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│         ┌──────────────┐                                    │
│         │   Primary    │ ← 所有写操作                        │
│         │   (主节点)    │                                    │
│         └──────┬───────┘                                    │
│                │                                            │
│                │ 同步数据(Oplog)                            │
│                │                                            │
│     ┌──────────┴──────────┐                                 │
│     ↓                     ↓                                  │
│ ┌──────────┐         ┌──────────┐                           │
│ │Secondary │         │Secondary │                           │
│ │ (从节点)  │         │ (从节点)  │                           │
│ └──────────┘         └──────────┘                           │
└─────────────────────────────────────────────────────────────┘

可选角色:

  • Arbiter(仲裁者):不存数据,只参与投票
  • Hidden Secondary:隐藏节点,不接收读请求
  • Delayed Secondary:延迟节点,用于灾备恢复
3.4.2 选主流程

选举算法:Raft 协议变种

markdown 复制代码
1. 发现 Primary 不可达的节点,发起选举
2. 该节点成为"候选人",增加自己的 term
3. 向其他节点发送选举请求(包含自己的优先级、数据新鲜度)
4. 其他节点投票(每个 term 只能投一票)
5. 获得多数票(> N/2)的节点成为新 Primary

投票规则:

因素 说明
优先级(priority) priority 越高,越可能被选为主
数据新鲜度(optime) optime 最新的节点优先
网络可达性 候选人必须能和多数节点通信
3.4.3 数据同步机制(Oplog)
markdown 复制代码
写操作流程:

1. 客户端写入 Primary
2. Primary 写入本地 Oplog(操作日志)
3. Primary 返回客户端成功
4. Secondary 异步拉取 Oplog,重放操作

Oplog 特点:
- 固定大小(capped collection)
- 幂等操作(可重复执行)
- 包含所有写操作(insert/update/delete)

3.5 为什么 MongoDB 不适合复杂关联查询

3.5.1 缺少高效的 JOIN 实现

MongoDB 的 $lookup(聚合管道)

javascript 复制代码
// 类似 SQL 的 LEFT JOIN
db.orders.aggregate([
  {
    $lookup: {
      from: "users",
      localField: "userId",
      foreignField: "_id",
      as: "user"
    }
  }
])

性能问题:

markdown 复制代码
执行过程:
1. 扫描 orders 集合
2. 对每个 order,扫描 users 集合找匹配项
3. 没有索引优化,类似"嵌套循环连接"

对比 MySQL JOIN:
- MySQL 有多种 JOIN 优化算法
- MongoDB 的 $lookup 本质上是嵌套循环,O(N*M)
3.5.2 数据模型设计理念不同

MongoDB 的设计哲学:嵌入 > 关联

bash 复制代码
关系型设计(MySQL):
┌─────────────┐       ┌─────────────┐
│   users     │       │   orders    │
├─────────────┤       ├─────────────┤
│ id          │ ←──── │ userId      │
│ name        │       │ orderId     │
│ email       │       │ amount      │
└─────────────┘       └─────────────┘
查询需要 JOIN

文档型设计(MongoDB):
{
  "_id": 1,
  "name": "张三",
  "email": "zhang@example.com",
  "orders": [                        ← 嵌入文档
    { "orderId": 101, "amount": 100 },
    { "orderId": 102, "amount": 200 }
  ]
}
查询一次即可获取所有数据

3.6 适用场景

场景 是否推荐
嵌入式数据模型(博客文章+评论) ✅ 推荐
日志、事件流 ✅ 推荐
物联网设备数据 ✅ 推荐
快速迭代的产品原型 ✅ 推荐
复杂的多表关联查询 ❌ 不推荐
强事务一致性要求(银行转账) ❌ 不推荐

四、Redis ------ 内存键值缓存/数据库

4.1 纯内存操作

数据主要存在 RAM 里,读写都是直接操作内存。

操作 MySQL(磁盘) Redis(内存)
读一条数据 ~1-10ms < 1μs
写一条数据 ~1-10ms < 1μs

差距大约 1000~10000 倍

4.2 持久化方案

4.2.1 RDB(快照)
perl 复制代码
每隔 N 分钟 / 写满多少条后
→ fork 一个子进程
→ 把当前内存里的全部数据 dump 到一个 .rdb 文件
→ 下次启动时加载这个文件恢复数据
特性 说明
文件大小
恢复速度
数据丢失 可能丢失最近几分钟的数据
4.2.2 AOF(追加日志)
css 复制代码
每次写操作(SET/DEL...)
→ 追加到 AOF 文件末尾
→ 可配置:每秒刷盘 / 每次写都刷盘
→ 重启时重放 AOF 恢复数据
特性 说明
文件大小
恢复速度
数据丢失 最多丢1秒数据

生产环境建议:两者都开,RDB 用于快速恢复,AOF 用于防丢数据

4.3 事件驱动单线程模型

4.3.1 「单线程」是指什么?

Redis 的命令处理是单线程的,所有客户端发来的命令串行执行:

sql 复制代码
时间轴 →
客户端A: SET a 1
客户端B: GET a
客户端C: DEL b
                    ↓
          Redis 单线程依次处理:
          SET a 1 → GET a → DEL b

优缺点:

优点 缺点
没有锁竞争 一个耗时命令会阻塞后面所有命令
没有死锁风险 不能充分利用多核 CPU
实现简单 -
4.3.2 「事件驱动」是什么?

Redis 用**事件循环(Event Loop)**来处理:

markdown 复制代码
无限循环:
  1. 等待事件发生(epoll/kqueue 监听 socket)
  2. 有事件来了 → 处理这个事件(读请求、执行命令、写响应)
  3. 回到步骤1

跟 Node.js、Nginx 的事件循环是同一个思路

4.3.3 Redis 6.0 之后的多线程

Redis 6.0 的「多线程」只针对网络 IO 部分:

ini 复制代码
传统单线程:
  [网络读] → [命令执行] → [网络写]
  全部由一个线程完成

Redis 6.0+:
  [网络读] ← 多线程并行读
  [命令执行] ← 仍然是单线程!
  [网络写] → 多线程并行写

核心命令执行还是单线程,只是把网络收发这种 CPU 密集的活分给多线程干

4.4 核心数据结构

结构 底层实现 典型场景
String SDS(Simple Dynamic String) 缓存、计数器
List quicklist(ziplist + linkedlist 混合) 消息队列
Set hashtable / intset 标签、去重
ZSet skiplist + hashtable 排行榜
Hash ziplist / hashtable 对象缓存
Stream radix tree 消息队列(替代 Pub/Sub)

4.5 跳跃表(Skiplist)详解

4.5.1 什么是跳跃表?

跳跃表 = 有序链表 + 多级索引

makefile 复制代码
原始有序链表(查找 O(N)):
1 → 3 → 5 → 7 → 9 → 11 → 13 → 15 → 17 → 19 → 21 → 23 → 25

加一级索引(每隔一个节点提取):
1 ──────────── 9 ──────────── 17 ──────────── 25
↓             ↓               ↓               ↓
1 → 3 → 5 → 7 → 9 → 11 → 13 → 15 → 17 → 19 → 21 → 23 → 25

再加一级索引:
1 ──────────────────────── 17 ──────────────────────── 25
↓                          ↓                           ↓
1 ──────────── 9 ──────────── 17 ──────────── 25
↓             ↓               ↓               ↓
1 → 3 → 5 → 7 → 9 → 11 → 13 → 15 → 17 → 19 → 21 → 23 → 25

时间复杂度:O(log N),和平衡二叉树一样

4.5.2 为什么 Redis 用跳跃表而不是红黑树?
维度 跳跃表 红黑树/AVL树
实现复杂度 简单(链表+随机层数) 复杂(旋转、变色)
范围查询 ✅ 天然支持(底层是链表) ❌ 需要中序遍历
内存占用 稍大(多级指针) 较小
并发友好 ✅ 局部更新 ❌ 树结构调整范围大
调试难度 ✅ 直观 ❌ 复杂

Redis 作者 Antirez 的原话:

"跳跃表更容易实现,调试更简单,对于 ZSET 的使用场景(范围查询),跳跃表更合适。"

4.5.3 ZSET 的实现
c 复制代码
// Redis 源码中的 ZSET 结构
typedef struct zset {
    dict *dict;      // 哈希表:member → score(O(1) 查找分数)
    zskiplist *zsl;  // 跳跃表:score → member(O(logN) 范围查询)
} zset;

为什么要同时用两个结构?

makefile 复制代码
查询某用户分数:用 dict,O(1)
ZSCORE user:rank 张三

查询排行榜前100名:用 skiplist,O(logN + M)
ZRANGE user:rank 0 99 WITHSCORES

范围查询(分数100-200):用 skiplist
ZRANGEBYSCORE user:rank 100 200 WITHSCORES

4.6 内存淘汰策略

4.6.1 为什么需要淘汰策略?
markdown 复制代码
Redis 是内存数据库,内存有限

当内存使用达到 maxmemory 阈值时:
1. 新写入会失败(OOM)
2. 或者根据淘汰策略删除部分 key
4.6.2 八种淘汰策略
vbnet 复制代码
┌─────────────────────────────────────────────────────────────┐
│                  Redis 内存淘汰策略                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  不淘汰(内存满时拒绝写入)                                     │
│  ├── noeviction        不淘汰,写入报错(默认)                 │
│                                                             │
│  全局淘汰(从所有 key 中选择)                                  │
│  ├── allkeys-lru       淘汰最近最少使用的 key                  │
│  ├── allkeys-lfu       淘汰最不经常使用的 key                  │
│  ├── allkeys-random    随机淘汰                              │
│                                                             │
│  仅淘汰设置了过期时间的 key                                    │
│  ├── volatile-lru      淘汰最近最少使用且设置了 TTL 的 key       │
│  ├── volatile-lfu      淘汰最不经常使用且设置了 TTL 的 key       │
│  ├── volatile-ttl      淘汰 TTL 最短(即将过期)的 key          │
│  ├── volatile-random   随机淘汰设置了 TTL 的 key               │
│                                                             │
└─────────────────────────────────────────────────────────────┘
4.6.3 如何选择淘汰策略?
场景 推荐策略
缓存场景(部分数据是热点) allkeys-lru
热点数据访问频率差异大 allkeys-lfu
只缓存临时数据(有 TTL) volatile-lru
数据重要性差异大 volatile-ttl
数据访问模式随机 allkeys-random
不能丢失任何数据 noeviction

4.7 缓存穿透/击穿/雪崩

4.7.1 缓存穿透(Penetration)

定义: 查询一个根本不存在的数据,缓存和数据库都没有

makefile 复制代码
请求流程:
客户端请求 key=999999
    ↓
Redis: 不存在
    ↓
MySQL: 也不存在
    ↓
返回空,但下次请求还会查数据库

危害: 恶意请求大量不存在的 key,压垮数据库

解决方案:

方案1:缓存空值

java 复制代码
public Object get(String key) {
    Object value = redis.get(key);
    if (value != null) {
        if ("NULL".equals(value)) {
            return null;  // 缓存的空值
        }
        return value;
    }

    // 查数据库
    value = db.query(key);
    if (value == null) {
        // 缓存空值,设置较短过期时间
        redis.set(key, "NULL", 60);
    } else {
        redis.set(key, value, 3600);
    }
    return value;
}

方案2:布隆过滤器(Bloom Filter)

markdown 复制代码
布隆过滤器:一个超大的位数组 + 多个哈希函数

写入时:
1. 对 key 计算多个哈希值
2. 在位数组对应位置标记为 1

查询时:
1. 对 key 计算多个哈希值
2. 检查位数组对应位置是否都为 1
   - 都为 1:key **可能存在**(有误判率)
   - 有一个为 0:key **一定不存在**
java 复制代码
// Redis 4.0+ 内置布隆过滤器
BF.ADD users user:1001    // 添加
BF.EXISTS users user:1001 // 查询
4.7.2 缓存击穿(Breakdown)

定义: 某个热点 key 过期,瞬间大量请求穿透到数据库

vbnet 复制代码
场景:
key="hot_product_123" 正在被大量访问
TTL 到期,key 被删除
此时 1000 个并发请求同时查询该 key
全部穿透到数据库

解决方案:

方案1:互斥锁(只让一个请求查库)

java 复制代码
public Object get(String key) {
    Object value = redis.get(key);
    if (value != null) {
        return value;
    }

    // 尝试获取锁
    String lockKey = "lock:" + key;
    if (redis.setnx(lockKey, "1", 10)) {
        try {
            // 再次检查缓存(双重检查)
            value = redis.get(key);
            if (value != null) {
                return value;
            }

            // 查数据库
            value = db.query(key);
            redis.set(key, value, 3600);
            return value;
        } finally {
            redis.del(lockKey);
        }
    } else {
        // 等待后重试
        Thread.sleep(50);
        return get(key);
    }
}

方案2:逻辑过期(不设置 TTL,用字段判断)

java 复制代码
// 数据结构
{
    "data": "商品信息",
    "expireTime": 1704067200  // 逻辑过期时间
}

public Object get(String key) {
    Object cached = redis.get(key);

    if (cached != null) {
        if (cached.expireTime > System.currentTimeMillis()) {
            return cached.data;  // 未过期
        }

        // 过期了,异步刷新
        asyncRefresh(key);
        return cached.data;  // 返回旧数据
    }

    // 缓存不存在,同步查询
    return syncRefresh(key);
}
4.7.3 缓存雪崩(Avalanche)

定义: 大量 key 同时过期 ,或 Redis 宕机

makefile 复制代码
场景1:批量设置缓存,过期时间相同
1小时后,所有 key 同时过期
瞬间大量请求穿透到数据库

场景2:Redis 宕机
所有请求全部打到数据库

解决方案:

方案1:过期时间加随机值

java 复制代码
// 过期时间 = 基础时间 + 随机时间
int baseExpire = 3600;
int randomExpire = new Random().nextInt(600);  // 0-600秒随机
redis.set(key, value, baseExpire + randomExpire);

方案2:多级缓存

scss 复制代码
请求 → L1 缓存(本地) → L2 缓存(Redis) → 数据库

本地缓存(Caffeine/Guava Cache):
- 容量小,速度快
- 即使 Redis 挂了,本地缓存还能抗一部分流量

方案3:熔断降级

java 复制代码
@SentinelResource(value = "getProduct", fallback = "getProductFallback")
public Object getProduct(String key) {
    return cache.get(key);
}

public Object getProductFallback(String key) {
    // 降级:返回默认值或提示
    return "服务繁忙,请稍后重试";
}

方案4:Redis 高可用

  • 主从 + 哨兵:主节点故障自动切换
  • Redis Cluster:分片 + 副本,部分节点故障不影响整体
4.7.4 三者对比总结
问题 原因 解决方案
穿透 查询不存在的数据 缓存空值、布隆过滤器
击穿 热点 key 过期 互斥锁、逻辑过期、永不过期
雪崩 大量 key 同时过期或 Redis 宕机 随机过期、多级缓存、熔断降级、高可用

4.8 适用场景

场景 是否推荐
缓存 ✅ 强推荐
会话管理 ✅ 推荐
排行榜 ✅ 推荐
消息队列 ✅ 推荐(Stream)
分布式锁 ✅ 推荐
持久化存储 ⚠️ 可以但不是最优
大量数据存储 ❌ 不推荐(内存成本高)

五、Elasticsearch ------ 全文检索引擎

5.1 倒排索引

5.1.1 正排索引 vs 倒排索引

假设三篇文档:

文档ID 内容
1 我喜欢吃苹果
2 苹果手机很好用
3 我喜欢香蕉

正排索引(MySQL 那种):

css 复制代码
文档1 → [我, 喜欢, 吃, 苹果]
文档2 → [苹果, 手机, 很, 好用]
文档3 → [我, 喜欢, 香蕉]

查询「苹果出现在哪些文档?」→ 得扫一遍所有文档 →

倒排索引(ES 那种):

css 复制代码
苹果 → [文档1, 文档2]    ← 词 → 文档列表
我   → [文档1, 文档3]
喜欢 → [文档1, 文档3]
吃   → [文档1]
手机 → [文档2]
...

查询「苹果」→ 直接查词典 → 瞬间知道在文档1和2里 →

倒排索引 = 以词找文档,而不是以文档找词

5.1.2 倒排索引结构(简化版)
scss 复制代码
┌──────────┬──────────────────────────────┐
│  词项    │  倒排列表(Posting List)       │
├──────────┼──────────────────────────────┤
│ 苹果     │ (doc1, 位置5) (doc2, 位置1)    │
│ 手机     │ (doc2, 位置2)                  │
│ 我喜欢   │ (doc1, 位置1) (doc3, 位置1)    │
└──────────┴──────────────────────────────┘

每个词项后面还记录了:

  • 文档ID
  • 词频(TF):该词在文档中出现次数
  • 位置:在文档中的第几个词
  • 偏移量:在原文中的字符位置(用于高亮)
5.1.3 建索引的过程(分词)
arduino 复制代码
输入:"苹果手机很好用"
        ↓
  分词器(Analyzer):
  1. 字符过滤(去掉HTML标签等)
  2. 分词(按空格/标点/语言规则切词)
  3. 词项过滤(转小写、去停用词、词干提取)
        ↓
  输出词项:[苹果, 手机, 很好, 用]
        ↓
  写入倒排索引

不同语言用不同分词器:

  • 中文:IK分词器、HanLP
  • 英文:Standard Analyzer(自动转小写+去复数)

5.2 Lucene 库

5.2.1 Lucene 是 ES 的发动机
  • Lucene:一个 Java 写的全文检索引擎库(Apache 基金会)
  • Elasticsearch:在 Lucene 外面包了一层:分布式、RESTful API、集群管理、监控......
ini 复制代码
Elasticsearch = 一辆汽车(整车)
Lucene       = 发动机(核心动力)
5.2.2 Segment(段)------ 不可变的秘密

Lucene 的索引由多个 Segment 组成:

css 复制代码
写入流程:
  新文档 → 内存缓冲区
        ↓(每秒 refresh)
  生成一个小 Segment(可被搜索)
        ↓(后台 merge)
  多个小 Segment 合并成大 Segment
        ↓
  删除旧的 Segment

特点:

  • Segment 一旦写入就不变了(immutable)
  • 删除文档只是在 Segment 里打个标记(不是真删)
  • 更新文档 = 标记旧文档删除 + 写入新文档
优点 缺点
写入时不需要加锁 删除的空间不会立即释放
读取时只需加载 Segment 索引 合并时会有 IO 压力
利用 OS Page Cache 加速 -

5.3 ES 的「近实时」实现

scss 复制代码
写入一条文档
        ↓
  ① 写入内存缓冲区(Memory Buffer)
        ↓(同时写入 translog 防止丢数据)
  ② 每 1 秒(默认) → refresh
        ↓
     内存缓冲区清空,生成新的 Segment
     这个 Segment 打开,可以被搜索
        ↓
  ③ 每 30 分钟 或 translog 满了 → flush
        ↓
     Segment 刷到磁盘
     translog 清空
  • 写入后最快 1 秒才能搜到(近实时,不是实时)
  • refresh_interval 改成 -1 → 永远搜不到刚写的数据

5.4 ES 的分布式架构

scss 复制代码
集群(Cluster)
  └── 节点(Node)
       ├── 主节点(Master):管理集群状态、分配分片
       └── 数据节点(Data):存数据、处理查询

索引(Index)
  └── 分片(Shard) ← 每个分片就是一个独立的 Lucene 实例
       ├── 主分片(Primary Shard):写入口
       └── 副本分片(Replica Shard):读备份
5.4.1 写入流程
bash 复制代码
客户端 POST /my_index/_doc/1
        ↓
  路由计算:shard = hash(_id) % number_of_primary_shards
        ↓
  转发到对应的主分片所在的节点
        ↓
  写入主分片 → 同步到副本分片 → 返回成功
5.4.2 查询流程
ini 复制代码
客户端 GET /my_index/_search?q=苹果
        ↓
  协调节点(接收请求的节点)
        ↓
  广播到所有分片(主分片 + 副本分片都行)
        ↓
  各分片本地搜索 → 返回结果给协调节点
        ↓
  协调节点合并排序 → 返回给客户端

5.5 BM25 评分公式

5.5.1 什么是 BM25?

BM25(Best Matching 25) 是 ES 默认的相关性评分算法,用于衡量文档与查询的匹配程度。

5.5.2 BM25 公式
scss 复制代码
score(D, Q) = Σ IDF(qi) · (f(qi, D) · (k1 + 1)) / (f(qi, D) + k1 · (1 - b + b · |D|/avgdl))

其中:
- D:文档
- Q:查询(包含多个词 qi)
- f(qi, D):词 qi 在文档 D 中的词频(TF)
- |D|:文档 D 的长度
- avgdl:平均文档长度
- k1:词频饱和参数(默认 1.2)
- b:文档长度归一化参数(默认 0.75)
5.5.3 BM25 vs TF-IDF
特性 TF-IDF BM25
词频增长 线性增长 饱和增长(有上限)
文档长度 不考虑 归一化(惩罚长文档)
参数可调 固定公式 k1、b 可调整

饱和效应示意:

makefile 复制代码
TF-IDF:
词频 1 → 分数 1.0
词频 2 → 分数 2.0
词频 10 → 分数 10.0  ← 无限增长

BM25:
词频 1 → 分数 0.8
词频 2 → 分数 1.3
词频 10 → 分数 2.0  ← 趋于饱和

5.6 ES 的聚合(Aggregation)原理

5.6.1 聚合类型
python 复制代码
┌─────────────────────────────────────────────────────────────┐
│                  Elasticsearch 聚合类型                       │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  桶聚合(Bucket) - 分组                                       │
│  ├── terms          按字段值分组                              │
│  ├── range          按数值范围分组                            │
│  ├── date_histogram 按时间间隔分组                            │
│  └── filter         按条件分组                               │
│                                                             │
│  指标聚合(Metric) - 计算                                     │
│  ├── avg / sum / min / max   基础统计                        │
│  ├── stats           综合统计                                │
│  ├── cardinality     去重计数(近似)                          │
│  └── percentiles     百分位数                                │
│                                                             │
│  管道聚合(Pipeline) - 基于其他聚合结果                         │
│  ├── derivative      求导                                    │
│  ├── cumulative_sum  累加和                                  │
│  └── moving_avg      移动平均                                │
│                                                             │
└─────────────────────────────────────────────────────────────┘
5.6.2 Terms 聚合原理

示例:统计每种商品的销量

json 复制代码
GET /orders/_search
{
  "size": 0,
  "aggs": {
    "products": {
      "terms": {
        "field": "product_name.keyword",
        "size": 10
      }
    }
  }
}

执行过程:

less 复制代码
1. 协调节点收到请求,广播到所有分片

2. 每个分片本地执行:
   a. 遍历所有文档(或匹配的文档)
   b. 读取 product_name 字段值
   c. 在内存中构建哈希表
   d. 排序,返回 Top N 给协调节点

3. 协调节点合并各分片结果:
   a. 合并哈希表
   b. 重新排序
   c. 返回最终 Top N

5.7 为什么 ES 不适合做事务型数据库

5.7.1 没有完整的事务支持
json 复制代码
ES 只支持"单文档"的原子性:

✅ 单文档操作是原子的
PUT /my_index/_doc/1 { ... }  // 成功或失败,不会半成功

❌ 多文档操作不是原子的
POST /_bulk
{ "index": { "_id": 1 } }
{ "balance": 100 }
{ "index": { "_id": 2 } }
{ "balance": 200 }

// 第一个成功,第二个失败 → 部分成功,无法回滚
5.7.2 最终一致性,非强一致性
diff 复制代码
ES 的写入流程:
- 写入后约 1 秒才能搜到(近实时)
- 宕机可能丢失最近 1 秒的数据(如果没 flush)

MySQL InnoDB:
- Redo Log 保证持久性(实时刷盘)
- 写入立即对其他事务可见(或按隔离级别)
- 宕机恢复到最新状态
5.7.3 更新成本高
diff 复制代码
ES 的更新 = 删除旧文档 + 写入新文档

问题:
- 更新比插入慢很多
- 频繁更新会产生大量标记删除,占用空间
- Merge 会消耗 IO

5.8 适用场景

场景 是否推荐
全文搜索 ✅ 推荐
日志分析(ELK) ✅ 推荐
监控告警 ✅ 推荐
时序数据 ✅ 推荐
银行转账 ❌ 不推荐
订单处理 ❌ 不推荐
库存扣减 ❌ 不推荐
复杂事务 ❌ 不推荐

六、横向对比与选型指南

6.1 核心特性对比

维度 MySQL MongoDB Redis Elasticsearch
核心数据结构 B+树(行) B-Tree(BSON文档) 哈希表(内存) 倒排索引(词→文档)
存储介质 磁盘 磁盘 内存 磁盘
读写速度 毫秒级 毫秒级 微秒级 毫秒级(搜索快)
主要用途 事务型数据 灵活文档存储 缓存/临时数据 全文搜索/日志
一致性 强一致(ACID) 最终一致(可调) 强一致(单线程) 最终一致
扩展方式 主从/分库分表 分片集群 集群(16384槽) 分片+副本

6.2 查询能力对比

查询类型 MySQL MongoDB Redis Elasticsearch
主键查询 ✅ 极快 ✅ 快 ✅ 极快 ✅ 快
范围查询 ✅ 快 ✅ 快 ✅ 快 ✅ 快
全文搜索 ⚠️ 弱 ⚠️ 弱 ❌ 不支持 ✅ 极强
复杂关联 ✅ 强 ⚠️ 弱 ❌ 不支持 ❌ 不支持
聚合分析 ✅ 强 ⚠️ 一般 ⚠️ 一般 ✅ 强

6.3 事务支持对比

数据库 事务支持 隔离级别 说明
MySQL ✅ 完整 ACID RU/RC/RR/Serializable 最成熟的事务支持
MongoDB ⚠️ 4.0+ 支持多文档事务 - 性能开销大
Redis ✅ 单命令原子性 - 事务队列(MULTI/EXEC)
Elasticsearch ❌ 单文档原子性 - 不支持跨文档事务

6.4 选型决策树

markdown 复制代码
开始选型
    │
    ├─ 需要强事务一致性?
    │   └─ 是 → MySQL
    │
    ├─ 需要全文搜索?
    │   └─ 是 → Elasticsearch
    │
    ├─ 需要极高性能缓存?
    │   └─ 是 → Redis
    │
    ├─ 数据结构灵活多变?
    │   └─ 是 → MongoDB
    │
    └─ 默认选择 → MySQL

6.5 常见组合方案

方案1:MySQL + Redis
diff 复制代码
┌──────────┐      ┌──────────┐
│   MySQL  │ ←──→ │   Redis  │
│  主数据库  │      │   缓存    │
└──────────┘      └──────────┘

适用场景:
- 需要缓存热点数据
- 读多写少
- 对一致性要求较高
方案2:MySQL + Elasticsearch
diff 复制代码
┌──────────┐      ┌──────────┐
│   MySQL  │ ───→ │    ES    │
│  主数据库  │ 同步  │   搜索    │
└──────────┘      └──────────┘

适用场景:
- 需要复杂搜索功能
- 日志分析
- MySQL 负责事务,ES 负责搜索
方案3:MySQL + Redis + Elasticsearch
diff 复制代码
┌──────────┐      ┌──────────┐      ┌──────────┐
│   MySQL  │ ←──→ │   Redis  │ ───→ │    ES    │
│  主数据库  │      │   缓存    │      │   搜索    │
└──────────┘      └──────────┘      └──────────┘

适用场景:
- 电商系统
- 内容管理系统
- 需要 ACID + 高性能 + 搜索

七、进阶深入方向

7.1 MySQL 进阶

主题 关键问题
MVCC Read View 如何配合 Undo Log 实现可重复读?
COUNT(*) 为什么 InnoDB 的 COUNT(*) 慢?如何优化?
索引失效 最左匹配、隐式转换、函数运算等场景
锁机制 行锁、间隙锁、临键锁的原理
Buffer Pool 页面管理、LRU 淘汰策略

7.2 MongoDB 进阶

主题 关键问题
分片 分片键如何选择?范围分片 vs 哈希分片?
复制集 选主流程(Raft)如何工作?
$lookup 为什么不适合复杂关联查询?
Change Stream 如何实现数据变更监听?
索引优化 复合索引、TTL 索引的使用

7.3 Redis 进阶

主题 关键问题
跳跃表 为什么 ZSET 用跳跃表而不是红黑树?
淘汰策略 LRU/LFU 的实现原理?如何选择?
缓存问题 穿透/击穿/雪崩的解决方案?
持久化 RDB vs AOF 的取舍?
集群 Redis Cluster 的槽位分配原理?

7.4 Elasticsearch 进阶

主题 关键问题
BM25 评分公式如何计算?参数如何调优?
聚合 Terms 聚合的精度问题?如何优化?
Segment 为什么 Segment 是不可变的?Merge 策略?
写入性能 bulk 批量写入的最佳实践?
分片策略 分片数量如何规划?热点问题如何解决?

附录:常见面试题速查

MySQL

问题 答案要点
为什么用 B+树? 矮胖减少磁盘 IO,叶子链表支持范围查询
聚簇索引 vs 二级索引? 聚簇索引叶节点存整行,二级索引存主键值(需回表)
MVCC 原理? 隐藏字段 + Undo Log + Read View
COUNT(*) 为什么慢? 需要遍历索引,MVVC 导致无法存储准确行数
索引何时失效? 最左匹配、隐式转换、函数运算、前缀通配符

MongoDB

问题 答案要点
BSON vs JSON? BSON 是二进制格式,支持更多类型,解析更快
分片键如何选择? 高基数、低频率、符合查询模式
复制集选主流程? Raft 协议变种,多数投票,优先级+数据新鲜度
为什么不适合复杂关联? $lookup 是嵌套循环,无 JOIN 优化

Redis

问题 答案要点
为什么单线程还快? 内存操作 + 事件驱动 + 无锁竞争
跳跃表 vs 红黑树? 实现简单、范围查询友好
淘汰策略有哪些? noeviction、allkeys-lru、volatile-lru 等 8 种
缓存穿透/击穿/雪崩? 布隆过滤器、互斥锁、随机过期

Elasticsearch

问题 答案要点
倒排索引原理? 词项 → 文档列表,以词找文档
为什么近实时? refresh 间隔 1 秒,Segment 才可被搜索
BM25 vs TF-IDF? 词频饱和增长、文档长度归一化
为什么不适合事务? 无跨文档事务、最终一致性、更新成本高
相关推荐
glartchen1 小时前
使用 Pixi 配置 Whisper 环境,实现采用本地大模型读取视频字幕
后端
站大爷IP1 小时前
明明传了参数,进程池却说没找到?记一次变量丢失的排查实录
后端
用户6757049885022 小时前
从失真到绝对精准:Go语言中如何进行高精度计算?
后端·go
程序员cxuan3 小时前
GPT-5.6 官宣解禁,明天全量发布!
人工智能·后端·程序员
郑州光合科技余经理3 小时前
海外外卖平台源码改造实战——多语言核心代码实现
java·开发语言·前端·后端·mysql·架构·php
喜欢打篮球的普通人3 小时前
Trition程序编写:从“Hello CUDA“到“Hello Triton“:向量加法背后的编译黑魔法
开发语言·后端·rust
苏三说技术3 小时前
Spring AI 2.0和Spring AI Alibaba,哪个更好?
后端
用户6757049885023 小时前
还在用字符串返回错误提示?Go语言优雅错误处理的正确姿势!
后端·go
雪隐3 小时前
用Flutter做背单词APP-02我画了设计稿,然后让AI帮我设计了数据库(顺便聊了会天)
前端·人工智能·后端