Java 操作 RocksDB
Java 操作 RocksDB
GitHub
1. Maven 依赖
<dependency><groupId>org.rocksdb</groupId><artifactId>rocksdbjni</artifactId><version>6.28.2</version></dependency>2. 加载 native 库
publicclassRocksDBExample{publicstaticvoidmain(String[]args){// 加载 RocksDB native 库(只需全局加载一次)RocksDB.loadLibrary();// ... 使用 RocksDB}}3. 基础 CRUD
importorg.rocksdb.*;importjava.nio.charset.StandardCharsets;importjava.io.File;publicclassBasicCrudExample{publicstaticvoidmain(String[]args){// 1. 加载 native 库RocksDB.loadLibrary();// 2. 配置选项Optionsoptions=newOptions();options.setCreateIfMissing(true);// 数据库不存在时自动创建StringdbPath="./testdb";// 删除旧的 LOCK 文件(如果存在)// 启动时清理可能导致崩溃的遗留锁文件FilelockFile=newFile(dbpath+File.separator+"LOCK");if(lockFile.exists()){lockFile.delete();}try{// 3. 打开数据库// RocksDB 会尝试获取 LOCK 文件的独占锁RocksDBdb=RocksDB.open(options,dbPath);// ==================== 写入操作 ====================Stringkey1="user:1001";byte[]value1="Hello RocksDB".getBytes();db.put(key1.getBytes(StandardCharsets.UTF_8),value1);// 带 WriteOptions 的写入WriteOptionswriteOpts=newWriteOptions();writeOpts.setSync(true);// 同步写入,保证数据持久化db.put(writeOpts,key1.getBytes(),"Sync Write".getBytes());// ==================== 读取操作 ====================// 简单读取byte[]result=db.get(key1.getBytes());if(result!=null){System.out.println("读取结果: "+newString(result));}// 带 ReadOptions 的读取ReadOptionsreadOpts=newReadOptions();readOpts.setVerifyChecksums(true);// 验证校验和result=db.get(readOpts,key1.getBytes());// ==================== 删除操作 ====================db.delete(key1.getBytes());// 或带 WriteOptionsdb.delete(writeOpts,key1.getBytes());// ==================== 检查是否存在 ====================byte[][]keys={"key1".getBytes(),"key2".getBytes(),"key3".getBytes()};List<byte[]>exists=db.multiGetAsList(Arrays.asList(keys));for(inti=0;i<exists.size();i++){System.out.println("key"+(i+1)+" 存在: "+(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. RocksObject
RocksObject是所有 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++ 内存泄漏ReadOptionsopts=newReadOptions();// 使用后不管,等 GC 回收是不靠谱的// 正确做法:使用 try-with-resources,确保 close() 一定被调用try(ReadOptionsopts=newReadOptions()){// 使用 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++ 内存(因为内部会有其他机制释放)
典型的所有权转移场景:
RocksDBdb=RocksDB.open(options,dbpath);try(ReadOptionsreadOpts=newReadOptions()){// db.getSnapshot() 返回一个 Snapshot 对象Snapshotsnapshot=db.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~4
7. 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)让路
noSlowdown
setNoSlowdown确实是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 Read)
RocksDBdb=RocksDB.open(options,dbpath);try(ReadOptionsreadOpts=newReadOptions()){Snapshotsnapshot=db.getSnapshot();try{// 获取当前的快照readOpts.setSnapshot(snapshot);// 在此执行一系列读取,保证看到同一时间点的数据db.get(readOpts,key1);db.get(readOpts,key2);}finally{// 无论是否异常,都必须释放快照,否则会阻塞底层 Compactiondb.releaseSnapshot(snapshot);}}8.2. 缓存与 I/O 策略
这部分决定了读取操作如何与 Block Cache 交互,以及是否真正触发底层文件 I/O
setFillCache(boolean fillCache)
- 作用:读取过程中从磁盘加载的数据块是否填充到 Block Cache 中
true(默认):会被填充到 Block Cache 中,供后续请求复用false:不会将读到的数据块放入缓存
- 场景:当进行大范围的全表扫描且确信这些数据近期不会再次被访问时,设置为
false可以避免污染 Block Cache,从而保护热数据不被淘汰
setVerifyChecksums(boolean verifyChecksums)
- 作用:是否对从磁盘读取的数据块校验和
true(默认):开启校验,会额外消耗一点 CPU,但能确保数据未损坏false:跳过校验,读取速度更快
- 场景:对数据一致性要求极高时保持
true;在进行内部诊断或确信底层存储绝对可靠且对性能极其敏感时,可设为false
setReadTier(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。如果为
true,RocksDB 可能会在后台预取数据,当实际需要该数据时,它可能已经在内存中了,从而减少同步等待的延迟。默认 false - 场景:在支持异步 I/O 的文件系统上,对于延迟敏感的随机读场景有显著提升
- 注意:Java 层面只是开关,实际性能提升依赖底层操作系统的 AIO 支持(如 Linux 的 io_uring 或 POSIX AIO)
8.3. 布隆过滤器与索引优化
这部分主要针对Get和点查操作,优化查找路径
setTotalOrderSeek(boolean totalOrderSeek)
- 作用:决定迭代器如何定位数据
false(默认):迭代器可以利用前缀提取器和布隆过滤器来快速定位,跳过不包含该前缀的 SST 文件和数据块true:禁用前缀裁剪,迭代器会按照全局字典序遍历所有数据
- 场景:当你配置了前缀布隆过滤器,但某次查询的前缀与配置的不一致,或者需要跨前缀进行范围扫描时,必须设为
true,否则可能漏掉数据
setTailing(boolean tailing)
- 作用:是否建一个“尾随迭代器”
- true:迭代过程中,如果后台有新的数据写入或 Compaction 发生,迭代器能看到最新的数据
- false(默认):遍历期间看到的数据版本是固定的,不受其他线程新增/修改的影响
- 场景:类似消息队列的消费场景,客户端需要不断轮询新写入的数据,而不想被快照卡住
- 注意:与
snapshot互斥,设置了tailing=true就不应该再设置snapshot
setPinData(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)
- 作用:自适应预取
true:RocksDB 会根据迭代器的访问模式自动调整预取大小。如果检测到顺序读取,会逐渐增大预取;如果检测到随机读取,则关闭预取false(默认):预取行为完全由setReadaheadSize决定
- 场景:不确定访问模式,或希望系统自动优化顺序扫描性能时开启
setAutoPrefixMode(boolean autoPrefixMode)
- 作用:是否开启自动前缀模式。当迭代器进行顺序扫描时,自动退化为
total_order_seek以保证正确性;当进行点查或小范围查询时,自动利用前缀布隆过滤器加速true:自动切换,RocksDB 自动判断当前操作是点查还是范围扫描。点查时自动利用前缀布隆过滤器加速;范围扫描时自动退化为totalOrderSeek的全局排序模式以保证正确性false(默认):完全依赖开发者手动设置totalOrderSeek来决定是否使用前缀优化
- 场景:在配置了前缀布隆过滤器的情况下,既想利用过滤器加速点查,又想保证范围扫描不丢数据,可开启此选项替代手动设置
total_order_seek
8.5. 限流与资源控制
setRateLimiter(RateLimiter rateLimiter)
作用:覆盖全局的 RateLimiter,为本次读取单独限流
场景:某些低优先级的后台分析任务读取数据时,为了不影响前端高优先级业务的读写,可以给后台任务配置一个带宽较小的独立 RateLimiter
setValueSizeSoftLimit(long valueSizeSoftLimit)
- 作用:读取 Value 的大小软限制。如果读取到的 Value 超过此大小,会抛异常。默认
0(无限制) - 场景:防止意外读取超大 Value 导致 JVM 堆内存 OOM
9. CompressionType
- NO_COMPRESSION
- 作用:数据不经过任何压缩直接写入磁盘
- 特点:写入速度最快,完全不消耗 CPU;但磁盘占用极大
- 使用场景:数据本身就是不可压缩的(如已加密、已压缩的视频/图片),或者 CPU 资源极其紧张,且磁盘空间无限的场景
- DISABLE_COMPRESSION_OPTION
- 作用:用来告诉 RocksDB:此层级禁用压缩,效果等同于
NO_COMPRESSION,但在代码语义和使用场景上有明确的区别
- 作用:用来告诉 RocksDB:此层级禁用压缩,效果等同于
- SNAPPY_COMPRESSION
- 作用:使用 Snappy 算法压缩
- 特点:Google 开源,RocksDB 的默认压缩算法。它的设计目标是极高的压缩和解压速度,而不是最大的压缩率。压缩率通常在
40%-50%左右 - 使用场景:绝大多数在线业务的首选。在 CPU 消耗和磁盘占用之间取得了最佳平衡
- ZLIB_COMPRESSION
- 作用:使用 Zlib (Deflate) 算法压缩
- 特点:属于传统压缩库,压缩率极高(通常比 Snappy 高 20%-30%),但压缩和解压速度极慢,非常消耗 CPU
- 使用场景:冷数据归档、对读取延迟要求不高、但极度追求磁盘空间节省的场景。通常只配置在最底层(Level Max)
- BZLIB2_COMPRESSION
- 作用:使用 bzip2 算法压缩
- 特点:与 Zlib 类似,压缩率比 Zlib 略高一点点,但速度通常比 Zlib 还要慢,CPU 消耗极大
- 使用场景:极少使用,除非特定数据类型下 bzip2 表现出远超 Zlib 的压缩率
- LZ4_COMPRESSION
- 作用:使用 LZ4 算法压缩
- 特点:Snappy 的最强替代品。压缩率与 Snappy 基本相当(有时略好),但压缩和解压速度比 Snappy 更快
- 使用场景:如果你发现 CPU 是瓶颈,且使用 Snappy 导致写入/读取延迟较高,强烈建议切换到 LZ4。目前很多现代系统已默认用 LZ4 取代 Snappy
- LZ4HC_COMPRESSION
- 作用:使用 LZ4 High Compression 算法
- 特点:LZ4 的高压缩率变种。解压速度和 LZ4 一样快,但压缩速度极慢(消耗大量 CPU 换取更高的压缩率)
- 使用场景:写入不频繁,但希望读取时解压快、同时磁盘占用比 LZ4 更小的场景
- ZSTD_COMPRESSION
- 作用:使用 Zstandard 算法压缩(Facebook 开源)
- 特点:综合表现最佳的算法。压缩率接近 Zlib,但压缩和解压速度远超 Zlib(通常快数倍)。解压速度受数据大小影响很小,非常稳定
- 使用场景:强烈推荐!如果你的 RocksDB 版本支持(较新版本均支持),建议替代 Zlib 用于底层冷数据压缩,甚至在某些场景下替代 Snappy/LZ4
- ZSTD_NOT_FINAL_COMPRESSION
- 作用:Zstandard 算法的早期测试版本
- 特点:在 Zstd 还未发布正式版 1.0 之前的过渡类型
- 使用场景:绝对不要在新项目中使用,仅用于兼容极老旧的数据库文件
- XPRESS_COMPRESSION
- 请无视它
