mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1906 字
6 分钟
存储引擎对比
2025-08-20

同一个 100 万行的 OLTP 工作负载,InnoDB 的 QPS 是 12 万,RocksDB 是 18 万,TiKV 是 8 万——换一个引擎,性能差 2 倍以上。不是谁更好,而是每个引擎的设计取舍不同:InnoDB 用 B+ 树换来了稳定的读延迟,RocksDB 用 LSM 树换来了极致的写吞吐,TiKV 用 Raft 共识换来了分布式一致性。

RUM 猜想(Read-Update-Memory Overhead Conjecture)给出了理论框架:读开销、更新开销、内存开销,三者不可能同时最小化。InnoDB 押注读取,RocksDB 押注写入,TiKV 在分布式一致性上加码——选型就是选取舍。

一、RUM 猜想:存储引擎的不可能三角#

1.1 三个维度的开销#

理解了对比后,需要理解一个理论框架——RUM 猜想(Read-Update-Memory Overhead Conjecture)。它指出:一个存储引擎不可能同时最小化以下三个开销:

graph TB R["读取开销<br/>Read Overhead"] --- C["不可能同时最小化"] U["更新开销<br/>Update Overhead"] --- C M["内存开销<br/>Memory Overhead"] --- C R --> R1["B+树:O(log N) 读取<br/>但更新需分裂/合并"] U --> U1["LSM树:O(1) 写入<br/>但读取需合并多层级"] M --> M1["更多索引/缓存<br/>减少读取但增加内存"] style C fill:#ffcdd2,stroke:#c62828 style R fill:#e3f2fd,stroke:#1565c0 style U fill:#e8f5e9,stroke:#2e7d32 style M fill:#fff3e0,stroke:#e65100
优化目标代价典型引擎
最小化读取开销更新时需要维护排序结构(分裂/合并),内存开销大B+ 树(InnoDB)
最小化更新开销读取时需要合并多个文件,空间放大LSM 树(RocksDB)
最小化内存开销读取和更新都需要更多 I/O压缩存储、列式存储
Note

RUM 猜想不是严格的数学定理,而是一个经验性的指导原则。它告诉我们:存储引擎的选型本质上是取舍——你必须在读取、更新、内存三个维度中选择优先优化哪个。

1.2 三种放大#

RUM 猜想在工程上体现为三种”放大”:

放大类型定义B+ 树LSM 树
写放大(Write Amplification)实际写入磁盘的数据量 / 用户写入的数据量2-4x(WAL + 页面写回)10-30x(Compaction 反复重写)
读放大(Read Amplification)读取一条记录需要的磁盘 I/O 次数3-5(树高度)10-100+(多层 SSTable 查找)
空间放大(Space Amplification)磁盘实际占用 / 有效数据量1.5-2x(页面碎片)10-50%(过期数据待 Compaction)
graph LR subgraph "B+树 — 读优化" B_READ["读放大:低<br/>O(log N)"] B_WRITE["写放大:中<br/>2-4x"] B_SPACE["空间放大:中<br/>1.5-2x"] end subgraph "LSM树 — 写优化" L_READ["读放大:高<br/>O(层级×查找)"] L_WRITE["写放大:高<br/>10-30x"] L_SPACE["空间放大:中<br/>10-50%"] end subgraph "LSM+Raft — 分布式" T_READ["读放大:高<br/>Raft + LSM"] T_WRITE["写放大:更高<br/>Raft日志 + LSM"] T_SPACE["空间放大:高<br/>3副本 + LSM"] end style B_READ fill:#c8e6c9,stroke:#2e7d32 style L_WRITE fill:#c8e6c9,stroke:#2e7d32 style T_SPACE fill:#ffcdd2,stroke:#c62828

二、InnoDB:B+ 树的工业标准#

2.1 架构总览#

InnoDB 是 MySQL 的默认存储引擎,也是 B+ 树存储引擎的工业标准实现。

