Java 操作 RocksDB Java 操作 RocksDBGitHub1. Maven 依赖dependencygroupIdorg.rocksdb/groupIdartifactIdrocksdbjni/artifactIdversion6.28.2/version/dependency2. 加载 native 库publicclassRocksDBExample{publicstaticvoidmain(String[]args){// 加载 RocksDB native 库只需全局加载一次RocksDB.loadLibrary();// ... 使用 RocksDB}}3. 基础 CRUDimportorg.rocksdb.*;importjava.nio.charset.StandardCharsets;importjava.io.File;publicclassBasicCrudExample{publicstaticvoidmain(String[]args){// 1. 加载 native 库RocksDB.loadLibrary();// 2. 配置选项OptionsoptionsnewOptions();options.setCreateIfMissing(true);// 数据库不存在时自动创建StringdbPath./testdb;// 删除旧的 LOCK 文件如果存在// 启动时清理可能导致崩溃的遗留锁文件FilelockFilenewFile(dbpathFile.separatorLOCK);if(lockFile.exists()){lockFile.delete();}try{// 3. 打开数据库// RocksDB 会尝试获取 LOCK 文件的独占锁RocksDBdbRocksDB.open(options,dbPath);// 写入操作 Stringkey1user:1001;byte[]value1Hello RocksDB.getBytes();db.put(key1.getBytes(StandardCharsets.UTF_8),value1);// 带 WriteOptions 的写入WriteOptionswriteOptsnewWriteOptions();writeOpts.setSync(true);// 同步写入保证数据持久化db.put(writeOpts,key1.getBytes(),Sync Write.getBytes());// 读取操作 // 简单读取byte[]resultdb.get(key1.getBytes());if(result!null){System.out.println(读取结果: newString(result));}// 带 ReadOptions 的读取ReadOptionsreadOptsnewReadOptions();readOpts.setVerifyChecksums(true);// 验证校验和resultdb.get(readOpts,key1.getBytes());// 删除操作 db.delete(key1.getBytes());// 或带 WriteOptionsdb.delete(writeOpts,key1.getBytes());// 检查是否存在 byte[][]keys{key1.getBytes(),key2.getBytes(),key3.getBytes()};Listbyte[]existsdb.multiGetAsList(Arrays.asList(keys));for(inti0;iexists.size();i){System.out.println(key(i1) 存在: (exists.get(i)!null));}// 关闭数据库 db.close();}catch(RocksDBExceptione){System.err.println(RocksDB 操作失败: e.getMessage());e.printStackTrace();}}}4. 锁机制加锁当调用RocksDB.open()时RocksDB 会尝试获取LOCK文件的独占锁启动成功如果获取锁成功说明没有其他进程占用该数据库数据库正常打开启动失败如果另一个进程已经持有了该锁当前进程的RocksDB.open()会抛出RocksDBException报错信息通常类似于lock hold by current process, acquire time ...或Resource temporarily unavailable从而拒绝打开数据库释放锁当调用RocksDB.close()正常关闭数据库时锁文件会被释放独占锁解除但默认情况下LOCK文件本身不会被删除它会留在磁盘上等待下一个进程重新加锁5. RocksObjectRocksObject是所有 RocksDB 核心 Java 类的基类。它代表了被 Java 封装的底层 C 原生对象5.1. 核心作用管理堆外内存普通的 Java 对象存在于 JVM 的堆内存中由垃圾回收器GC自动管理生命周期RocksDB 的底层是用 C 编写的C 分配的内存属于堆外内存JVM 的 GC 管不到这块内存RocksObject的设计就是为了解决这个跨语言问题持有句柄RocksObject内部维护了一个long类型的成员变量nativeHandle_。这个值其实就是 C 对象的内存指针地址。Java 层面通过这个地址来调用底层的 C 方法管理生命周期它实现了AutoCloseable接口提供了一种机制让 Java 开发者能够显式地通知 C 释放内存5.2. 必须调用 close()因为 JVM 的 GC 只能回收 Java 的RocksObject对象本身极小的包装对象但无法回收它指向的 C 内存。如果不手动释放C 的内存就会泄漏RocksObject的close()方法内部会调用一个 C 函数去释放nativeHandle_指向的 C 对象最佳实践始终使用 try-with-resources// 错误做法依赖 GC极易导致底层 C 内存泄漏ReadOptionsoptsnewReadOptions();// 使用后不管等 GC 回收是不靠谱的// 正确做法使用 try-with-resources确保 close() 一定被调用try(ReadOptionsoptsnewReadOptions()){// 使用 opts 执行操作}// 离开大括号后自动调用 opts.close()释放 C 内存5.3. 哪些类继承了 RocksObject在 RocksDB 的 Java API 中几乎所有涉及底层状态或资源的类都是RocksObject的子类。包括但不限于数据库核心RocksDB,ColumnFamilyHandle选项与配置Options,DBOptions,ColumnFamilyOptions,ReadOptions,WriteOptions迭代器RocksIterator快照Snapshot缓存与限流Cache,RateLimiter,WriteBufferManager过滤器与压缩BloomFilter,CompressionOptions当你看到这些类时脑海中应该立刻弹出一个警告用完必须close()5.4. isOwningHandle()为了避免同一块 C 内存被多次释放RocksObject内部有一个布尔标志位owningHandle_默认情况当你通过new关键字创建一个对象如new ReadOptions()时Java 层面拥有这个 C 对象的所有权isOwningHandle() true此时调用close()会真正释放 C 内存转移所有权有些时候Java 对象只是指向了 RocksDB 内部管理的 C 对象并不拥有它。此时isOwningHandle() false调用close()不会释放 C 内存因为内部会有其他机制释放典型的所有权转移场景RocksDBdbRocksDB.open(options,dbpath);try(ReadOptionsreadOptsnewReadOptions()){// db.getSnapshot() 返回一个 Snapshot 对象Snapshotsnapshotdb.getSnapshot();try{readOpts.setSnapshot(snapshot);// 执行读取...}finally{// 注意这里不能调用 snapshot.close()// 因为 snapshot 是从 db 中获取的它的生命周期由 db 管理// 如果强行 close()可能会破坏 db 内部的快照状态db.releaseSnapshot(snapshot);// 正确的释放方式}}6. Options配置项太多了介绍一些常用的setCreateIfMissing(boolean)如果数据库目录不存在是否自动创建setInfoLogLevel(InfoLogLevel)日志级别。DEBUG级别会输出大量 Compaction 信息通常设为INFO或WARNsetStatsDumpPeriodSec(int)每隔多少秒将数据库统计信息 dump 到日志中调优时非常有用setWriteBufferSize(long)单个 MemTable 的大小。当 MemTable 写满后会变为 Immutable MemTable并触发后台 Flush。建议调大通常设为 64MB ~ 256MB减少 Flush 频率setTargetFileSizeBase(long)用来控制SST 文件大小的核心参数setMaxWriteBufferNumber(int)MemTable 的最大数量包括活跃的和 Immutable 的。当 Immutable MemTable 达到该数量仍未 Flush 完成时写操作会被阻塞Write Stall。建议设为 3~5setMinWriteBufferNumberToMerge(int)将多少个 Immutable MemTable 合并后再 Flush 到磁盘。设为 2 可以减少 SST 文件数量但可能增加读放大setCompressionType(CompressionType)设置压缩策略强烈建议设置setBottommostCompressionType(CompressionType)最底层Level 最大的 SST 文件压缩方式。最底层体积最大setMaxBytesForLevelBase(long)Level 1 的总大小上限。Level 2 的大小上限是Level 1 * MaxBytesForLevelMultiplier。如果 Write Buffer 很大建议调大此值通常设为WriteBufferSize * 10setMaxBytesForLevelMultiplier(double)每个 Level 大小是上一级的多少倍setLevelCompactionDynamicLevelBytes(boolean)开启后RocksDB 会动态调整每层的大小目标而不是固定的 10MB - 100MB - 1GB能更有效地控制空间放大setUseFsync(boolean)使用fsync而不是fdatasync来保证数据持久化。fdatasync不刷元数据性能更好通常保持 false 即可setAllowMmapReads(boolean)是否允许使用内存映射文件读取 SST。在 Linux 环境下如果数据集小于可用内存开启此选项可以极大提升读性能但可能导致内存不可控setBlockCache(BlockCache)极其重要SST 文件的 Block 缓存直接影响读性能setBlockSize(long)SST 文件中 Data Block 的大小。如果经常进行大范围 Scan可以调大到 16KB 或 64KB如果是随机点查4KB 或 8KB 即可setMaxBackgroundJobs(int)后台执行 Flush 和 Compaction 的最大线程数。现代服务器通常有多核建议设为 CPU 核心数的 1/2 或全量如 4~8避免 Compaction 跟不上导致 Write StallsetMaxSubcompactions(int)在 Level 层级做 Compaction 时允许拆分成多少个子任务并行执行。对于大文件合并很有帮助建议设为 2~47. WriteOptions// 建议配置writeOptions.setSync(false);writeOptions.setDisableWAL(false);writeOptions.setLowPri(false);writeOptions.setNoSlowdown(false);writeOptions.setIgnoreMissingColumnFamilies(true);**sync **控制每次写入后是否强制将数据刷到物理磁盘false写入操作会将数据先写入操作系统的页面缓存然后立即返回。这种方式性能极高但如果发生机器断电或操作系统崩溃最后几次写入的数据可能会丢失true写入操作会等待数据被真正写入物理磁盘后才返回。这确保了数据的持久性但性能会急剧下降可能慢几个数量级disableWAL是否禁用 WAL (Write-Ahead Log预写日志)false写入会先记录到 WAL 日志文件然后再写入 MemTable。即使进程崩溃重启后也能通过 WAL 恢复未刷盘的数据true写入操作会绕过 WAL直接写入 MemTable。这能提升写入速度但如果进程崩溃所有还在 MemTable 中未刷盘的数据都会丢失ignoreMissingColumnFamilies当使用WriteBatch进行批量写入并且 batch 中包含了指向不存在的列族Column Family的操作时此选项决定行为false如果 batch 中引用了不存在的列族整个写入操作会失败并抛出RocksDBExceptiontrue忽略那些针对不存在的列族的操作只执行有效的操作noSlowdown当发生写入减速Write Stall时例如Level 0 的 SST 文件太多Compaction 跟不上写入速度此选项控制行为false写入线程会被延迟睡眠以减慢写入速度给 Compaction 追赶的机会true写入线程不会被延迟。如果无法立即完成写入写入操作会立即失败并抛出异常。这适用于需要低延迟、宁愿失败也不愿等待的场景lowPri标记这个写入请求是低优先级的false不会被延迟处理true当数据库繁忙时它会被延迟处理以便为高优先级的写入如 WAL Sync让路noSlowdownsetNoSlowdown确实是WriteOptions里比较难懂的一个配置因为它涉及 RocksDB 内部的限流机制Write Stall为了彻底弄懂我们分三步来理解为什么会慢默认怎么处理NoSlowdown又是怎么处理的第一步为什么写入会突然变慢Write Stall 产生的原因RocksDB 的写入速度极快是因为数据先写内存。但内存不可能无限大写满后必须把数据刷到磁盘上这个动作叫Flush同时磁盘上的文件越来越多需要合并整理这个动作叫Compaction如果你的写入速度太快超过了磁盘 Flush 和 Compaction 的处理能力就会发生“内存积压”内存中的 MemTable 写满了还没来得及刷盘磁盘上 Level 0 层的 SST 文件堆积得越来越多如果不加以控制内存迟早会被撑爆。因此RocksDB 有一套自我保护机制Write Stall写入停顿/限流。当积压超过阈值时RocksDB 会强制让写入线程“睡一会”Sleep等后台的 Flush/Compaction 追上来再允许继续写入第二步setNoSlowdown(false)的行为当noSlowdown false时如果触发了 Write Stall你的写入线程比如执行db.put()或db.write()的线程会被阻塞挂起表现本来put操作只需 1 毫秒突然变成了 50 毫秒甚至几百毫秒好处保证了数据一定能写进去不会丢数据坏处延迟变得不稳定毛刺如果你的接口有严格的超时限制可能会因为底层阻塞导致超时第三步setNoSlowdown(true)的行为如果你设置了noSlowdown true就是在告诉 RocksDB“我绝对不能忍受写入被阻塞等待如果现在写不进去你就直接告诉我失败我绝不等你”表现当 RocksDB 内部积压严重本该发生 Write Stall 时因为设置了NoSlowdown它不会让线程 Sleep而是立即抛出异常RocksDBException状态码为Status.Code.Incomplete或TimedOut好处写入延迟极其稳定永远不会卡顿。put操作要么瞬间成功要么瞬间失败坏处你需要自己处理写入失败的情况比如重试、或者丢弃数据打个比方去餐厅吃饭默认情况 (noSlowdown false)你去餐厅点餐厨师忙不过来了。服务员会让你在座位上等一会等厨师做完之前的菜再给你做。你最终吃到了饭但等了半小时开启noSlowdown true)你去餐厅点餐厨师忙不过来。服务员直接告诉你“现在做不了您换一家吧”你没有等但也饿着肚子写入失败你得自己决定是去下一家重试还是干脆不吃了丢弃8. ReadOptions配置项太多了介绍一些常用的在 RocksDB 中ReadOptions是每次执行读操作如Get、MultiGet、NewIterator等时传入的参数结构体。它控制着读取操作的具体行为包括一致性级别、缓存策略、数据可见性等8.1. 快照一致性这部分决定了读取操作能看到数据库的哪个版本setSnapshot(Snapshot snapshot)作用指定读取操作使用的快照实现一致性读Repeatable ReadRocksDBdbRocksDB.open(options,dbpath);try(ReadOptionsreadOptsnewReadOptions()){Snapshotsnapshotdb.getSnapshot();try{// 获取当前的快照readOpts.setSnapshot(snapshot);// 在此执行一系列读取保证看到同一时间点的数据db.get(readOpts,key1);db.get(readOpts,key2);}finally{// 无论是否异常都必须释放快照否则会阻塞底层 Compactiondb.releaseSnapshot(snapshot);}}8.2. 缓存与 I/O 策略这部分决定了读取操作如何与 Block Cache 交互以及是否真正触发底层文件 I/OsetFillCache(boolean fillCache)作用读取过程中从磁盘加载的数据块是否填充到 Block Cache 中true默认会被填充到 Block Cache 中供后续请求复用false不会将读到的数据块放入缓存场景当进行大范围的全表扫描且确信这些数据近期不会再次被访问时设置为false可以避免污染 Block Cache从而保护热数据不被淘汰setVerifyChecksums(boolean verifyChecksums)作用是否对从磁盘读取的数据块校验和true默认开启校验会额外消耗一点 CPU但能确保数据未损坏false跳过校验读取速度更快场景对数据一致性要求极高时保持true在进行内部诊断或确信底层存储绝对可靠且对性能极其敏感时可设为falsesetReadTier(ReadTier readTier)作用限制读取的层级READ_ALL_TIER默认允许读取所有层级内存 磁盘BLOCK_CACHE_TIER仅从 MemTable 和 Block Cache 读取。如果数据不在内存中不会去读磁盘而是直接抛异常MEMTABLE_TIER只能在 MemTable包括活跃的 Active MemTable 和不可变的 Immutable MemTable中查找PERSISTED_TIER只能从已经持久化到磁盘的 SST 文件中查找数据完全忽略 MemTable 中的数据场景适用于对读取延迟极其敏感、允许偶尔读取不到数据的场景如缓存的旁路读取setAsyncIO(boolean asyncIo)作用是否启用异步 I/O。如果为trueRocksDB 可能会在后台预取数据当实际需要该数据时它可能已经在内存中了从而减少同步等待的延迟。默认 false场景在支持异步 I/O 的文件系统上对于延迟敏感的随机读场景有显著提升注意Java 层面只是开关实际性能提升依赖底层操作系统的 AIO 支持如 Linux 的 io_uring 或 POSIX AIO8.3. 布隆过滤器与索引优化这部分主要针对Get和点查操作优化查找路径setTotalOrderSeek(boolean totalOrderSeek)作用决定迭代器如何定位数据false默认迭代器可以利用前缀提取器和布隆过滤器来快速定位跳过不包含该前缀的 SST 文件和数据块true禁用前缀裁剪迭代器会按照全局字典序遍历所有数据场景当你配置了前缀布隆过滤器但某次查询的前缀与配置的不一致或者需要跨前缀进行范围扫描时必须设为true否则可能漏掉数据setTailing(boolean tailing)作用是否建一个“尾随迭代器”true迭代过程中如果后台有新的数据写入或 Compaction 发生迭代器能看到最新的数据false默认遍历期间看到的数据版本是固定的不受其他线程新增/修改的影响场景类似消息队列的消费场景客户端需要不断轮询新写入的数据而不想被快照卡住注意与snapshot互斥设置了tailingtrue就不应该再设置snapshotsetPinData(boolean pinData)作用是否固定数据在内存中true迭代器访问过的数据块如表索引、数据块会被固定在 Block Cache 中直到迭代器被销毁才释放false默认不固定如果内存紧张随时可以被淘汰换出场景当需要长时间持有迭代器并多次访问相同数据时可以防止数据在迭代期间被换出缓存导致重复 I/O。通常与tailing结合使用8.4. 预取与 I/O 调优setReadaheadSize(long readaheadSize)作用设置预取大小。在扫描或迭代时RocksDB 会按此大小提前从磁盘读取后续数据到内存。默认 0 不预取场景对于大范围的顺序扫描适当增大该值如设为 64KB ~ 4MB可以显著减少 I/O 次数提升吞吐量。对于随机读保持默认的 0 即可setAdaptiveReadahead(boolean adaptiveReadahead)作用自适应预取trueRocksDB 会根据迭代器的访问模式自动调整预取大小。如果检测到顺序读取会逐渐增大预取如果检测到随机读取则关闭预取false默认预取行为完全由setReadaheadSize决定场景不确定访问模式或希望系统自动优化顺序扫描性能时开启setAutoPrefixMode(boolean autoPrefixMode)作用是否开启自动前缀模式。当迭代器进行顺序扫描时自动退化为total_order_seek以保证正确性当进行点查或小范围查询时自动利用前缀布隆过滤器加速true自动切换RocksDB 自动判断当前操作是点查还是范围扫描。点查时自动利用前缀布隆过滤器加速范围扫描时自动退化为totalOrderSeek的全局排序模式以保证正确性false默认完全依赖开发者手动设置totalOrderSeek来决定是否使用前缀优化场景在配置了前缀布隆过滤器的情况下既想利用过滤器加速点查又想保证范围扫描不丢数据可开启此选项替代手动设置total_order_seek8.5. 限流与资源控制setRateLimiter(RateLimiter rateLimiter)作用覆盖全局的 RateLimiter为本次读取单独限流场景某些低优先级的后台分析任务读取数据时为了不影响前端高优先级业务的读写可以给后台任务配置一个带宽较小的独立 RateLimitersetValueSizeSoftLimit(long valueSizeSoftLimit)作用读取 Value 的大小软限制。如果读取到的 Value 超过此大小会抛异常。默认0无限制场景防止意外读取超大 Value 导致 JVM 堆内存 OOM9. CompressionTypeNO_COMPRESSION作用数据不经过任何压缩直接写入磁盘特点写入速度最快完全不消耗 CPU但磁盘占用极大使用场景数据本身就是不可压缩的如已加密、已压缩的视频/图片或者 CPU 资源极其紧张且磁盘空间无限的场景DISABLE_COMPRESSION_OPTION作用用来告诉 RocksDB此层级禁用压缩效果等同于NO_COMPRESSION但在代码语义和使用场景上有明确的区别SNAPPY_COMPRESSION作用使用 Snappy 算法压缩特点Google 开源RocksDB 的默认压缩算法。它的设计目标是极高的压缩和解压速度而不是最大的压缩率。压缩率通常在40%-50%左右使用场景绝大多数在线业务的首选。在 CPU 消耗和磁盘占用之间取得了最佳平衡ZLIB_COMPRESSION作用使用 Zlib (Deflate) 算法压缩特点属于传统压缩库压缩率极高通常比 Snappy 高 20%-30%但压缩和解压速度极慢非常消耗 CPU使用场景冷数据归档、对读取延迟要求不高、但极度追求磁盘空间节省的场景。通常只配置在最底层Level MaxBZLIB2_COMPRESSION作用使用 bzip2 算法压缩特点与 Zlib 类似压缩率比 Zlib 略高一点点但速度通常比 Zlib 还要慢CPU 消耗极大使用场景极少使用除非特定数据类型下 bzip2 表现出远超 Zlib 的压缩率LZ4_COMPRESSION作用使用 LZ4 算法压缩特点Snappy 的最强替代品。压缩率与 Snappy 基本相当有时略好但压缩和解压速度比 Snappy 更快使用场景如果你发现 CPU 是瓶颈且使用 Snappy 导致写入/读取延迟较高强烈建议切换到 LZ4。目前很多现代系统已默认用 LZ4 取代 SnappyLZ4HC_COMPRESSION作用使用 LZ4 High Compression 算法特点LZ4 的高压缩率变种。解压速度和 LZ4 一样快但压缩速度极慢消耗大量 CPU 换取更高的压缩率使用场景写入不频繁但希望读取时解压快、同时磁盘占用比 LZ4 更小的场景ZSTD_COMPRESSION作用使用 Zstandard 算法压缩Facebook 开源特点综合表现最佳的算法。压缩率接近 Zlib但压缩和解压速度远超 Zlib通常快数倍。解压速度受数据大小影响很小非常稳定使用场景强烈推荐如果你的 RocksDB 版本支持较新版本均支持建议替代 Zlib 用于底层冷数据压缩甚至在某些场景下替代 Snappy/LZ4ZSTD_NOT_FINAL_COMPRESSION作用Zstandard 算法的早期测试版本特点在 Zstd 还未发布正式版 1.0 之前的过渡类型使用场景绝对不要在新项目中使用仅用于兼容极老旧的数据库文件XPRESS_COMPRESSION请无视它