
【C三方組件】RocksDB寫多讀少的嵌入式 KV【摘要】RocksDB 是 Facebook 從 LevelDB 演化出的嵌入式持久 KV用 LSM-tree 把寫路徑變成順序追加先寫內(nèi)存 memtable 與 WAL再批量落成不可變 SST后臺(tái) compaction 分層歸并。本篇講 Put / Get / Status 的日常、WriteBatch 千條成批的實(shí)測(cè)吞吐差、迭代器范圍掃描、快照一致性讀、Column Family 的命名空間與「重開(kāi)必須帶全」的坑以及 write_buffer_size 一檔入門調(diào)參與 GetProperty 觀測(cè)末尾對(duì)照 SQLite 與 LMDB給出寫多讀少場(chǎng)景的選型邊界?!娟P(guān)鍵詞】RocksDB、LSM-tree、嵌入式 KV、WriteBatch、Column Family、compaction【版本基準(zhǔn)】RocksDB 11.8.1雙許可GPLv2 / Apache-2.0 二選一示例 C20——11.x 起要求 C20GCC ≥ 11 / Clang ≥ 10 / MSVC v143 及以上文中輸出與吞吐均為本機(jī)實(shí)測(cè)MSVC v145VS2026Windows數(shù)字隨機(jī)器與磁盤而變1. What為寫而生的 KV 引擎RocksDB 是一個(gè)嵌入式持久化鍵值庫(kù)鍵值皆字節(jié)串rocksdb::Slice按字典序組織單個(gè)進(jìn)程直接打開(kāi)本地目錄使用——沒(méi)有服務(wù)進(jìn)程數(shù)據(jù)庫(kù)就是一個(gè)文件夾。它 2012 年從 Google 的 LevelDB fork 而來(lái)FacebookMeta為服務(wù)端負(fù)載持續(xù)改造十年多線程 compaction、Column Family、速率限制、備份、事務(wù)……今天的用戶名單里有 TiKV、MyRocks、Flink 的 state backend——**「寫多讀少、數(shù)據(jù)量大、要持久」**是它們的共同點(diǎn)。與第 33 篇 SQLite 的根本分野在寫路徑的物理形態(tài)。SQLite 是 B-tree 頁(yè)式存儲(chǔ)一條 UPDATE 意味著在文件中間某頁(yè)就地改寫——隨機(jī)寫。RocksDB 是 LSM-treeLog-Structured Merge-tree寫入只追加到內(nèi)存 memtable 和磁盤 WALmemtable 寫滿后一次性順序刷成不可變的 SST 文件后臺(tái) compaction 再把多層 SST 歸并壓縮。磁盤最擅長(zhǎng)的順序?qū)懕环诺街髀冯S機(jī)寫被趕到后臺(tái)——這就是「寫多讀少」的物理來(lái)源。代價(jià)也長(zhǎng)在同一處讀要跨 memtable 多層 SST 查找讀放大刪除只是寫墓碑、空間要等 compaction 才回收空間放大compaction 占用后臺(tái) CPU 與 IO寫放大與延遲抖動(dòng)。三個(gè)放大是 LSM 的三角形約束調(diào)參第 8 節(jié)本質(zhì)是在三角里挪位置。API 面很窄全是rocksdb::Status通道std::unique_ptrrocksdb::DBdb;rocksdb::DB::Open(Options,path,db);// 打開(kāi)11.x 出參是 unique_ptrdb-Put(wo,key,value);// 寫db-Get(ro,key,value);// 讀NotFound 是分支不是錯(cuò)誤db-Delete(wo,key);// 刪寫墓碑db-Write(wo,batch);// 批量原子寫db-NewIterator(ro);// 有序掃描db-GetSnapshot()/ReleaseSnapshot();// 快照db-CreateColumnFamily(...)// 列族從舊教程遷移的讀者會(huì)撞到的第一個(gè)變化11.x 的DB::Open出參是std::unique_ptrDB*10.3 棄用、11.0 移除了DB**版本——所有權(quán)從此由智能指針表達(dá)delete db的手工紀(jì)律退場(chǎng)Column Family 句柄仍是裸指針用db-DestroyColumnFamilyHandle(h)歸還。注意它不是SQL 數(shù)據(jù)庫(kù)沒(méi)有表、沒(méi)有查詢計(jì)劃值是一塊不透明的字節(jié)——需要結(jié)構(gòu)化查詢回到第 33 篇需要把 JSON/protobuf 自己塞進(jìn) value。2. Why自研持久 KV 的深水區(qū)「寫個(gè) map 存硬盤」的樸素版本兩天就能跑起來(lái)然后深水區(qū)一個(gè)接一個(gè)崩潰安全要求每次修改先寫日志再改內(nèi)存結(jié)構(gòu)WALfsync 時(shí)機(jī)錯(cuò)一步就丟數(shù)據(jù)或?qū)憠奈募emtable 滿了要刷盤刷盤期間新的寫去哪不可變 memtable 輪換磁盤文件不可變更新與刪除只能靠后臺(tái)歸并compaction回收空間而 compaction 的層級(jí)策略、觸發(fā)時(shí)機(jī)、限速、與前臺(tái) IO 的隔離每一項(xiàng)都是論文級(jí)的調(diào)優(yōu)對(duì)象范圍掃描要跨 memtable 與多層 SST 歸并出一致視圖并發(fā)要處理多線程讀寫同一 memtable 的正確性。RocksDB 把這些全部做好了而且是在 Meta 的生產(chǎn)規(guī)模上錘出來(lái)的。選它的賬本很簡(jiǎn)單一顆經(jīng)過(guò)十年生產(chǎn)驗(yàn)證的存儲(chǔ)引擎樹(shù)換來(lái)你完全不用發(fā)明 compaction。成本同樣明確編譯時(shí)間本篇示例在 Windows 上從源碼全量編譯一次MSVC Ninja -j6 實(shí)測(cè)約 68 分鐘、二進(jìn)制代價(jià)靜態(tài)庫(kù)本體數(shù)百 MB好在鏈接器只抽取用到的符號(hào)——示例程序鏈接后僅 5.5 MB、以及不得不學(xué)的調(diào)參詞匯表。3. How接入與運(yùn)行vcpkgvcpkg install rocksdbCMake 走find_package(RocksDB CONFIG REQUIRED)目標(biāo)是rocksdb::rocksdb源碼GitHub release 下載解壓add_subdirectory編?kù)o態(tài)庫(kù)。示例工程的CMakeLists.txt預(yù)設(shè)了一組「最小可用」開(kāi)關(guān)——WITH_SNAPPY / WITH_ZLIB / WITH_LZ4 / WITH_ZSTD / WITH_BZ2全關(guān)壓縮可后補(bǔ)不影響 API、WITH_TESTS / WITH_TOOLS全關(guān)、ROCKSDB_BUILD_SHARED OFF只編?kù)o態(tài)庫(kù)。?? 接入前先看語(yǔ)言門檻RocksDB 11.x 要求 C20頭文件里已經(jīng)用上默認(rèn)比較運(yùn)算符等特性源碼默認(rèn)CMAKE_CXX_STANDARD 20官方要求 GCC ≥ 11 / Clang ≥ 10。項(xiàng)目還鎖在 C17 時(shí)有兩個(gè)選擇降級(jí)用 RocksDB 8.x / 9.xC17 時(shí)代版本API 兼容本篇示例或把它隔離在單獨(dú)的靜態(tài)庫(kù)里、邊界只暴露 C 接口。本系列其它示例保持 C17本篇示例單獨(dú)用 C20——配套 CMakeLists 里兩個(gè)目標(biāo)各釘各的標(biāo)準(zhǔn)。完整示例 rocksdb_demo.cpp 一個(gè) main 串七節(jié)基礎(chǔ)讀寫、WriteBatch、迭代器、快照、吞吐實(shí)測(cè)、Column Family、運(yùn)行時(shí)觀測(cè)。構(gòu)建與運(yùn)行入口見(jiàn) 配套說(shuō)明。4. 基礎(chǔ)讀寫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 沒(méi)有異常、沒(méi)有 errno——一切成敗都裝在返回的Status里ok() / IsNotFound() / IsCorruption() / IsIOError()分類分支。兩個(gè)習(xí)慣要養(yǎng)成Get查無(wú)此鍵不是錯(cuò)誤IsNotFound()是與ok()平級(jí)的正常分支不檢查 Status 的調(diào)用等于把 IO 錯(cuò)誤當(dāng)成功示例的check()小包裝即為此。WriteOptions最要緊的一個(gè)開(kāi)關(guān)是sync默認(rèn) falsefalse 時(shí)寫只進(jìn) WAL 緩沖不強(qiáng)制落盤——進(jìn)程崩潰不丟機(jī)器斷電可能丟最近的寫true 則每筆寫都 fsync持久但慢。這對(duì)應(yīng) SQLite 的synchronous語(yǔ)義默認(rèn)檔位同樣是「快而夠用」。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 次獨(dú)立的 WAL 追加與鎖競(jìng)爭(zhēng)合成一次還附帶原子性要么全部可見(jiàn)要么全不可見(jiàn)崩潰恢復(fù)時(shí)按整條 WAL 記錄重放。實(shí)測(cè)吞吐差10 萬(wàn)條key 12 B / value 100 B本機(jī) NVMe逐條 Put : 788.3 ms12.7 萬(wàn)條/秒 千條成批 : 33.8 ms296.1 萬(wàn)條/秒約 23 倍但差距的來(lái)源與第 33 篇那次不同RocksDB 逐條Put默認(rèn)不 fsyncWAL 只進(jìn)緩沖batch 省掉的是每條一次的鎖獲取、序列化與 WAL 記錄封裝。把兩組實(shí)測(cè)放一起看更有意思——同為「逐條寫」本篇 12.7 萬(wàn)條/秒第 33 篇 SQLite autocommit 約 108 條/秒差三個(gè)數(shù)量級(jí)因?yàn)?SQLite 每條 COMMIT 都在等 fsync而 LSM 把落盤推遲給了后臺(tái)。數(shù)量級(jí)結(jié)論成批寫入值得「逐條 Put」在 RocksDB 上不是災(zāi)難在 SQLite autocommit 上才是。6. 迭代器與快照讀側(cè)的兩件工具迭代器是有序掃描的正門——Seek到第一個(gè)大于等于目標(biāo)的位置Next一路向后rocksdb::Iterator*itdb-NewIterator(ro);it-Seek(batch:2);for(;it-Valid();it-Next())// it-key() / it-value() 是 Slice用 ToString()deleteit;// 迭代器要手動(dòng)歸還兩個(gè)紀(jì)律迭代器用完必須 delete否則擋住底層文件回收遍歷結(jié)束查一下it-status().ok()——中途 IO 錯(cuò)誤會(huì)讓Valid()提前變 false不查就把錯(cuò)誤當(dāng)「掃完了」??煺战o「一邊寫一邊讀」的場(chǎng)景一個(gè)固定時(shí)刻的一致性視圖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);實(shí)測(cè)輸出快照讀 100當(dāng)前讀 200。?? 快照用完必須 Release——它釘住了那一刻的全部數(shù)據(jù)版本不釋放會(huì)擋住 compaction 刪除舊文件是磁盤空間泄漏的常見(jiàn)來(lái)源。這根 「用完歸還」的弦與迭代器、Column Family 句柄第 7 節(jié)是同一根。7. Column Family一個(gè)庫(kù)里的多張「邏輯表」Column FamilyCF是 RocksDB 的「命名空間」所有 KV 默認(rèn)住在default新建 CF 得到獨(dú)立的 memtable、compaction 策略與參數(shù)——可以給熱數(shù)據(jù)配大內(nèi)存、給索引數(shù)據(jù)開(kāi)布隆過(guò)濾器、給日志數(shù)據(jù)配 TTL而它們共享一個(gè) WAL 與一份打開(kāi)的文件句柄。db-CreateColumnFamily(cf_opts,users,users);db-Put(wo,users,alice,1);// 寫進(jìn) users 列族db-Get(ro,users,alice,v);db-DestroyColumnFamilyHandle(users);// 句柄要手動(dòng)歸還db.reset();// unique_ptr 關(guān)庫(kù)實(shí)測(cè)輸出cf[users].alice 1cf[orders].o-1001 alice 重開(kāi)前列出 CFdefault users orders 重開(kāi)后 cf[users].alice 1數(shù)據(jù)仍在本篇最大的坑在重開(kāi)打開(kāi)數(shù)據(jù)庫(kù)時(shí)必須先ListColumnFamilies列全、把每個(gè) CF 的描述傳給DB::Open——漏列一個(gè)Open 直接失敗“You have to open all column families”。這是新庫(kù)最常見(jiàn)的一類「升級(jí)后打不開(kāi)」事故加了一個(gè) CF忘了把重開(kāi)路徑上的列表從硬編碼換成動(dòng)態(tài)列舉。關(guān)閉順序同樣有講究先歸還全部 CF 句柄再關(guān) DB。8. 調(diào)參入門與運(yùn)行時(shí)觀測(cè)Options有上百個(gè)旋鈕入門只需要盯住寫路徑這一組參數(shù)默認(rèn)作用write_buffer_size64 MB單個(gè) memtable 上限越大 flush 越稀、SST 越大max_write_buffer_number2memtable 槽位寫快于 flush 時(shí)要加max_background_jobs2flush / compaction 后臺(tái)線程數(shù)compressionSnappy*各層壓縮空間與 CPU 的交換*壓縮默認(rèn) Snappy但編譯時(shí)未啟用 snappy 時(shí)靜默退化為不壓縮示例工程即如此——空間敏感的項(xiàng)目要顯式接上壓縮依賴再確認(rèn)Options::compression生效。直覺(jué)模型寫太快 → flush 跟不上 →write_buffer_size與槽數(shù)加倍compaction 跟不上 → 后臺(tái)任務(wù)數(shù)加倍。示例第 7 節(jié)演示了這些設(shè)置。調(diào)參不是玄學(xué)開(kāi)工而是先觀測(cè)再動(dòng)db-GetIntProperty(rocksdb.estimate-num-keys,keys);// 活鍵估計(jì)db-GetProperty(rocksdb.stats,stats);// 各層文件/讀寫放大實(shí)測(cè)rocksdb.stats摘錄寫入 20 萬(wàn)條后** Compaction Stats [default] ** Level Files Size Score Read(GB) Rn(GB) Rnp1(GB) Write(GB) ... --------------------------------------------------------------------同時(shí)estimate-num-keys實(shí)測(cè)為 200005default 列族 20 萬(wàn)條吞吐數(shù)據(jù)加幾個(gè)演示鍵。注意它是估計(jì)值——墓碑與覆蓋未即時(shí)扣除拿它做監(jiān)控趨勢(shì)可以做精確計(jì)數(shù)不行。9. 使用邊界與選型不適合 RocksDB 的信號(hào)讀遠(yuǎn)多于寫、要求穩(wěn)定點(diǎn)查延遲LSM 層級(jí)查找與 compaction 抖動(dòng)天然不利于此全庫(kù)順序掃描是主負(fù)載每次都要?dú)w并全部 SST不想引入調(diào)參與運(yùn)維心智三個(gè)放大需要長(zhǎng)期盯著數(shù)據(jù)庫(kù)必須放在網(wǎng)絡(luò)文件系統(tǒng)上NFS / SMB 不支持與 SQLite 的 WAL 同一物理原因。以及一條工程判斷數(shù)據(jù)量長(zhǎng)不大 1 GB、又是結(jié)構(gòu)化查詢需求SQLite 一把梭更省——不必為 10 萬(wàn)條配置文件上 compaction。嵌入式存儲(chǔ)三路線對(duì)照本篇為其中一極SQLite33RocksDB本篇LMDB35模型SQL / B-treeKV / LSM-treeKV / B-tree mmap寫吞吐中單寫者高低寫者串行點(diǎn)查延遲穩(wěn)定有層級(jí)開(kāi)銷極穩(wěn)空間回收即時(shí)等 compaction即時(shí)調(diào)參負(fù)擔(dān)小大極小需求信號(hào)要 SQL / 關(guān)系寫多讀少、量大讀多寫少、極簡(jiǎn)一句話選型要 SQL 選 SQLite要寫吞吐選 RocksDB要極簡(jiǎn)穩(wěn)定讀選 LMDB——第 35 篇從 mmap 路線再會(huì)?!碴P(guān)聯(lián)〕LSM-tree 的分層歸并、RocksDB 的讀寫路徑剖析屬設(shè)計(jì)內(nèi)幕后續(xù)在cpp-source-reading專欄展開(kāi)LSM vs B-tree 的復(fù)雜度分析是面試高頻見(jiàn)cpp-interview-faq。10. 參考資料官方文檔 github.com/facebook/rocksdb/wikiBasic Operations、Column Families、RocksDB Tuning Guide 三篇與本篇對(duì)應(yīng)HISTORY.md從 LevelDB fork 以來(lái)的演進(jìn)時(shí)間線完整構(gòu)建與運(yùn)行入口examples/33-34/README上一篇第 33 篇SQLite——同一「嵌入式存儲(chǔ)」賽道上 SQL / B-tree 路線的對(duì)照極。參考facebook/rocksdb v11.8.1雙許可 GPLv2 / Apache-2.0。本篇全部輸出與吞吐逐條 Put 與 WriteBatch 對(duì)比、CF 重開(kāi)、快照讀、stats 摘錄均為本機(jī)實(shí)測(cè)MSVC v145VS2026WindowsNVMe SSD數(shù)字隨機(jī)器與磁盤而變。