graph TB subgraph "InnoDB 架构" CLIENT["客户端查询"] --> BP["缓冲池<br/>Buffer Pool"] BP -->|"命中"| RET["返回数据"] BP -->|"未命中"| BTREE["B+ 树<br/>聚簇索引"] BTREE --> LEAF["叶子节点<br/>数据页"] BTREE --> NONLEAF["非叶节点<br/>索引页"] LEAF --> AHI["自适应哈希索引<br/>AHI"] LEAF --> CHANGE["Change Buffer<br/>二级索引缓存"] WAL["WAL<br/>Redo Log"] --> DISK["磁盘"] DBLWRITE["双写缓冲<br/>Doublewrite"] --> DISK BTREE --> DISK end style BP fill:#e3f2fd,stroke:#1565c0 style BTREE fill:#c8e6c9,stroke:#2e7d32 style WAL fill:#fff9c4,stroke:#f9a825

2.2 核心组件#

组件作用详见
Buffer Pool内存中的页缓存,减少磁盘 I/OCh10 缓冲池
B+ 树索引聚簇索引(主键)+ 二级索引Ch5 B树深入
WAL (Redo Log)顺序写入保证持久性Ch9 WAL与崩溃恢复
Doublewrite防止页面撕裂(partial write)Ch10 缓冲池
Adaptive Hash Index自动为热点页创建哈希索引本节
Change Buffer缓存二级索引的修改,合并写入本节
MVCCUndo Log 实现多版本并发控制Ch12 MVCC与版本管理

2.3 写路径分析#

-- InnoDB 写入一条记录的完整路径
INSERT INTO users (id, name, email) VALUES (1001, 'Alice', 'alice@example.com');
-- 1. 写入 Redo Log(WAL)
-- 顺序写入,保证持久性
-- 2. 更新 Buffer Pool 中的数据页
-- 如果页不在内存中,先从磁盘读取
-- 3. 更新聚簇索引(主键 B+ 树)
-- 可能触发页面分裂
-- 4. 写入 Change Buffer(二级索引)
-- 二级索引的修改异步合并
-- 5. 写入 Undo Log(MVCC)
-- 用于回滚和快照读
-- 观察 InnoDB 写入状态
SHOW ENGINE INNODB STATUS\G
-- 关键指标
-- Buffer Pool hit rate: 应该 > 99%
-- Log sequence number: Redo Log 的 LSN
-- Pages read/created/written: 页面 I/O 统计

2.4 读路径分析#

-- InnoDB 读取一条记录的路径
SELECT * FROM users WHERE id = 1001;
-- 1. 检查 Buffer Pool → 命中则直接返回
-- 2. 未命中 → 从 B+ 树根节点开始查找
-- 3. 非叶节点 → 二分查找确定子节点
-- 4. 叶子节点 → 找到数据行
-- 5. 如果有自适应哈希索引 → O(1) 直接定位
-- 范围查询
SELECT * FROM users WHERE id BETWEEN 1000 AND 2000;
-- 1. B+ 树定位到起始键
-- 2. 沿叶子节点的链表顺序扫描
-- 3. 利用预读(prefetch)优化顺序 I/O
Tip

InnoDB 的 B+ 树通常 3-4 层即可存储上亿条记录。每层一次 I/O,加上 Buffer Pool 的命中率,实际读取延迟通常在微秒级。

三、RocksDB:LSM 树的高性能实现#

3.1 架构总览#

RocksDB 是 Facebook 基于 LevelDB 开发的高性能嵌入式存储引擎,采用 LSM 树结构,专为写入密集型场景优化。

graph TB subgraph "RocksDB 架构" WRITE["写入请求"] --> WAL["WAL<br/>顺序日志"] WRITE --> MEM["MemTable<br/>内存中的跳表"] MEM -->|"满"| IMMU["Immutable MemTable"] IMMU -->|"Flush"| L0["L0 SSTable"] L0 -->|"Compaction"| L1["L1 SSTable"] L1 -->|"Compaction"| L2["L2 SSTable"] L2 -->|"Compaction"| LN["... LN SSTable"] READ["读取请求"] --> MEM READ --> IMMU READ --> BLOCK["Block Cache"] READ --> BLOOM["Bloom Filter"] BLOOM --> SST["SSTable 文件"] end style MEM fill:#e3f2fd,stroke:#1565c0 style L0 fill:#fff9c4,stroke:#f9a825 style BLOOM fill:#c8e6c9,stroke:#2e7d32

