好的,以下是为您整理的文章内容,您可以自行将之前讨论的代码填充进去。
Spring Boot 集成 RocksDB 实战:打造高性能 KV 存储加速层
一、为什么选择 RocksDB?
在现代微服务架构中,PostgreSQL 等关系型数据库承担着核心的事务处理职责,但当面对海量高频读写 、毫秒级低延迟查询等场景时,传统的磁盘型关系数据库往往力不从心。
RocksDB 是 Facebook 基于 Google LevelDB 开发的高性能嵌入式 KV 存储引擎,具有以下显著优势:
- 极致写入性能:LSM-Tree 架构,顺序写入,吞吐量远超 B+Tree 结构
- 极低读取延迟:配合布隆过滤器,点查询可达微秒级
- 零网络开销:嵌入式运行在应用进程内,无额外网络往返
- 高度可定制:支持多种压缩算法、列族管理、增量备份
将 RocksDB 作为 PostgreSQL 的热数据缓存层 或冷数据归档层,可以形成互补协同的存储架构:PostgreSQL 负责复杂事务与关系查询,RocksDB 负责高频低延迟的 KV 读写。
二、架构设计:冷热分离与查询加速
2.1 整体架构
本方案采用模式一:冷热数据分离与查询加速架构,核心设计如下:
- PostgreSQL:作为主数据源,处理所有事务性写入和复杂关系查询
- RocksDB:作为本地嵌入式缓存层,承载海量热点数据的极速读写
- Spring Boot:作为应用层,统一调度双端存储,对上层业务透明
2.2 数据流设计
写入流程:
- 业务请求到达 Service 层
- 先写入 PostgreSQL 确保持久化
- 同步写入 RocksDB 作为缓存(失败不影响主流程)
- 可选:通过 Debezium + Kafka 异步同步,进一步解耦
读取流程:
- 业务请求到达 Service 层
- 优先从 RocksDB 查询(命中则直接返回,延迟 < 1ms)
- 未命中则回源 PostgreSQL
- 异步将结果回填到 RocksDB(缓存预热)
删除流程:
- 先删除 PostgreSQL 中的数据
- 再删除 RocksDB 中的缓存(带重试机制)
三、核心实现
3.1 项目依赖
本文使用 Hutool 替代 Jackson 进行 JSON 序列化,API 更简洁,无需处理受检异常。
主要依赖包括:
rocksdbjni:RocksDB Java 绑定hutool-all:JSON 工具及通用工具包spring-retry:提供重试模板支持
3.2 配置层:RocksDB 实例管理
配置类是 RocksDB 集成的基础,核心职责包括:
- 加载 RocksDB 本地库
- 配置数据库选项(内存、并发、布隆过滤器等)
- 以单例模式管理 RocksDB 实例生命周期
- 支持默认数据目录 :若配置文件中未指定
rocksdb.data-dir,自动使用/data/app
关键技术点 :使用 Spring 的 @Value 注解的默认值语法 ${rocksdb.data-dir:/data/app},实现"有配置则用配置,无配置则用默认"的优雅降级。
3.3 服务层:RocksDB 操作封装
RocksDBService 封装了所有基础 CRUD 操作,对外提供统一接口:
- 字符串级别操作 :
put、get、delete、exists - 对象级别操作 :
putObject、getObject,内部自动完成 JSON 序列化与反序列化 - 批量操作 :
batchPut、batchDelete,支持批量写入和删除
设计要点:
get方法在出错时返回Optional.empty(),不抛异常,保证读路径的高可用putObject和getObject使用 Hutool 的JSONUtil,一行代码完成对象与 JSON 的转换- 所有 RocksDB 原生对象(如
WriteBatch、WriteOptions)均使用 try-with-resources 确保正确释放
3.4 工具层:JSON 序列化与重试机制
JSON 工具类 :基于 Hutool 的 JSONUtil 封装静态方法,替代传统的 ObjectMapper。相比 Jackson,Hutool 无需构造实例,无受检异常,代码更简洁。
重试工具类 :基于 Spring Retry 的 RetryTemplate,封装了 RocksDB 操作的重试逻辑:
- 默认重试 3 次,间隔 100ms 固定退避
- 自定义
RocksDBOperation函数式接口,允许抛出RocksDBException受检异常 - 提供无返回值和有返回值两种重载方法
3.5 业务层:UserService 完整示例
业务层是架构的核心调度者,负责编排 PostgreSQL 和 RocksDB 的读写顺序。
写入示例:
- 先通过 JPA Repository 写入 PostgreSQL
- 再调用
rocksDBService.putObject()写入缓存 - 缓存写入失败仅记录日志,不影响主事务
查询示例:
- 优先调用
rocksDBService.getObject()从缓存读取 - 缓存命中则直接返回
- 缓存未命中则回源
userRepository.findById() - 使用
CompletableFuture.runAsync()异步回填缓存
删除示例:
- 先调用
userRepository.deleteById()删除主数据 - 再通过
RocksDBRetryUtil.executeWithRetry()删除缓存 - 缓存删除失败时自动重试 3 次
四、关键技术细节
4.1 默认配置路径
Spring 占位符 ${rocksdb.data-dir:/data/app} 的解析机制:
| 配置情况 | 实际使用路径 |
|---|---|
配置文件未定义 rocksdb.data-dir |
/data/app |
配置文件定义为 /custom/path |
/custom/path |
| 配置文件定义为空值 | /data/app |
4.2 异步缓存回填
查询未命中时,使用 CompletableFuture.runAsync() 异步回填缓存,避免缓存未命中时的额外延迟,保证读路径的响应速度。
4.3 缓存一致性策略
本方案采用 Cache-Aside Pattern(旁路缓存) 模式:
- 写入:先写数据库,再写缓存(或删除缓存)
- 读取:先读缓存,未命中则读数据库并回填
- 删除:先删数据库,再删缓存
对于强一致性场景,建议配合 Debezium CDC 实现异步同步,确保最终一致性。
4.4 重试与容错
RocksDB 写入/删除失败时,通过 Spring Retry 自动重试 3 次(间隔 100ms)。若重试全部失败,记录错误日志并可由监控系统告警,或发往死信队列等待补偿处理。
五、性能优化建议
- 布隆过滤器:点查询场景下启用布隆过滤器,可大幅减少不必要的磁盘 I/O
- 合理设置缓存大小 :根据服务器内存设置
BlockBasedTableConfig的 LRU 缓存大小 - 批量写入 :对大批量数据使用
WriteBatch批量提交,减少写放大 - 列族管理:按业务模块划分列族,实现粒度更细的资源隔离
- 定期 Compaction:根据业务负载配置合适的 Compaction 策略,平衡读放大和写放大
- 监控指标 :导出 RocksDB 的
statistics和perf_context,接入 Prometheus 监控
六、总结
本文介绍了如何在 Spring Boot 项目中集成 RocksDB,构建 PostgreSQL + RocksDB 的协同存储架构。通过配置层、服务层、工具层、业务层四层封装,实现了:
- ✅ 零侵入的业务代码(对上层透明)
- ✅ 毫秒级热点数据查询
- ✅ 自动序列化与反序列化(基于 Hutool)
- ✅ 自动重试与容错机制
- ✅ 默认配置路径的优雅降级
这种架构适用于高并发查询、热点数据缓存、冷热数据分离等多种场景,是提升系统性能的有效手段。
代码如下:
xml
<properties>
<retry.version>2.0.12</retry.version>
<hutool.version>5.8.44</hutool.version>
<rocksdb.version>10.10.1.1</rocksdb.version>
</properties>
<dependencies>
<!-- Spring Retry -->
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>${retry.version}</version>
</dependency>
<dependency>
<groupId>org.rocksdb</groupId>
<artifactId>rocksdbjni</artifactId>
<version>${rocksdb.version}</version>
</dependency>
<!-- json模块 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-json</artifactId>
<version>${hutool.version}</version>
</dependency>
</dependencies>
java
package com.cqcloud.platform.common.matter.config;
import com.cqcloud.platform.common.matter.utils.RocksDBService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import java.io.File;
import org.rocksdb.*;
/**
* @author weimeilayer@gmail.com ✨
* @date 💓💕 2024年3月4日 🐬🐇 💓💕
*/
@Slf4j
@Configuration
public class RocksDBConfig {
/**
* 配置项:rocksdb.data-dir
* 如果 application.yml 中未配置,则使用默认值 /data/app
*/
@Value("${rocksdb.data-dir:/data/app}")
private String dataDir;
@Bean
public RocksDB rocksDB() {
try {
// 加载本地库
RocksDB.loadLibrary();
// 确保目录存在
File dbDir = new File(dataDir);
if (!dbDir.exists()) {
boolean created = dbDir.mkdirs();
if (!created) {
throw new RuntimeException("无法创建 RocksDB 数据目录: " + dataDir);
}
log.info("已创建 RocksDB 数据目录: {}", dataDir);
}
// 配置选项
try (Options options = new Options()) {
options.setCreateIfMissing(true);
options.setCreateMissingColumnFamilies(true);
options.setMaxBackgroundJobs(4);
options.setMaxOpenFiles(-1);
options.setAllowConcurrentMemtableWrite(true);
// 布隆过滤器
BlockBasedTableConfig tableConfig = new BlockBasedTableConfig();
tableConfig.setFilterPolicy(new BloomFilter(10, false));
tableConfig.setWholeKeyFiltering(true);
tableConfig.setCacheIndexAndFilterBlocks(true);
options.setTableFormatConfig(tableConfig);
log.info("正在打开 RocksDB,数据目录: {}", dataDir);
RocksDB db = RocksDB.open(options, dataDir);
log.info("RocksDB 启动成功");
return db;
}
} catch (RocksDBException e) {
log.error("RocksDB 初始化失败", e);
throw new RuntimeException("RocksDB 初始化失败", e);
}
}
@Bean
public RocksDBService rocksDBService(RocksDB rocksDB) {
return new RocksDBService(rocksDB);
}
}
java
package com.cqcloud.platform.common.matter.utils;
import cn.hutool.json.JSONUtil;
/**
* JSON 序列化工具类(基于 Hutool)
* @author weimeilayer@gmail.com ✨
* @date 💓💕 2024年3月4日 🐬🐇 💓💕
*/
public class RocksDBJsonUtil {
/**
* 对象转 JSON 字符串
*/
public static String toJson(Object obj) {
return JSONUtil.toJsonStr(obj);
}
/**
* JSON 字符串转对象
*/
public static <T> T fromJson(String json, Class<T> clazz) {
return JSONUtil.toBean(json, clazz);
}
/**
* JSON 字符串转对象(带默认值,解析失败返回默认对象)
*/
public static <T> T fromJsonOrDefault(String json, Class<T> clazz, T defaultValue) {
try {
return JSONUtil.toBean(json, clazz);
} catch (Exception e) {
return defaultValue;
}
}
/**
* 格式化输出(用于日志打印)
*/
public static String toJsonPretty(Object obj) {
return JSONUtil.toJsonPrettyStr(obj);
}
}
java
package com.cqcloud.platform.common.matter.utils;
import lombok.extern.slf4j.Slf4j;
import org.rocksdb.RocksDBException;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.backoff.FixedBackOffPolicy;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;
import java.util.Collections;
/**
* RocksDB 重试工具类
* @author weimeilayer@gmail.com ✨
* @date 💓💕 2024年3月4日 🐬🐇 💓💕
*/
@Slf4j
public class RocksDBRetryUtil {
private static final RetryTemplate RETRY_TEMPLATE = new RetryTemplate();
static {
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(
3,
Collections.singletonMap(RocksDBException.class, true)
);
RETRY_TEMPLATE.setRetryPolicy(retryPolicy);
FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
backOffPolicy.setBackOffPeriod(100);
RETRY_TEMPLATE.setBackOffPolicy(backOffPolicy);
}
/**
* 自定义函数式接口:允许抛出 RocksDBException
*/
@FunctionalInterface
public interface RocksDBOperation {
void execute() throws RocksDBException;
}
/**
* 执行普通 Runnable(不需要处理受检异常的场景)
*/
public static void executeWithRetry(Runnable operation, String key) {
try {
RETRY_TEMPLATE.execute((RetryCallback<Void, RocksDBException>) context -> {
operation.run();
return null;
});
} catch (RocksDBException e) {
log.error("RocksDB 操作失败,已重试3次,key: {}", key, e);
}
}
/**
* 执行 RocksDB 专用操作(需要抛出 RocksDBException 的场景)
*/
public static void executeWithRetry(RocksDBOperation operation, String key) {
try {
RETRY_TEMPLATE.execute((RetryCallback<Void, RocksDBException>) context -> {
operation.execute();
return null;
});
} catch (RocksDBException e) {
log.error("RocksDB 操作失败,已重试3次,key: {}", key, e);
}
}
}
java
package com.cqcloud.platform.common.matter.utils;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.rocksdb.RocksDB;
import org.rocksdb.RocksDBException;
import org.rocksdb.WriteBatch;
import org.rocksdb.WriteOptions;
import java.nio.ByteBuffer;
import java.util.Map;
import java.util.Optional;
/**
* RocksDB 基础操作服务
* @author weimeilayer@gmail.com ✨
* @date 💓💕 2024年3月4日 🐬🐇 💓💕
*/
@Slf4j
public class RocksDBService {
private final RocksDB rocksDB;
public RocksDBService(RocksDB rocksDB) {
this.rocksDB = rocksDB;
}
// ==================== 字符串操作 ====================
public void put(String key, String value) throws RocksDBException {
try {
rocksDB.put(key.getBytes(), value.getBytes());
log.debug("RocksDB put 成功: key={}", key);
} catch (RocksDBException e) {
log.error("RocksDB put 失败: key={}", key, e);
throw e;
}
}
public Optional<String> get(String key) {
try {
byte[] value = rocksDB.get(key.getBytes());
return value != null ? Optional.of(new String(value)) : Optional.empty();
} catch (RocksDBException e) {
log.error("RocksDB get 失败: key={}", key, e);
return Optional.empty();
}
}
public void delete(String key) throws RocksDBException {
try {
rocksDB.delete(key.getBytes());
log.debug("RocksDB delete 成功: key={}", key);
} catch (RocksDBException e) {
log.error("RocksDB delete 失败: key={}", key, e);
throw e;
}
}
public boolean exists(String key) {
// 最简单的用法,只传入 key
return rocksDB.keyMayExist(ByteBuffer.wrap(key.getBytes()));
}
// ==================== 批量操作 ====================
public void batchPut(Map<String, String> keyValues) throws RocksDBException {
try (WriteBatch batch = new WriteBatch();
WriteOptions writeOptions = new WriteOptions()) {
writeOptions.setSync(false);
for (Map.Entry<String, String> entry : keyValues.entrySet()) {
batch.put(entry.getKey().getBytes(), entry.getValue().getBytes());
}
rocksDB.write(writeOptions, batch);
log.info("RocksDB 批量写入成功,数量: {}", keyValues.size());
}
}
public void batchDelete(String... keys) throws RocksDBException {
try (WriteBatch batch = new WriteBatch();
WriteOptions writeOptions = new WriteOptions()) {
for (String key : keys) {
batch.delete(key.getBytes());
}
rocksDB.write(writeOptions, batch);
log.info("RocksDB 批量删除成功,数量: {}", keys.length);
}
}
// ==================== 对象操作(使用 Hutool JSON) ====================
/**
* 直接存储 Java 对象(自动转 JSON)
*/
public void putObject(String key, Object obj) throws RocksDBException {
String json = JSONUtil.toJsonStr(obj);
put(key, json);
}
/**
* 读取并自动反序列化为指定类型
*/
public <T> Optional<T> getObject(String key, Class<T> clazz) {
Optional<String> json = get(key);
if (json.isPresent()) {
try {
return Optional.of(JSONUtil.toBean(json.get(), clazz));
} catch (Exception e) {
log.error("JSON 反序列化失败: key={}, type={}", key, clazz.getName(), e);
}
}
return Optional.empty();
}
}
调用案例
java
package com.cqcloud.platform.common.matter.utils;
import cn.hutool.json.JSONUtil;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.rocksdb.RocksDBException;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
/**
* 调用案例
* @author weimeilayer@gmail.com ✨
* @date 💓💕 2024年3月4日 🐬🐇 💓💕
*/
@Slf4j
@AllArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final RocksDBService rocksDBService;
private static final String KEY_PREFIX = "user:";
private String buildKey(Long id) {
return KEY_PREFIX + id;
}
// ==================== 写入 ====================
@Transactional
public SysUser save(SysUser user) {
// 1. 写入 PostgreSQL
SysUser saved = userRepository.save(user);
// 2. 同步到 RocksDB(使用 Hutool 序列化)
String key = buildKey(saved.getId());
try {
String json = JSONUtil.toJsonStr(saved); // ← Hutool 序列化
rocksDBService.put(key, json);
} catch (Exception e) {
log.error("同步到 RocksDB 失败: key={}", key, e);
}
return saved;
}
// ==================== 使用 putObject 的简化写入 ====================
@Transactional
public SysUser saveV2(SysUser user) {
SysUser saved = userRepository.save(user);
String key = buildKey(saved.getId());
try {
rocksDBService.putObject(key, saved); // ← 直接存对象
} catch (Exception e) {
log.error("同步到 RocksDB 失败: key={}", key, e);
}
return saved;
}
// ==================== 查询(优先缓存,未命中回源) ====================
public Optional<SysUser> findById(Long id) {
String key = buildKey(id);
// 1. 优先从 RocksDB 查询(方式一:手动反序列化)
try {
Optional<String> json = rocksDBService.get(key);
if (json.isPresent()) {
SysUser user = JSONUtil.toBean(json.get(), SysUser.class); // ← Hutool 反序列化
return Optional.of(user);
}
} catch (Exception e) {
log.warn("RocksDB 查询失败,回退到 PostgreSQL: key={}", key, e);
}
// 2. 回源 PostgreSQL
Optional<SysUser> userOpt = userRepository.findById(id);
// 3. 异步回填缓存
userOpt.ifPresent(user ->
CompletableFuture.runAsync(() -> {
try {
String json = JSONUtil.toJsonStr(user); // ← Hutool 序列化
rocksDBService.put(key, json);
} catch (Exception ex) {
log.error("异步回填缓存失败: key={}", key, ex);
}
})
);
return userOpt;
}
// ==================== 使用 getObject 的简化查询 ====================
public Optional<SysUser> findByIdV2(Long id) {
String key = buildKey(id);
// 直接取对象
Optional<SysUser> cached = rocksDBService.getObject(key, SysUser.class);
if (cached.isPresent()) {
return cached;
}
// 回源并回填
Optional<SysUser> fromDb = userRepository.findById(id);
fromDb.ifPresent(user ->
CompletableFuture.runAsync(() -> {
try {
rocksDBService.putObject(key, user);
} catch (Exception e) {
log.error("回填失败: key={}", key, e);
}
})
);
return fromDb;
}
// ==================== 删除 ====================
@Transactional
public void deleteById(Long id) {
String key = buildKey(id);
userRepository.deleteById(id);
RocksDBRetryUtil.executeWithRetry(
(Runnable) () -> {
try {
rocksDBService.delete(key);
} catch (RocksDBException e) {
throw new RuntimeException(e);
}
},
key
);
}
}