
【C三方組件】RocksDB寫多讀少的嵌入式 KV【摘要】RocksDB 是 Facebook 從 LevelDB 演化出的嵌入式持久 KV用 LSM-tree 把寫路徑變成順序追加先寫內(nèi)存 memtable 與 WAL再批量落成不可變 SST后臺 compaction 分層歸并。本篇講 Put / Get / Status 的日常、WriteBatch 千條成批的實測吞吐差、迭代器范圍掃描、快照一致性讀、Column Family 的命名空間與「重開必須帶全」的坑以及 write_buffer_size 一檔入門調(diào)參與 GetProperty 觀測末尾對照 SQLite 與 LMDB給出寫多讀少場景的選型邊界?!娟P鍵詞】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數(shù)字隨機器與磁盤而變1. What為寫而生的 KV 引擎RocksDB 是一個嵌入式持久化鍵值庫鍵值皆字節(jié)串rocksdb::Slice按字典序組織單個進程直接打開本地目錄使用——沒有服務進程數(shù)據(jù)庫就是一個文件夾。它 2012 年從 Google 的 LevelDB fork 而來FacebookMeta為服務端負載持續(xù)改造十年多線程 compaction、Column Family、速率限制、備份、事務……今天的用戶名單里有 TiKV、MyRocks、Flink 的 state backend——**「寫多讀少、數(shù)據(jù)量大、要持久」**是它們的共同點。與第 33 篇 SQLite 的根本分野在寫路徑的物理形態(tài)。SQLite 是 B-tree 頁式存儲一條 UPDATE 意味著在文件中間某頁就地改寫——隨機寫。RocksDB 是 LSM-treeLog-Structured Merge-tree寫入只追加到內(nèi)存 memtable 和磁盤 WALmemtable 寫滿后一次性順序刷成不可變的 SST 文件后臺 compaction 再把多層 SST 歸并壓縮。磁盤最擅長的順序寫被放到主路隨機寫被趕到后臺——這就是「寫多讀少」的物理來源。代價也長在同一處讀要跨 memtable 多層 SST 查找讀放大刪除只是寫墓碑、空間要等 compaction 才回收空間放大compaction 占用后臺 CPU 與 IO寫放大與延遲抖動。三個放大是 LSM 的三角形約束調(diào)參第 8 節(jié)本質是在三角里挪位置。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 數(shù)據(jù)庫沒有表、沒有查詢計劃值是一塊不透明的字節(jié)——需要結構化查詢回到第 33 篇需要把 JSON/protobuf 自己塞進 value。2. Why自研持久 KV 的深水區(qū)「寫個 map 存硬盤」的樸素版本兩天就能跑起來然后深水區(qū)一個接一個崩潰安全要求每次修改先寫日志再改內(nèi)存結構WALfsync 時機錯一步就丟數(shù)據(jù)或寫壞文件memtable 滿了要刷盤刷盤期間新的寫去哪不可變 memtable 輪換磁盤文件不可變更新與刪除只能靠后臺歸并compaction回收空間而 compaction 的層級策略、觸發(fā)時機、限速、與前臺 IO 的隔離每一項都是論文級的調(diào)優(yōu)對象范圍掃描要跨 memtable 與多層 SST 歸并出一致視圖并發(fā)要處理多線程讀寫同一 memtable 的正確性。RocksDB 把這些全部做好了而且是在 Meta 的生產(chǎn)規(guī)模上錘出來的。選它的賬本很簡單一顆經(jīng)過十年生產(chǎn)驗證的存儲引擎樹換來你完全不用發(fā)明 compaction。成本同樣明確編譯時間本篇示例在 Windows 上從源碼全量編譯一次MSVC Ninja -j6 實測約 68 分鐘、二進制代價靜態(tài)庫本體數(shù)百 MB好在鏈接器只抽取用到的符號——示例程序鏈接后僅 5.5 MB、以及不得不學的調(diào)參詞匯表。3. How接入與運行vcpkgvcpkg install rocksdbCMake 走find_package(RocksDB CONFIG REQUIRED)目標是rocksdb::rocksdb源碼GitHub release 下載解壓add_subdirectory編靜態(tài)庫。示例工程的CMakeLists.txt預設了一組「最小可用」開關——WITH_SNAPPY / WITH_ZLIB / WITH_LZ4 / WITH_ZSTD / WITH_BZ2全關壓縮可后補不影響 API、WITH_TESTS / WITH_TOOLS全關、ROCKSDB_BUILD_SHARED OFF只編靜態(tài)庫。?? 接入前先看語言門檻RocksDB 11.x 要求 C20頭文件里已經(jīng)用上默認比較運算符等特性源碼默認CMAKE_CXX_STANDARD 20官方要求 GCC ≥ 11 / Clang ≥ 10。項目還鎖在 C17 時有兩個選擇降級用 RocksDB 8.x / 9.xC17 時代版本API 兼容本篇示例或把它隔離在單獨的靜態(tài)庫里、邊界只暴露 C 接口。本系列其它示例保持 C17本篇示例單獨用 C20——配套 CMakeLists 里兩個目標各釘各的標準。完整示例 rocksdb_demo.cpp 一個 main 串七節(jié)基礎讀寫、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()分類分支。兩個習慣要養(yǎng)成Get查無此鍵不是錯誤IsNotFound()是與ok()平級的正常分支不檢查 Status 的調(diào)用等于把 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 條/秒差三個數(shù)量級因為 SQLite 每條 COMMIT 都在等 fsync而 LSM 把落盤推遲給了后臺。數(shù)量級結論成批寫入值得「逐條 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不查就把錯誤當「掃完了」??煺战o「一邊寫一邊讀」的場景一個固定時刻的一致性視圖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——它釘住了那一刻的全部數(shù)據(jù)版本不釋放會擋住 compaction 刪除舊文件是磁盤空間泄漏的常見來源。這根 「用完歸還」的弦與迭代器、Column Family 句柄第 7 節(jié)是同一根。7. Column Family一個庫里的多張「邏輯表」Column FamilyCF是 RocksDB 的「命名空間」所有 KV 默認住在default新建 CF 得到獨立的 memtable、compaction 策略與參數(shù)——可以給熱數(shù)據(jù)配大內(nèi)存、給索引數(shù)據(jù)開布隆過濾器、給日志數(shù)據(jù)配 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數(shù)據(jù)仍在本篇最大的坑在重開打開數(shù)據(jù)庫時必須先ListColumnFamilies列全、把每個 CF 的描述傳給DB::Open——漏列一個Open 直接失敗“You have to open all column families”。這是新庫最常見的一類「升級后打不開」事故加了一個 CF忘了把重開路徑上的列表從硬編碼換成動態(tài)列舉。關閉順序同樣有講究先歸還全部 CF 句柄再關 DB。8. 調(diào)參入門與運行時觀測Options有上百個旋鈕入門只需要盯住寫路徑這一組參數(shù)默認作用write_buffer_size64 MB單個 memtable 上限越大 flush 越稀、SST 越大max_write_buffer_number2memtable 槽位寫快于 flush 時要加max_background_jobs2flush / compaction 后臺線程數(shù)compressionSnappy*各層壓縮空間與 CPU 的交換*壓縮默認 Snappy但編譯時未啟用 snappy 時靜默退化為不壓縮示例工程即如此——空間敏感的項目要顯式接上壓縮依賴再確認Options::compression生效。直覺模型寫太快 → flush 跟不上 →write_buffer_size與槽數(shù)加倍compaction 跟不上 → 后臺任務數(shù)加倍。示例第 7 節(jié)演示了這些設置。調(diào)參不是玄學開工而是先觀測再動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 萬條吞吐數(shù)據(jù)加幾個演示鍵。注意它是估計值——墓碑與覆蓋未即時扣除拿它做監(jiān)控趨勢可以做精確計數(shù)不行。9. 使用邊界與選型不適合 RocksDB 的信號讀遠多于寫、要求穩(wěn)定點查延遲LSM 層級查找與 compaction 抖動天然不利于此全庫順序掃描是主負載每次都要歸并全部 SST不想引入調(diào)參與運維心智三個放大需要長期盯著數(shù)據(jù)庫必須放在網(wǎng)絡文件系統(tǒng)上NFS / SMB 不支持與 SQLite 的 WAL 同一物理原因。以及一條工程判斷數(shù)據(jù)量長不大 1 GB、又是結構化查詢需求SQLite 一把梭更省——不必為 10 萬條配置文件上 compaction。嵌入式存儲三路線對照本篇為其中一極SQLite33RocksDB本篇LMDB35模型SQL / B-treeKV / LSM-treeKV / B-tree mmap寫吞吐中單寫者高低寫者串行點查延遲穩(wěn)定有層級開銷極穩(wěn)空間回收即時等 compaction即時調(diào)參負擔小大極小需求信號要 SQL / 關系寫多讀少、量大讀多寫少、極簡一句話選型要 SQL 選 SQLite要寫吞吐選 RocksDB要極簡穩(wěn)定讀選 LMDB——第 35 篇從 mmap 路線再會?!碴P聯(lián)〕LSM-tree 的分層歸并、RocksDB 的讀寫路徑剖析屬設計內(nèi)幕后續(xù)在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數(shù)字隨機器與磁盤而變。