3.2 核心组件#

组件作用详见
MemTable内存中的有序数据结构(跳表),写入的第一站Ch6 LSM树深入
Immutable MemTable等待 Flush 的只读 MemTableCh7 写路径
WAL保证写入持久性Ch9 WAL与崩溃恢复
SSTable磁盘上的有序文件,按层级组织Ch6 LSM树深入
Block Cache缓存频繁访问的数据块Ch8 读路径
Bloom Filter快速判断键是否可能在 SSTable 中Ch8 读路径
Compaction合并 SSTable,清理过期数据Ch6 LSM树深入
Column Families逻辑命名空间,共享 WAL 但独立 Compaction本节

3.3 写路径分析#

// RocksDB 写入流程(Go 伪代码)
func (db *RocksDB) Put(key, value []byte) error {
// 1. 写入 WAL(可选,sync 模式保证持久性)
if db.options.WriteSync {
db.wal.Append(key, value)
db.wal.Sync() // fsync,最耗时的步骤
}
// 2. 写入 MemTable(跳表插入,O(log N))
db.memTable.Put(key, value)
// 3. 如果 MemTable 满了
if db.memTable.ApproximateSize() >= db.options.MemTableSize {
// 3a. 将当前 MemTable 切换为 Immutable
db.immutable = db.memTable
// 3b. 创建新的 MemTable
db.memTable = NewMemTable()
// 3c. 后台 Flush Immutable → L0 SSTable
go db.flushImmutable()
}
return nil
}
# RocksDB 写入性能调优参数
# 写入缓冲区大小(单个 MemTable)
write_buffer_size=64MB
# 最大 MemTable 数量(超过后阻塞写入)
max_write_buffer_number=3
# L0 层触发 Compaction 的文件数
level0_file_num_compaction_trigger=4
# L0 层停止写入的文件数
level0_slowdown_writes_trigger=20
level0_stop_writes_trigger=36
# Compaction 线程数
max_background_compactions=4
# WAL 同步模式
# 0: 不同步(最快,可能丢数据)
# 1: 周期同步(折中)
# 2: 每次写入同步(最慢,最安全)
wal_sync_mode=2

3.4 读路径分析#

// RocksDB 读取流程(Go 伪代码)
func (db *RocksDB) Get(key []byte) ([]byte, error) {
// 1. 查 MemTable
if val, ok := db.memTable.Get(key); ok {
return val, nil
}
// 2. 查 Immutable MemTable
if db.immutable != nil {
if val, ok := db.immutable.Get(key); ok {
return val, nil
}
}
// 3. 查 Block Cache
if val, ok := db.blockCache.Get(key); ok {
return val, nil
}
// 4. 查 L0 SSTable(可能有范围重叠,需要查多个)
for _, sst := range db.levels[0].files {
if sst.MayContain(key, db.bloomFilter) {
if val, ok := sst.Get(key); ok {
db.blockCache.Put(key, val)
return val, nil
}
}
}
// 5. 查 L1 → L2 → ... → LN(每层最多查一个文件)
for level := 1; level <= db.maxLevel; level++ {
sst := db.levels[level].FindFile(key)
if sst != nil && sst.MayContain(key, db.bloomFilter) {
if val, ok := sst.Get(key); ok {
db.blockCache.Put(key, val)
return val, nil
}
}
}
return nil, ErrNotFound
}
Warning

RocksDB 的读放大是 LSM 树的固有代价。L0 层文件之间可能有键范围重叠,需要查多个文件。Bloom Filter 可以大幅减少无效查找,但不能完全消除。

四、TiKV:分布式 LSM + Raft#

