同一个 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)。它指出:一个存储引擎不可能同时最小化以下三个开销:
| 优化目标 | 代价 | 典型引擎 |
|---|---|---|
| 最小化读取开销 | 更新时需要维护排序结构(分裂/合并),内存开销大 | B+ 树(InnoDB) |
| 最小化更新开销 | 读取时需要合并多个文件,空间放大 | LSM 树(RocksDB) |
| 最小化内存开销 | 读取和更新都需要更多 I/O | 压缩存储、列式存储 |
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) |
二、InnoDB:B+ 树的工业标准
2.1 架构总览
InnoDB 是 MySQL 的默认存储引擎,也是 B+ 树存储引擎的工业标准实现。
2.2 核心组件
| 组件 | 作用 | 详见 |
|---|---|---|
| Buffer Pool | 内存中的页缓存,减少磁盘 I/O | Ch10 缓冲池 |
| B+ 树索引 | 聚簇索引(主键)+ 二级索引 | Ch5 B树深入 |
| WAL (Redo Log) | 顺序写入保证持久性 | Ch9 WAL与崩溃恢复 |
| Doublewrite | 防止页面撕裂(partial write) | Ch10 缓冲池 |
| Adaptive Hash Index | 自动为热点页创建哈希索引 | 本节 |
| Change Buffer | 缓存二级索引的修改,合并写入 | 本节 |
| MVCC | Undo 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/OInnoDB 的 B+ 树通常 3-4 层即可存储上亿条记录。每层一次 I/O,加上 Buffer Pool 的命中率,实际读取延迟通常在微秒级。
三、RocksDB:LSM 树的高性能实现
3.1 架构总览
RocksDB 是 Facebook 基于 LevelDB 开发的高性能嵌入式存储引擎,采用 LSM 树结构,专为写入密集型场景优化。
3.2 核心组件
| 组件 | 作用 | 详见 |
|---|---|---|
| MemTable | 内存中的有序数据结构(跳表),写入的第一站 | Ch6 LSM树深入 |
| Immutable MemTable | 等待 Flush 的只读 MemTable | Ch7 写路径 |
| 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=20level0_stop_writes_trigger=36
# Compaction 线程数max_background_compactions=4
# WAL 同步模式# 0: 不同步(最快,可能丢数据)# 1: 周期同步(折中)# 2: 每次写入同步(最慢,最安全)wal_sync_mode=23.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}RocksDB 的读放大是 LSM 树的固有代价。L0 层文件之间可能有键范围重叠,需要查多个文件。Bloom Filter 可以大幅减少无效查找,但不能完全消除。
四、TiKV:分布式 LSM + Raft
4.1 架构总览
TiKV 是 PingCAP 开发的分布式键值存储引擎,底层使用 RocksDB,上层通过 Raft 共识协议实现多副本一致性,是 TiDB 的存储层。
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 → value4.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_mbFROM information_schema.tikv_region_statusGROUP BY store_id;
-- 查看写入延迟SELECT instance, SUM(rate) AS write_rateFROM information_schema.tikv_store_statusGROUP 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 向量化执行TiKV 的读路径比单机 RocksDB 复杂得多。即使数据在本地,也需要经过 Raft 层的一致性检查。Follower Read 虽然可以分担 Leader 压力,但需要额外的 Read Index 协商。
五、三大引擎全面对比
5.1 架构对比
| 维度 | InnoDB | RocksDB | TiKV |
|---|---|---|---|
| 数据结构 | B+ 树 | LSM 树 | LSM 树 + Raft |
| 部署模式 | 单机嵌入式 | 单机嵌入式 | 分布式集群 |
| 索引类型 | 聚簇索引 + 二级索引 | 仅主键索引 | 仅主键索引 |
| 事务支持 | ACID(2PL + MVCC) | 基础事务(WriteBatch) | 分布式 ACID(2PC + MVCC) |
| 并发控制 | 行锁 + MVCC | 乐观并发 / CAS | MVCC + Per-Key Lock |
| 副本机制 | 主从复制(异步/半同步) | 无内置副本 | Raft 多副本 |
| 分区 | 无内置分区 | 无内置分区 | Region 自动分裂 |
| 语言 | C++ | C++ | Rust |
5.2 性能对比
| 性能指标 | InnoDB | RocksDB | TiKV |
|---|---|---|---|
| 随机写 QPS | 10K-30K | 100K-500K | 50K-100K |
| 随机读 QPS | 50K-100K | 30K-80K | 20K-50K |
| 范围扫描 | 最优 | 中等 | 中等 |
| 写入延迟 P99 | 2-10ms | 0.5-3ms | 5-20ms |
| 读取延迟 P99 | 0.5-5ms | 1-10ms | 3-15ms |
| 写放大 | 2-4x | 10-30x | 15-40x |
| 读放大 | 3-5x | 10-100x | 15-120x |
| 空间放大 | 1.5-2x | 10-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';| 对比维度 | InnoDB | MyRocks |
|---|---|---|
| 存储空间 | 1x | 0.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 查询走 TiKVSELECT * FROM orders WHERE id = 1001;
-- OLAP 查询走 TiFlashSELECT user_id, SUM(amount)FROM ordersWHERE created_at >= '2026-01-01'GROUP BY user_id;6.3 选型决策树
七、总结
| 维度 | InnoDB | RocksDB | TiKV |
|---|---|---|---|
| 最佳场景 | 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 |
| 分布式 | 否 | 否 | 是 |
| 压缩率 | 一般 | 优秀 | 优秀 |
| 运维复杂度 | 低 | 中 | 高 |
选型没有银弹。InnoDB 适合传统 OLTP,RocksDB 适合写入密集的嵌入式场景,TiKV 适合需要分布式一致性的场景。理解 RUM 猜想,明确你的优先级——读取、更新还是内存——才能做出正确的选择。
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






