加群联系作者vx:xiaoda0423
仓库地址:webvueblog.github.io/JavaPlusDoc...
好的,咱们直接讲清楚重点:
在 Apache Cassandra 中,Session
是你跟数据库交互的核心对象 。
它主要负责干两件事:
- 管理连接 :
Session
代表着和 Cassandra 集群 建立好的一组连接(通常是连接池),而不是一个简单的 TCP 连接。
你可以理解成:Session 帮你打通了集群里多个节点之间的路,准备好随时发请求。 - 执行 CQL(Cassandra Query Language)语句 :
通过Session
,你可以执行查询(SELECT
)、插入(INSERT
)、更新(UPDATE
)、删除(DELETE
)等操作。
Session 会帮你把这些 CQL 命令发给合适的节点去处理。
简单来说:
Session 是 Cassandra 客户端和集群之间进行读写交互的通道,且是线程安全的,整个应用通常只创建一个或者少量几个 Session。
举个实际开发里的例子:
scss
// 1. 创建连接集群
CqlSession session = CqlSession.builder()
.withKeyspace("my_keyspace")
.build();
// 2. 用 Session 执行一条查询
ResultSet rs = session.execute("SELECT * FROM users WHERE id = 123");
// 3. 遍历查询结果
for (Row row : rs) {
System.out.println(row.getString("name"));
}
// 4. 用完关闭(重要,避免连接泄漏)
session.close();
这里的
CqlSession
是新版 Java Driver 4.x 之后引入的标准(比老的Session
更强大一些)。
常见认知误区
误区 | 正解 |
---|---|
Session 是单一连接 | ❌ 不是,它内部有连接池,可以高并发请求 |
每次用完都要新建 Session | ❌ 不要,Session 是重量级对象,应该复用 |
Session 只能操作一个 keyspace | ❌ 可以动态切换,也可以一开始就绑定某个 keyspace |
🛠 【Kafka消费者配置流程】详细过程说明
① Spring Boot 项目启动阶段
-
Spring 启动时,扫描到
@Configuration
标注的EventConsumerConfig
类。 -
Spring 自动执行
@Bean
方法:- 调用
batchRecordConsumerFactory()
方法,注册名字叫"BRConsumerFactory"
的 批量消费容器工厂。 - 调用
oneByOneRecordConsumerFactory()
方法,注册名字叫"OBORConsumerFactory"
的 单条消费容器工厂。
- 调用
-
在
getConsumerFactory(true/false)
方法里:- 创建
ConcurrentKafkaListenerContainerFactory
。 - 设置关联的
ConsumerFactory
(封装了 Kafka 连接参数)。 - 配置并发数量(即几条消费线程同时消费)。
- 配置是否是批量消费。
- 配置消费完消息后,手动提交 offset (
AckMode.MANUAL_IMMEDIATE
)。
- 创建
整体链路简化图(文字版)
scss
Spring Boot 启动
↓
加载 EventConsumerConfig
↓
注册 KafkaListenerContainerFactory (批量/单条)
↓
应用内 @KafkaListener 启动监听
↓
Kafka 推送消息
↓
KafkaListener 容器拉取消息
↓
调用 onMessage 处理逻辑
↓
手动提交 offset (ack.acknowledge())
↓
继续拉取下一批消息
Kafka 消费者配置(EventConsumerConfig
)操作流程说明
1. 项目启动时,Spring 加载配置类 EventConsumerConfig
@Configuration
注解告诉 Spring:这是一个配置类,会被容器扫描。- Spring 创建
EventConsumerConfig
实例,并注入需要的配置属性(通过@Value
)。
2. 注入配置属性到对象字段
通过 @Value("${xxx}")
,从配置文件(比如 application.yml
)读取Kafka消费者的基础属性,比如:
- Kafka集群地址 (
kafka.consumer.servers
) - 是否自动提交 (
kafka.consumer.enable.auto.commit
) - 超时时间 (
kafka.consumer.session.timeout
) - 消费者组 ID (
kafka.consumer.group.id
) - 并发线程数 (
kafka.consumer.concurrency
) 等等。
如果某些配置没配,带默认值的 @Value
注解会保证程序不因空值报错。
3. 创建 Kafka 消费者参数 Map (consumerConfigs
方法)
调用 consumerConfigs()
方法时,会组装一份消费者所需的配置参数 Map<String, Object>
。
主要包含:
- 连接Kafka服务器 (
BOOTSTRAP_SERVERS_CONFIG
) - 禁用自动提交offset (
ENABLE_AUTO_COMMIT_CONFIG = false
) - 配置session超时
- 配置key/value反序列化器
- 动态组装消费者GroupId(带主机名后缀)
- 配置拉取最大消息数
- 配置初始offset位置(
auto.offset.reset
)
🔔 特别注意 :
此处强制手动提交offset,保证业务处理完成后再提交,避免消息丢失。
Kafka Producer 数据流程步骤
从应用代码调用 kafkaTemplate.send()
到 Kafka Broker 落盘,整体分成下面 7 个主要步骤:
1. KafkaTemplate 发送消息
- 应用程序调用
kafkaTemplate.send(topic, key, value)
方法发起发送。 KafkaTemplate
封装了 Producer API,异步地把消息交给 Kafka Client。
2. Producer 将消息序列化
KeySerializer
和ValueSerializer
(这里用的是StringSerializer
)把 key 和 value 转换成字节数组(byte[])。- 序列化是必须的,因为网络传输需要字节流。
3. 将消息分配到 Partition
- 根据发送时指定的
key
(如果有),使用 Kafka 的 分区器(Partitioner) 算法计算出具体的 partition。 - 如果没指定 key,则使用轮询或随机分配 partition。
4. 消息写入 RecordAccumulator(本地缓存池)
-
Kafka Producer 不会立刻发送 每一条消息,而是先放到内存中的
RecordAccumulator
缓冲池中。 -
什么时候触发发送?
- 累积到一定
batch.size
大小。 - 或者等待时间超过
linger.ms
。
- 累积到一定
(👉 你的配置:batch.size
、linger.ms
就是在控制这里的行为。)
5. Sender 线程异步发送数据
- Producer 后台有一个 Sender线程 ,专门不断从
RecordAccumulator
拉取数据打包,异步发送到 Kafka Broker。 - 通过 TCP 网络连接(socket)发送 ProduceRequest。
6. Broker 收到请求并应答(ACK)
- Kafka Broker Leader Partition 接收消息。
- 内存中先缓存 ,然后立即返回 ACK 应答给 Producer(不一定等落盘! ,Kafka快的原因之一)。
- 是否等待 ISR 集群同步完副本,取决于
acks
配置(你的代码里目前是默认 acks=1)。
7. Producer Callback 回调处理
- Producer 收到 Broker 的 ACK。
- 如果发送成功,触发成功回调(
onSuccess
)。 - 如果失败(比如网络中断、broker不可用),触发异常回调(
onFailure
),可以重试或记录异常。
Kafka Producer 采用了 "内存批处理 + 异步发送 + 硬件顺序写" 模式,极大提高了消息发送性能与吞吐量。
发送数据(Send Process)
发送一条消息到 Kafka:
csharp
ProducerRecord<String, String> record = new ProducerRecord<>("topic-name", "key", "value");
producer.send(record, (metadata, exception) -> {
if (exception == null) {
System.out.println("发送成功, topic: " + metadata.topic() + ", offset: " + metadata.offset());
} else {
exception.printStackTrace();
}
});
过程细节:
步骤 | 说明 |
---|---|
构造 ProducerRecord |
封装消息主题、key、value 等信息 |
调用 send() 方法 |
异步将消息发送到消息累加器(RecordAccumulator) |
判断是否触发发送 | 满批量(batch.size)或逗留时间(linger.ms)后,真正打包发送 |
选择 Partition | Kafka 内部根据 key 的 hash 算法,或随机分配 Partition |
网络线程 IO | 由 KafkaProducer 的 I/O 线程异步发送数据包到 Kafka Broker |
Broker 处理 | Kafka Broker 持久化写入、返回应答(根据 acks 决定是否需要副本确认) |
回调函数执行 | 成功/失败都会触发回调(Callback)逻辑 |
刷新与关闭(Flush & Close)
保证缓冲区内所有数据都发送出去,并优雅关闭资源:
scss
producer.flush(); // 强制立即发送缓冲区内所有数据
producer.close(); // 关闭生产者,释放连接资源
Java 实操总结要点
- 异步发送,提升吞吐量。
- 批量累加,减少网络请求次数。
- 配置合理 acks、retries,兼顾性能和可靠性。
- 合理使用 callback,捕捉异常与记录日志。
- flush() + close() ,确保优雅退出。
java
// 使用线程池处理异步任务
private static final BlockingQueue<Runnable> blockingQueue = new LinkedBlockingQueue<>();
private static final ExecutorService fixedThreadPool = new ThreadPoolExecutor(
2, 6, 60L, TimeUnit.SECONDS, blockingQueue
);
-
try-catch
范围精简,异常日志更规范(避免只打e.getMessage()
导致漏掉栈信息)。 -
入参判空 (
null
校验) 增强,防止 NPE。 -
JSON转换加上了异常保护。
-
BlockingQueue
泛型加了<Runnable>
,防止编译警告。 -
统一使用
@PostMapping
代替@RequestMapping(method=POST)
,风格更一致。 -
日志参数化 (
logger.info("xxx {}", value)
),性能更优。 -
定义了一个静态常量阻塞队列 ,用来存储等待执行的任务(Runnable) 。
-
是给下面这个线程池:
iniExecutorService fixedThreadPool = new ThreadPoolExecutor(..., blockingQueue);
用的。
-
本质 是作为线程池任务缓冲区 。
当线程池的线程忙不过来时,新来的任务(Runnable)就先塞进
blockingQueue
,排队等执行。
说白了,就是让线程池不丢任务、顺序排队的地方。
线程池 + 阻塞队列配合流程:
顺着你的代码理解是这样:
-
线程池初始化
- 核心线程数:2
- 最大线程数:6
- 队列:
blockingQueue
-
提交任务时(比如异步提交一个 Runnable)
- 如果当前活跃线程数 < 核心线程数 (2个): ➔ 直接新建线程执行任务。
- 如果活跃线程数 ≥ 核心线程数 : ➔ 任务塞到
blockingQueue
里面排队。 - 如果blockingQueue 满了 ,并且活跃线程数 < 最大线程数(6): ➔ 继续扩容线程去处理。
- 如果最大线程数也满了 ,且队列也满了 : ➔ 执行拒绝策略(默认抛异常,可以自定义处理)。
-
线程空闲60秒后 ,如果是非核心线程 ,会被回收销毁。
简单流转:
markdown
提交任务 Runnable
↓
活跃线程数 < 核心线程数(2)?
└→ 是:直接新建线程处理
↓ 否
blockingQueue 队列未满?
└→ 是:入队排队等待线程处理
↓ 否
活跃线程数 < 最大线程数(6)?
└→ 是:再创建新线程处理
↓ 否
执行拒绝策略(抛异常或自定义处理)
数据库示例(以 Cassandra 为例)
表结构(示例) :
假设我们在 Cassandra 中有这样一张表:
arduino
CREATE TABLE IF NOT EXISTS logs (
id text PRIMARY KEY,
partition_key text,
sort_key text,
log_time text,
info text
);
或者业务数据表:
arduino
CREATE TABLE IF NOT EXISTS user_data (
id text PRIMARY KEY,
user_id text,
device_id text,
info text
);
id
:唯一主键(如 MongoDB ObjectId)partition_key, sort_key
:用于分区和排序log_time
:日志时间戳info
:对象数据序列化成的 JSON 字符串
🧱 数据结构设计(详细)
字段 | 类型 | 描述 |
---|---|---|
clientId | varchar | 客户端 ID(设备或租户编号) |
day | varchar | 日期(格式如 20240420) |
dir | varchar | 消息方向(如 "up"=上行, "down"=下行) |
objId | varchar | 对象 ID,自动生成唯一值(MongoDB 风格) |
自动清理过期表 + 自动导出数据备份
-
节省存储成本
-
保证系统长期稳定
-
支持灾备和离线分析需求
目标拆分
1. 定时清理过期表
- 找出超过 N 个月的历史表(比如保留最近 6 个月)
- 自动 DROP TABLE
2. 自动导出备份
- 先把即将删除的表数据导出到文件(比如 JSON 或 CSV)
- 备份到对象存储、NAS,或者直接推到 Kafka 等离线系统
- 确认备份完成后再删除表
总体思路
步骤 | 描述 |
---|---|
1 | 分页查询大表数据(防止一次性读完爆内存) |
2 | 多线程并发导出(加速 IO,提升速率) |
3 | 分批写文件(比如每1000条落一次磁盘) |
4 | 最后合并小文件(optional,根据需要) |
ini
private static final int PAGE_SIZE = 1000; // 每页查1000条
private static final int THREAD_COUNT = 4; // 4个线程并发导出
private void exportTableData(String tableName) {
ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);
String baseDir = "/data/backup/" + tableName;
new File(baseDir).mkdirs();
List<Future<File>> futures = new ArrayList<>();
AtomicInteger fileCounter = new AtomicInteger(0);
try {
Statement stmt = new SimpleStatement(String.format("SELECT * FROM %s.%s;", KEYSPACE, tableName))
.setFetchSize(PAGE_SIZE);
ResultSet resultSet = session.execute(stmt);
Iterator<Row> iterator = resultSet.iterator();
List<Row> batch = new ArrayList<>(PAGE_SIZE);
while (iterator.hasNext()) {
batch.add(iterator.next());
if (batch.size() >= PAGE_SIZE) {
List<Row> batchToExport = new ArrayList<>(batch);
batch.clear();
futures.add(executor.submit(() -> exportBatch(batchToExport, baseDir, fileCounter.incrementAndGet())));
}
}
// 导出最后剩余不足 PAGE_SIZE 的数据
if (!batch.isEmpty()) {
futures.add(executor.submit(() -> exportBatch(batch, baseDir, fileCounter.incrementAndGet())));
}
// 等待所有导出任务完成
for (Future<File> future : futures) {
future.get();
}
System.out.println("[Exporter] Exported table " + tableName + " in " + futures.size() + " parts.");
} catch (Exception e) {
throw new RuntimeException("Batch export failed for " + tableName, e);
} finally {
executor.shutdown();
}
}
private File exportBatch(List<Row> rows, String baseDir, int partNumber) {
File file = new File(baseDir, "part_" + partNumber + ".json");
try (PrintWriter writer = new PrintWriter(file)) {
ObjectMapper objectMapper = new ObjectMapper();
for (Row row : rows) {
Map<String, Object> rowMap = new HashMap<>();
row.getColumnDefinitions().forEach(col -> {
rowMap.put(col.getName(), row.getObject(col.getName()));
});
writer.println(objectMapper.writeValueAsString(rowMap));
}
} catch (IOException e) {
throw new RuntimeException("Failed to export part " + partNumber, e);
}
return file;
}
导出机制总结
点 | 做法 | 作用 |
---|---|---|
分页拉取 | 每次只拉取1000条 | 不会 OOM |
多线程导出 | 4线程并行 | 提升 IO 速度 |
分文件存储 | 每批数据一个小文件 | 易于管理,防止文件过大 |
可横向扩展 | 调整 PAGE_SIZE、线程数 | 灵活适配不同规模数据 |
高并发 + 防爆内存 + 可扩展 + 可落盘备份
1. 高并发(High Concurrency)
目标:
多线程并行导出,提升大表数据备份速度。
具体做法:
技术 | 描述 |
---|---|
ExecutorService 线程池 |
控制固定数量线程同时工作,防止CPU过载 |
每批数据异步提交导出任务 | 每拉取一页数据(如1000行),提交一个异步导出任务 |
Future / CompletableFuture | 异步任务结果收集,统一等待完成 |
写磁盘操作并发执行 | 提升磁盘 I/O 吞吐量 |
效果:
最大化利用 CPU、内存、磁盘带宽,数据量再大也能稳定快速导出。
2. 防爆内存(Prevent OOM)
目标:
无论数据量多大,始终保持内存使用在合理范围内。
具体做法:
技术 | 描述 |
---|---|
Cassandra 分页查询(FetchSize) | 每次只拉一小批(如1000条)数据进内存 |
按批处理 | 拉一批、写一批、清理一批,避免累积太多对象 |
异步批次分批导出 | 每个线程只维护自己那一小批数据 |
效果:
即使单表百万行、千万行,也不会一次性把内存吃满,保证系统稳定运行。
可落盘备份(Durable Backup)
目标:
导出的数据安全持久保存,不丢失、不破坏。
具体做法:
技术 | 描述 |
---|---|
按批次小文件保存 | 每批导出一个小 JSON 文件,如 part_1.json |
可选压缩(gzip) | 小文件自动压缩,节省空间 |
完成后合并小文件(可选) | 也可以保留分片,方便增量恢复 |
目录规范化管理 | 每张表一个目录,按表名+日期组织 |
增加导出校验 | 导出完成后记录总行数、校验码(MD5)防止误差 |
🚀 实现方案(Java 控制台多线程进度条)
✨ Step 1:核心工具类 ProgressBar
arduino
public class ProgressBar {
private final int total;
private final AtomicInteger current = new AtomicInteger(0);
private final String taskName;
private final int barWidth = 20;
public ProgressBar(int total, String taskName) {
this.total = total;
this.taskName = taskName;
}
public void step(int count) {
current.addAndGet(count);
print();
}
public void print() {
int now = current.get();
double percent = now * 1.0 / total;
int len = (int)(barWidth * percent);
String bar = "[" +
"=".repeat(len) +
"-".repeat(barWidth - len) +
"] " + String.format("%3d%%", (int)(percent * 100)) +
" (" + now + " / " + total + ")" +
" 表:" + taskName +
(now >= total ? " ✔ Done" : "");
synchronized (System.out) {
System.out.print("\r" + bar); // 输出当前行
}
}
}
在程序中缓存到本地的数据时,通常会使用本地持久化存储(如文件、数据库、内存缓存等)来存储数据。这样,即使程序重新启动,也能恢复之前缓存的数据。以下是几种常见的实现方案:
1. 使用文件缓存
将缓存数据保存到本地文件中(如 JSON、XML、或二进制文件),在程序启动时读取文件恢复缓存。
示例:使用 JSON 文件存储缓存
java
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.HashMap;
public class FileCache {
private static final ObjectMapper objectMapper = new ObjectMapper();
private static final String CACHE_FILE_PATH = "cache_data.json"; // 缓存文件路径
private static Map<String, String> cache = new HashMap<>();
// 读取缓存数据
public static void loadCache() {
File cacheFile = new File(CACHE_FILE_PATH);
if (cacheFile.exists()) {
try {
cache = objectMapper.readValue(cacheFile, Map.class);
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 将数据缓存到文件
public static void saveCache() {
try {
objectMapper.writeValue(new File(CACHE_FILE_PATH), cache);
} catch (IOException e) {
e.printStackTrace();
}
}
// 获取缓存
public static String getCache(String key) {
return cache.get(key);
}
// 设置缓存
public static void setCache(String key, String value) {
cache.put(key, value);
saveCache();
}
public static void main(String[] args) {
// 加载缓存
loadCache();
// 设置缓存
setCache("user_123", "John Doe");
// 获取缓存
String user = getCache("user_123");
System.out.println("User: " + user);
}
}
优点:
- 简单易实现。
- 可以在文件中持久化数据,程序重启后可以恢复缓存。
缺点:
- 如果数据量非常大,使用文件缓存可能会变得缓慢。
- 文件存储的安全性较差,容易丢失或被篡改。
2. 使用数据库缓存
可以使用嵌入式数据库(如 SQLite)将缓存数据存储在本地数据库中。程序启动时从数据库中恢复缓存。
示例:使用 SQLite 存储缓存
typescript
import java.sql.*;
public class DatabaseCache {
private static final String DB_URL = "jdbc:sqlite:cache.db"; // SQLite数据库文件路径
private static Connection connection;
static {
try {
connection = DriverManager.getConnection(DB_URL);
Statement stmt = connection.createStatement();
stmt.execute("CREATE TABLE IF NOT EXISTS cache (key TEXT PRIMARY KEY, value TEXT)");
} catch (SQLException e) {
e.printStackTrace();
}
}
// 获取缓存
public static String getCache(String key) {
try {
PreparedStatement stmt = connection.prepareStatement("SELECT value FROM cache WHERE key = ?");
stmt.setString(1, key);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
return rs.getString("value");
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
// 设置缓存
public static void setCache(String key, String value) {
try {
PreparedStatement stmt = connection.prepareStatement("REPLACE INTO cache (key, value) VALUES (?, ?)");
stmt.setString(1, key);
stmt.setString(2, value);
stmt.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
// 设置缓存
setCache("user_123", "John Doe");
// 获取缓存
String user = getCache("user_123");
System.out.println("User: " + user);
}
}
优点:
- 数据持久化到数据库,可以进行复杂的查询操作。
- 数据可靠性较高,支持事务。
缺点:
- 相比内存缓存,数据库的读写性能较慢。
- 需要依赖数据库引擎。
3. 使用内存缓存+定期持久化(如 Redis)
Redis 是一个非常流行的内存缓存系统,通常用于存储快速读取的数据。如果需要缓存并持久化,可以使用 Redis 提供的持久化选项(RDB 或 AOF)。
Redis 持久化方式:
- RDB(快照) :在指定时间间隔内生成数据的快照进行持久化。
- AOF(追加文件) :记录每次修改操作,保证数据的持久化。
示例:Redis 缓存(Spring Data Redis)
typescript
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
@Component
public class RedisCache {
@Autowired
private RedisTemplate<String, String> redisTemplate;
// 获取缓存
public String getCache(String key) {
ValueOperations<String, String> ops = redisTemplate.opsForValue();
return ops.get(key);
}
// 设置缓存
public void setCache(String key, String value) {
ValueOperations<String, String> ops = redisTemplate.opsForValue();
ops.set(key, value);
}
}
优点:
- 高性能,支持分布式缓存。
- 可以持久化缓存数据,提供高可用性。
缺点:
- 需要额外的 Redis 服务部署。
- 如果 Redis 配置不当,可能会丢失部分数据。
4. 使用本地内存缓存 + 恢复机制
如果数据量较小且只需要在内存中缓存,可以选择将数据存储在内存中,在程序启动时加载缓存数据。使用一个简单的文件或数据库同步机制,将内存数据持久化到文件或数据库中。
5. 程序重启后的恢复:
无论使用哪种缓存方案,在程序重启时,都需要加载之前存储的缓存数据。常见的做法是:
- 在启动时读取文件、数据库或 Redis 中存储的数据并恢复到内存缓存中。
- 根据具体的需求选择是否清理缓存,或是恢复历史数据。
本地缓存恢复机制 就是:程序启动时读取缓存快照(文件、数据库)==> 恢复到内存中。
1. 缓存保存
比如你的应用里有个 Map<String, Object> localCache
,要在程序运行时,把它保存到硬盘,通常在两个时机:
- 定时保存,比如每5分钟存一次
- 关闭程序(Shutdown Hook)时保存一次,避免数据丢失
保存方法可以是:
- JSON 文件
- 本地数据库(如 SQLite)
scss
// 定时保存示例
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
executor.scheduleAtFixedRate(() -> {
saveLocalCacheToFile();
}, 5, 5, TimeUnit.MINUTES);
// 程序关闭时保存
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
saveLocalCacheToFile();
}));
2. 缓存恢复
- 程序启动时,先去读本地文件,把之前保存的缓存数据加载进来。
typescript
@PostConstruct
public void loadCache() {
Map<String, Object> cacheData = loadLocalCacheFromFile();
if (cacheData != null) {
localCache.putAll(cacheData);
}
}
简单总结流程:
场景 | 处理方式 |
---|---|
程序运行中 | 定时保存到文件 |
程序退出时 | 保存一次到文件 |
程序启动时 | 读取文件恢复 |
二、使用Redis缓存时,如何做恢复?(Redis本身的持久化机制)
Redis 本身是内存数据库,为了防止宕机数据丢失,有两种主流持久化方式:
1. RDB 持久化(快照方式)
- 定时(比如每隔5分钟、或者每有100条写入)保存整个内存快照到磁盘(
dump.rdb
)。 - Redis 宕机、重启后,会自动加载
dump.rdb
文件,把数据恢复到内存。
特点
- 轻量级,适合大部分普通应用。
- 有可能丢失最近一小段时间(最后一次保存后)修改的数据。
配置示例(redis.conf
)
bash
save 900 1 # 900秒(15分钟)内有1次修改就保存快照
save 300 10 # 300秒内有10次修改就保存快照
save 60 10000 # 60秒内有10000次修改就保存快照
2. AOF 持久化(日志方式)
- 把每次写命令(如 SET、HSET、LPUSH)追加到 AOF 文件。
- Redis 宕机、重启后,按日志回放的方式重新执行这些指令,恢复数据。
特点
- 数据最完整,可以做到几乎不丢数据。
- 缺点是AOF文件增长快,需要后台定期压缩(重写)。
配置示例(redis.conf
)
bash
appendonly yes # 打开AOF持久化
appendfsync everysec # 每秒钟同步一次
# appendfsync always # 每次写操作都同步,最安全但最慢
# appendfsync no # 不主动同步,依赖系统(性能高但可能丢数据)
3. RDB vs AOF 总结对比
持久化方式 | 优点 | 缺点 |
---|---|---|
RDB | 占用小,恢复快,适合冷备份 | 宕机时丢失最后一次快照后的数据 |
AOF | 数据最完整,适合核心数据保护 | 写入频繁,占用磁盘大,恢复速度慢一点 |
实际项目里一般是:
- RDB+AOF同时开启,互为备份。Redis支持同时打开两种模式。
- Redis重启时,优先用 AOF 恢复,AOF坏了再用RDB。
三、如果是你写的应用,想结合Redis缓存做恢复,一般这样做
- 正常用Redis做缓存
- Redis配置了RDB或AOF持久化,保障数据存储。
- 应用层做好容错,比如读Redis失败时,从数据库兜底,或者重新补缓存。
- 应用程序启动时,可以适当做一次缓存预热(预加载常用数据到缓存,提高命中率)。
示例:
typescript
@PostConstruct
public void preloadCache() {
// 程序启动时,把常用的设备状态、配置,提前加载到Redis里
cabinetsService.preloadCabinetsInfo();
}
程序启动时,要把之前保存的缓存数据恢复回来,也就是常说的 ------
缓存恢复 / 缓存预热(Application Start Cache Recovery)
🛠 一、【本地缓存】启动恢复
假设你程序有个本地 ConcurrentHashMap<String, Object> localCache
。
那你启动的时候,应该做 这三步:
- 读取磁盘文件(比如 JSON 文件 / 本地数据库 SQLite)
- 反序列化成对象
- putAll到内存缓存中
示例代码(假设用 JSON 文件保存的)
typescript
@Component
public class LocalCacheManager {
private static final Logger logger = LoggerFactory.getLogger(LocalCacheManager.class);
private final ConcurrentHashMap<String, Object> localCache = new ConcurrentHashMap<>();
private static final String CACHE_FILE = "/tmp/local_cache.json";
@PostConstruct
public void loadLocalCache() {
File file = new File(CACHE_FILE);
if (!file.exists()) {
logger.warn("缓存文件不存在,跳过加载");
return;
}
try (FileReader reader = new FileReader(file)) {
Type type = new TypeToken<Map<String, Object>>(){}.getType();
Map<String, Object> cacheFromFile = new Gson().fromJson(reader, type);
if (cacheFromFile != null) {
localCache.putAll(cacheFromFile);
logger.info("本地缓存加载完成,条数:" + cacheFromFile.size());
}
} catch (Exception e) {
logger.error("本地缓存恢复失败", e);
}
}
public Object get(String key) {
return localCache.get(key);
}
}
📌 注意:
-
@PostConstruct
:Spring容器初始化后自动调用 -
如果是复杂对象(比如设备状态类),反序列化的时候需要注意泛型
-
启动时检查Redis关键数据是否存在
-
缺失的话,要去数据库/接口补充加载到Redis里
示例代码(Redis缓存预热)
java
@Component
public class RedisCachePreloader {
private static final Logger logger = LoggerFactory.getLogger(RedisCachePreloader.class);
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private CabinetsService cabinetsService; // 自己的业务服务,查DB
@PostConstruct
public void preloadCache() {
logger.info("启动时预热Redis缓存...");
List<CabinetsInfo> cabinetsList = cabinetsService.queryAllCabinets();
for (CabinetsInfo info : cabinetsList) {
if (!Boolean.TRUE.equals(redisTemplate.hasKey(info.getCabinetId()))) {
redisTemplate.opsForHash().put("cabinetInfo", info.getCabinetId(), info.getServerAddr());
}
}
logger.info("Redis缓存预热完成,数量:" + cabinetsList.size());
}
}
(缓存恢复完整流程)
css
[程序启动]
↓
【本地缓存】
- 读取缓存快照文件(JSON、SQLite)
- 反序列化
- 填充ConcurrentHashMap
【Redis缓存】
- Redis自动恢复(RDB/AOF)
- 程序检测关键数据
- 缓存缺失时补充预热