4.1 架构总览#

TiKV 是 PingCAP 开发的分布式键值存储引擎,底层使用 RocksDB,上层通过 Raft 共识协议实现多副本一致性,是 TiDB 的存储层。

graph TB subgraph "TiKV 架构" CLIENT["TiDB SQL 层"] --> PD["Placement Driver<br/>调度中心"] CLIENT --> RAFT["Raft 层<br/>共识协议"] RAFT --> REGION1["Region 1<br/>[a, f)"] RAFT --> REGION2["Region 2<br/>[f, p)"] RAFT --> REGION3["Region 3<br/>[p, z)"] REGION1 --> RAFTGROUP1["Raft Group 1<br/>Leader + 2 Followers"] REGION2 --> RAFTGROUP2["Raft Group 2<br/>Leader + 2 Followers"] REGION3 --> RAFTGROUP3["Raft Group 3<br/>Leader + 2 Followers"] RAFTGROUP1 --> RDB1["RocksDB<br/>节点 A"] RAFTGROUP1 --> RDB2["RocksDB<br/>节点 B"] RAFTGROUP1 --> RDB3["RocksDB<br/>节点 C"] end style PD fill:#e1bee7,stroke:#6a1b9a style RAFT fill:#fff9c4,stroke:#f9a825 style RDB1 fill:#c8e6c9,stroke:#2e7d32

4.2 核心组件#

组件作用
Region数据分片,默认 96MB,是 Raft 共识的基本单位
Raft Group一个 Region 的多个副本组成一个 Raft Group
RocksDB每个 TiKV 节点使用两个 RocksDB 实例
RaftDB专门存储 Raft Log 的 RocksDB 实例
Coprocessor下推计算:在存储节点执行过滤/聚合
MVCC基于时间戳的多版本并发控制
PD (Placement Driver)全局调度:Region 分裂/合并/迁移

4.3 两个 RocksDB 实例#

// TiKV 使用两个 RocksDB 实例
// 1. raft_db: 存储 Raft Log
// - 配置为写入优先
// - 较小的 write_buffer_size
// - 独立的 Compaction 线程
// 2. kv_db: 存储用户数据
// - 标准 LSM 配置
// - Block Cache 共享
// - Column Family 分离 Raft 状态和用户数据
// TiKV 数据编码格式
// Key: {table_id}_{index_id}_{encoded_key}_{timestamp}
// Value: {type_flag}{encoded_value}
//
// MVCC 通过 timestamp 后缀实现:
// Put(key, value, ts=100) → key_100 → value
// Put(key, value, ts=200) → key_200 → value
// Get(key, ts=150) → 找到 ts <= 150 的最大版本 → key_100 → value

4.4 写路径分析#

// TiKV 写入流程(简化)
// 1. 客户端 → PD 获取 Region Leader 位置
// 2. 客户端 → Region Leader 发起 Propose
// 3. Leader 将写请求追加到 Raft Log
// 4. Raft Log 复制到 Follower
// 5. 多数派确认后,Leader Apply 写入
// 6. Apply: 写入 RaftDB + 写入 KV DB
// 写入延迟分解
// Raft Propose: ~0.1ms (内存操作)
// Raft Replicate: ~1-3ms (网络 RTT,跨机房更高)
// Raft Apply: ~0.5ms (RocksDB 写入)
// 总延迟: ~2-5ms (单次写入)
-- TiDB 中观察 TiKV 写入性能
-- 查看Region分布
SELECT
store_id,
COUNT(*) AS region_count,
SUM(approximate_size) AS total_size_mb
FROM information_schema.tikv_region_status
GROUP BY store_id;
-- 查看写入延迟
SELECT
instance,
SUM(rate) AS write_rate
FROM information_schema.tikv_store_status
GROUP BY instance;

4.5 读路径分析#

