ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

【C++三方组件】RocksDB:写多读少的嵌入式 KV

【C++三方组件】RocksDB:写多读少的嵌入式 KV 【C三方组件】RocksDB写多读少的嵌入式 KV【摘要】RocksDB 是 Facebook 从 LevelDB 演化出的嵌入式持久 KV用 LSM-tree 把写路径变成顺序追加先写内存 memtable 与 WAL再批量落成不可变 SST后台 compaction 分层归并。本篇讲 Put / Get / Status 的日常、WriteBatch 千条成批的实测吞吐差、迭代器范围扫描、快照一致性读、Column Family 的命名空间与「重开必须带全」的坑以及 write_buffer_size 一档入门调参与 GetProperty 观测末尾对照 SQLite 与 LMDB给出写多读少场景的选型边界。【关键词】RocksDB、LSM-tree、嵌入式 KV、WriteBatch、Column Family、compaction【版本基准】RocksDB 11.8.1双许可GPLv2 / Apache-2.0 二选一示例 C20——11.x 起要求 C20GCC ≥ 11 / Clang ≥ 10 / MSVC v143 及以上文中输出与吞吐均为本机实测MSVC v145VS2026Windows数字随机器与磁盘而变1. What为写而生的 KV 引擎RocksDB 是一个嵌入式持久化键值库键值皆字节串rocksdb::Slice按字典序组织单个进程直接打开本地目录使用——没有服务进程数据库就是一个文件夹。它 2012 年从 Google 的 LevelDB fork 而来FacebookMeta为服务端负载持续改造十年多线程 compaction、Column Family、速率限制、备份、事务……今天的用户名单里有 TiKV、MyRocks、Flink 的 state backend——**「写多读少、数据量大、要持久」**是它们的共同点。与第 33 篇 SQLite 的根本分野在写路径的物理形态。SQLite 是 B-tree 页式存储一条 UPDATE 意味着在文件中间某页就地改写——随机写。RocksDB 是 LSM-treeLog-Structured Merge-tree写入只追加到内存 memtable 和磁盘 WALmemtable 写满后一次性顺序刷成不可变的 SST 文件后台 compaction 再把多层 SST 归并压缩。磁盘最擅长的顺序写被放到主路随机写被赶到后台——这就是「写多读少」的物理来源。代价也长在同一处读要跨 memtable 多层 SST 查找读放大删除只是写墓碑、空间要等 compaction 才回收空间放大compaction 占用后台 CPU 与 IO写放大与延迟抖动。三个放大是 LSM 的三角形约束调参第 8 节本质是在三角里挪位置。API 面很窄全是rocksdb::Status通道std::unique_ptrrocksdb::DBdb;rocksdb::DB::Open(Options,path,db);// 打开11.x 出参是 unique_ptrdb-Put(wo,key,value);// 写db-Get(ro,key,value);// 读NotFound 是分支不是错误db-Delete(wo,key);// 删写墓碑db-Write(wo,batch);// 批量原子写db-NewIterator(ro);// 有序扫描db-GetSnapshot()/ReleaseSnapshot();// 快照db-CreateColumnFamily(...)// 列族从旧教程迁移的读者会撞到的第一个变化11.x 的DB::Open出参是std::unique_ptrDB*10.3 弃用、11.0 移除了DB**版本——所有权从此由智能指针表达delete db的手工纪律退场Column Family 句柄仍是裸指针用db-DestroyColumnFamilyHandle(h)归还。注意它不是SQL 数据库没有表、没有查询计划值是一块不透明的字节——需要结构化查询回到第 33 篇需要把 JSON/protobuf 自己塞进 value。2. Why自研持久 KV 的深水区「写个 map 存硬盘」的朴素版本两天就能跑起来然后深水区一个接一个崩溃安全要求每次修改先写日志再改内存结构WALfsync 时机错一步就丢数据或写坏文件memtable 满了要刷盘刷盘期间新的写去哪不可变 memtable 轮换磁盘文件不可变更新与删除只能靠后台归并compaction回收空间而 compaction 的层级策略、触发时机、限速、与前台 IO 的隔离每一项都是论文级的调优对象范围扫描要跨 memtable 与多层 SST 归并出一致视图并发要处理多线程读写同一 memtable 的正确性。RocksDB 把这些全部做好了而且是在 Meta 的生产规模上锤出来的。选它的账本很简单一颗经过十年生产验证的存储引擎树换来你完全不用发明 compaction。成本同样明确编译时间本篇示例在 Windows 上从源码全量编译一次MSVC Ninja -j6 实测约 68 分钟、二进制代价静态库本体数百 MB好在链接器只抽取用到的符号——示例程序链接后仅 5.5 MB、以及不得不学的调参词汇表。3. How接入与运行vcpkgvcpkg install rocksdbCMake 走find_package(RocksDB CONFIG REQUIRED)目标是rocksdb::rocksdb源码GitHub release 下载解压add_subdirectory编静态库。示例工程的CMakeLists.txt预设了一组「最小可用」开关——WITH_SNAPPY / WITH_ZLIB / WITH_LZ4 / WITH_ZSTD / WITH_BZ2全关压缩可后补不影响 API、WITH_TESTS / WITH_TOOLS全关、ROCKSDB_BUILD_SHARED OFF只编静态库。⚠️ 接入前先看语言门槛RocksDB 11.x 要求 C20头文件里已经用上默认比较运算符等特性源码默认CMAKE_CXX_STANDARD 20官方要求 GCC ≥ 11 / Clang ≥ 10。项目还锁在 C17 时有两个选择降级用 RocksDB 8.x / 9.xC17 时代版本API 兼容本篇示例或把它隔离在单独的静态库里、边界只暴露 C 接口。本系列其它示例保持 C17本篇示例单独用 C20——配套 CMakeLists 里两个目标各钉各的标准。完整示例 rocksdb_demo.cpp 一个 main 串七节基础读写、WriteBatch、迭代器、快照、吞吐实测、Column Family、运行时观测。构建与运行入口见 配套说明。4. 基础读写Status 是唯一通道rocksdb::Status sdb-Put(wo,name,rocksdb);if(!s.ok()){/* s.ToString() 看详情 */}sdb-Get(ro,name,value);// 命中sdb-Get(ro,missing,value);// s.IsNotFound() truesdb-Delete(wo,temp);RocksDB 没有异常、没有 errno——一切成败都装在返回的Status里ok() / IsNotFound() / IsCorruption() / IsIOError()分类分支。两个习惯要养成Get查无此键不是错误IsNotFound()是与ok()平级的正常分支不检查 Status 的调用等于把 IO 错误当成功示例的check()小包装即为此。WriteOptions最要紧的一个开关是sync默认 falsefalse 时写只进 WAL 缓冲不强制落盘——进程崩溃不丢机器断电可能丢最近的写true 则每笔写都 fsync持久但慢。这对应 SQLite 的synchronous语义默认档位同样是「快而够用」。5. WriteBatch一批写原子生效rocksdb::WriteBatch batch;for(inti0;i1000;i)batch.Put(key_of(i),value);batch.Delete(old);db-Write(wo,batch);// 一批一次 WAL 追加Batch 把 N 次独立的 WAL 追加与锁竞争合成一次还附带原子性要么全部可见要么全不可见崩溃恢复时按整条 WAL 记录重放。实测吞吐差10 万条key 12 B / value 100 B本机 NVMe逐条 Put : 788.3 ms12.7 万条/秒 千条成批 : 33.8 ms296.1 万条/秒约 23 倍但差距的来源与第 33 篇那次不同RocksDB 逐条Put默认不 fsyncWAL 只进缓冲batch 省掉的是每条一次的锁获取、序列化与 WAL 记录封装。把两组实测放一起看更有意思——同为「逐条写」本篇 12.7 万条/秒第 33 篇 SQLite autocommit 约 108 条/秒差三个数量级因为 SQLite 每条 COMMIT 都在等 fsync而 LSM 把落盘推迟给了后台。数量级结论成批写入值得「逐条 Put」在 RocksDB 上不是灾难在 SQLite autocommit 上才是。6. 迭代器与快照读侧的两件工具迭代器是有序扫描的正门——Seek到第一个大于等于目标的位置Next一路向后rocksdb::Iterator*itdb-NewIterator(ro);it-Seek(batch:2);for(;it-Valid();it-Next())// it-key() / it-value() 是 Slice用 ToString()deleteit;// 迭代器要手动归还两个纪律迭代器用完必须 delete否则挡住底层文件回收遍历结束查一下it-status().ok()——中途 IO 错误会让Valid()提前变 false不查就把错误当「扫完了」。快照给「一边写一边读」的场景一个固定时刻的一致性视图constrocksdb::Snapshot*snapdb-GetSnapshot();db-Put(wo,counter,200);// 快照之后的写rocksdb::ReadOptions ro_snap;ro_snap.snapshotsnap;db-Get(ro_snap,counter,v);// 仍是 100db-ReleaseSnapshot(snap);实测输出快照读 100当前读 200。⚠️ 快照用完必须 Release——它钉住了那一刻的全部数据版本不释放会挡住 compaction 删除旧文件是磁盘空间泄漏的常见来源。这根 「用完归还」的弦与迭代器、Column Family 句柄第 7 节是同一根。7. Column Family一个库里的多张「逻辑表」Column FamilyCF是 RocksDB 的「命名空间」所有 KV 默认住在default新建 CF 得到独立的 memtable、compaction 策略与参数——可以给热数据配大内存、给索引数据开布隆过滤器、给日志数据配 TTL而它们共享一个 WAL 与一份打开的文件句柄。db-CreateColumnFamily(cf_opts,users,users);db-Put(wo,users,alice,1);// 写进 users 列族db-Get(ro,users,alice,v);db-DestroyColumnFamilyHandle(users);// 句柄要手动归还db.reset();// unique_ptr 关库实测输出cf[users].alice 1cf[orders].o-1001 alice 重开前列出 CFdefault users orders 重开后 cf[users].alice 1数据仍在本篇最大的坑在重开打开数据库时必须先ListColumnFamilies列全、把每个 CF 的描述传给DB::Open——漏列一个Open 直接失败“You have to open all column families”。这是新库最常见的一类「升级后打不开」事故加了一个 CF忘了把重开路径上的列表从硬编码换成动态列举。关闭顺序同样有讲究先归还全部 CF 句柄再关 DB。8. 调参入门与运行时观测Options有上百个旋钮入门只需要盯住写路径这一组参数默认作用write_buffer_size64 MB单个 memtable 上限越大 flush 越稀、SST 越大max_write_buffer_number2memtable 槽位写快于 flush 时要加max_background_jobs2flush / compaction 后台线程数compressionSnappy*各层压缩空间与 CPU 的交换*压缩默认 Snappy但编译时未启用 snappy 时静默退化为不压缩示例工程即如此——空间敏感的项目要显式接上压缩依赖再确认Options::compression生效。直觉模型写太快 → flush 跟不上 →write_buffer_size与槽数加倍compaction 跟不上 → 后台任务数加倍。示例第 7 节演示了这些设置。调参不是玄学开工而是先观测再动db-GetIntProperty(rocksdb.estimate-num-keys,keys);// 活键估计db-GetProperty(rocksdb.stats,stats);// 各层文件/读写放大实测rocksdb.stats摘录写入 20 万条后** Compaction Stats [default] ** Level Files Size Score Read(GB) Rn(GB) Rnp1(GB) Write(GB) ... --------------------------------------------------------------------同时estimate-num-keys实测为 200005default 列族 20 万条吞吐数据加几个演示键。注意它是估计值——墓碑与覆盖未即时扣除拿它做监控趋势可以做精确计数不行。9. 使用边界与选型不适合 RocksDB 的信号读远多于写、要求稳定点查延迟LSM 层级查找与 compaction 抖动天然不利于此全库顺序扫描是主负载每次都要归并全部 SST不想引入调参与运维心智三个放大需要长期盯着数据库必须放在网络文件系统上NFS / SMB 不支持与 SQLite 的 WAL 同一物理原因。以及一条工程判断数据量长不大 1 GB、又是结构化查询需求SQLite 一把梭更省——不必为 10 万条配置文件上 compaction。嵌入式存储三路线对照本篇为其中一极SQLite33RocksDB本篇LMDB35模型SQL / B-treeKV / LSM-treeKV / B-tree mmap写吞吐中单写者高低写者串行点查延迟稳定有层级开销极稳空间回收即时等 compaction即时调参负担小大极小需求信号要 SQL / 关系写多读少、量大读多写少、极简一句话选型要 SQL 选 SQLite要写吞吐选 RocksDB要极简稳定读选 LMDB——第 35 篇从 mmap 路线再会。〔关联〕LSM-tree 的分层归并、RocksDB 的读写路径剖析属设计内幕后续在cpp-source-reading专栏展开LSM vs B-tree 的复杂度分析是面试高频见cpp-interview-faq。10. 参考资料官方文档 github.com/facebook/rocksdb/wikiBasic Operations、Column Families、RocksDB Tuning Guide 三篇与本篇对应HISTORY.md从 LevelDB fork 以来的演进时间线完整构建与运行入口examples/33-34/README上一篇第 33 篇SQLite——同一「嵌入式存储」赛道上 SQL / B-tree 路线的对照极。参考facebook/rocksdb v11.8.1双许可 GPLv2 / Apache-2.0。本篇全部输出与吞吐逐条 Put 与 WriteBatch 对比、CF 重开、快照读、stats 摘录均为本机实测MSVC v145VS2026WindowsNVMe SSD数字随机器与磁盘而变。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表