// TiKV 读取流程
// 1. Leader Read(默认)
// - 请求发送到 Region Leader
// - Leader 从本地 RocksDB 读取
// - 需要检查 Lease 确保自己是 Leader
// 2. Follower Read(v3.0+)
// - 请求可发送到任意副本
// - 副本需要向 Leader 申请 Read Index
// - 等待 State Machine >= Read Index 后读取
// - 减轻 Leader 压力
// 3. Coprocessor 下推
// - 将过滤/聚合下推到 TiKV 执行
// - 减少网络传输量
// - 利用 SSE/AVX 向量化执行
Note

TiKV 的读路径比单机 RocksDB 复杂得多。即使数据在本地,也需要经过 Raft 层的一致性检查。Follower Read 虽然可以分担 Leader 压力,但需要额外的 Read Index 协商。

五、三大引擎全面对比#

5.1 架构对比#

维度InnoDBRocksDBTiKV
数据结构B+ 树LSM 树LSM 树 + Raft
部署模式单机嵌入式单机嵌入式分布式集群
索引类型聚簇索引 + 二级索引仅主键索引仅主键索引
事务支持ACID(2PL + MVCC)基础事务(WriteBatch)分布式 ACID(2PC + MVCC)
并发控制行锁 + MVCC乐观并发 / CASMVCC + Per-Key Lock
副本机制主从复制(异步/半同步)无内置副本Raft 多副本
分区无内置分区无内置分区Region 自动分裂
语言C++C++Rust

5.2 性能对比#

graph LR subgraph "写入性能" W_INNODB["InnoDB<br/>~10K ops/s<br/>随机写瓶颈"] W_ROCKS["RocksDB<br/>~100K ops/s<br/>顺序写优化"] W_TIKV["TiKV<br/>~50K ops/s<br/>Raft 开销"] end subgraph "读取性能(点查)" R_INNODB["InnoDB<br/>~50K ops/s<br/>B+树O(logN)"] R_ROCKS["RocksDB<br/>~30K ops/s<br/>多层查找"] R_TIKV["TiKV<br/>~20K ops/s<br/>Raft一致性"] end subgraph "范围扫描" S_INNODB["InnoDB<br/>最优<br/>叶子链表"] S_ROCKS["RocksDB<br/>中等<br/>需合并层"] S_TIKV["TiKV<br/>中等<br/>Coprocessor"] end style W_ROCKS fill:#c8e6c9,stroke:#2e7d32 style R_INNODB fill:#c8e6c9,stroke:#2e7d32 style S_INNODB fill:#c8e6c9,stroke:#2e7d32
性能指标InnoDBRocksDBTiKV
随机写 QPS10K-30K100K-500K50K-100K
随机读 QPS50K-100K30K-80K20K-50K
范围扫描最优中等中等
写入延迟 P992-10ms0.5-3ms5-20ms
读取延迟 P990.5-5ms1-10ms3-15ms
写放大2-4x10-30x15-40x
读放大3-5x10-100x15-120x
空间放大1.5-2x10-50%3x副本+10-50%

5.3 写放大深度分析#

InnoDB 写放大分解:
1. Redo Log 写入:1x(顺序写)
2. 数据页写回:1x(随机写,但 Buffer Pool 缓解)
3. Doublewrite:1x(额外写一次防撕裂)
4. Undo Log:0.5x(回滚段)
总计:~3-4x
RocksDB 写放大分解:
1. WAL 写入:1x
2. MemTable → L0 Flush:1x
3. L0 → L1 Compaction:1x
4. L1 → L2 Compaction:~10x(L2 是 L1 的 10 倍大小)
5. L2 → L3 Compaction:~10x
总计:~20-30x(取决于层级数和大小比例)
TiKV 写放大分解:
1. Raft Log 写入(RaftDB):1x
2. Raft Apply 写入(KVDB):同 RocksDB 放大
3. 3 副本:3x
总计:Raft(1x) + 3 × RocksDB(~20x) = ~60x
实际优化后:~30-40x(Raft Log 复用、Pipeline 优化)

六、混合方案#

6.1 MyRocks:RocksDB 作为 MySQL 引擎#

-- MyRocks: 使用 RocksDB 替代 InnoDB 作为 MySQL 存储引擎
-- 优势:压缩率更高(约 50% 空间节省),写入吞吐更高
-- 劣势:读取性能略低,范围扫描不如 InnoDB
-- 创建 MyRocks 表
CREATE TABLE users (
id BIGINT PRIMARY KEY,
name VARCHAR(255),
email VARCHAR(255),
INDEX idx_email (email)
) ENGINE=ROCKSDB;
-- MyRocks 压缩配置
SET SESSION rocksdb_default_cf_options=
'compression=kLZ4Compression;bottommost_compression=kZSTD';
对比维度InnoDBMyRocks
存储空间1x0.5x(压缩后)
写入吞吐基准2-3x
读取延迟基准1.2-2x(略慢)
范围扫描
事务支持完整 ACID完整 ACID
二级索引高效较慢(需回表)

6.2 TiDB + TiFlash:HTAP 混合#

-- TiDB HTAP: TiKV(行存)+ TiFlash(列存)
-- TiKV: 服务 OLTP 负载
-- TiFlash: 服务 OLAP 负载
-- 通过 Raft Learner 异步复制数据
-- 创建表并启用 TiFlash 副本
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT,
amount DECIMAL(10,2),
created_at DATETIME,
INDEX idx_user (user_id)
);
-- 添加 TiFlash 副本
ALTER TABLE orders SET TIFLASH REPLICA 2;
-- OLTP 查询走 TiKV
SELECT * FROM orders WHERE id = 1001;
-- OLAP 查询走 TiFlash
SELECT user_id, SUM(amount)
FROM orders
WHERE created_at >= '2026-01-01'
GROUP BY user_id;

6.3 选型决策树#

graph TB START["存储引擎选型"] --> Q1{"需要分布式?"} Q1 -->|"是"| Q2{"需要 SQL?"} Q1 -->|"否"| Q3{"写入密集?"} Q2 -->|"是"| TIKV["TiKV<br/>+ TiDB"] Q2 -->|"否"| Q4{"需要强一致?"} Q4 -->|"是"| TIKV Q4 -->|"否"| Q5{"最终一致可接受?"} Q5 -->|"是"| CASS["Cassandra<br/>ScyllaDB"] Q3 -->|"是"| Q6{"需要二级索引?"} Q3 -->|"否"| Q7{"读取密集?"} Q6 -->|"是"| MYROCKS["MyRocks<br/>RocksDB+MySQL"] Q6 -->|"否"| ROCKS["RocksDB<br/>纯 KV"] Q7 -->|"是"| Q8{"需要事务?"} Q7 -->|"否"| ROCKS Q8 -->|"是"| INNODB["InnoDB"] Q8 -->|"否"| Q9{"需要范围扫描?"} Q9 -->|"是"| INNODB Q9 -->|"否"| ROCKS style TIKV fill:#e1bee7,stroke:#6a1b9a style INNODB fill:#c8e6c9,stroke:#2e7d32 style ROCKS fill:#fff9c4,stroke:#f9a825

七、总结#

维度InnoDBRocksDBTiKV
最佳场景OLTP、读写均衡、范围扫描写入密集、嵌入式 KV分布式 OLTP、HTAP
数据结构B+ 树LSM 树LSM + Raft
写放大低(2-4x)高(10-30x)很高(30-40x)
读放大低(3-5x)高(10-100x)很高(15-120x)
空间放大中(1.5-2x)中(10-50%)高(3x副本+10-50%)
事务完整 ACID基础事务分布式 ACID
分布式
压缩率一般优秀优秀
运维复杂度
Tip

选型没有银弹。InnoDB 适合传统 OLTP,RocksDB 适合写入密集的嵌入式场景,TiKV 适合需要分布式一致性的场景。理解 RUM 猜想,明确你的优先级——读取、更新还是内存——才能做出正确的选择。

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

存储引擎对比
https://blog.souloss.com/posts/storage/storage-engine-comparison/
作者
Souloss
发布于
2025-08-20
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时