分布式微服务系统架构第116集:设备网关,处理字节的大数据,过亿缓存

加群联系作者vx:xiaoda0423

仓库地址:webvueblog.github.io/JavaPlusDoc...

1024bat.cn/

好的,咱们直接讲清楚重点:

Apache Cassandra 中,Session 是你跟数据库交互的核心对象

它主要负责干两件事:

  1. 管理连接
    Session 代表着和 Cassandra 集群 建立好的一组连接(通常是连接池),而不是一个简单的 TCP 连接。
    你可以理解成:Session 帮你打通了集群里多个节点之间的路,准备好随时发请求。
  2. 执行 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 项目启动阶段

  1. Spring 启动时,扫描到 @Configuration 标注的 EventConsumerConfig 类。

  2. Spring 自动执行 @Bean 方法:

    • 调用 batchRecordConsumerFactory() 方法,注册名字叫 "BRConsumerFactory"批量消费容器工厂。
    • 调用 oneByOneRecordConsumerFactory() 方法,注册名字叫 "OBORConsumerFactory"单条消费容器工厂。
  3. 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 将消息序列化

  • KeySerializerValueSerializer(这里用的是 StringSerializer)把 key 和 value 转换成字节数组(byte[])。
  • 序列化是必须的,因为网络传输需要字节流。

3. 将消息分配到 Partition

  • 根据发送时指定的 key(如果有),使用 Kafka 的 分区器(Partitioner) 算法计算出具体的 partition。
  • 如果没指定 key,则使用轮询或随机分配 partition。

4. 消息写入 RecordAccumulator(本地缓存池)

  • Kafka Producer 不会立刻发送 每一条消息,而是先放到内存中的 RecordAccumulator 缓冲池中。

  • 什么时候触发发送?

    • 累积到一定 batch.size 大小。
    • 或者等待时间超过 linger.ms

(👉 你的配置:batch.sizelinger.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)

  • 是给下面这个线程池:

    ini 复制代码
    ExecutorService fixedThreadPool = new ThreadPoolExecutor(..., blockingQueue);

    用的。

  • 本质 是作为线程池任务缓冲区

    当线程池的线程忙不过来时,新来的任务(Runnable)就先塞进 blockingQueue,排队等执行。

说白了,就是让线程池不丢任务、顺序排队的地方。

线程池 + 阻塞队列配合流程:

顺着你的代码理解是这样:

  1. 线程池初始化

    • 核心线程数:2
    • 最大线程数:6
    • 队列:blockingQueue
  2. 提交任务时(比如异步提交一个 Runnable)

    • 如果当前活跃线程数 < 核心线程数 (2个): ➔ 直接新建线程执行任务
    • 如果活跃线程数 ≥ 核心线程数 : ➔ 任务塞到 blockingQueue 里面排队
    • 如果blockingQueue 满了 ,并且活跃线程数 < 最大线程数(6): ➔ 继续扩容线程去处理。
    • 如果最大线程数也满了 ,且队列也满了 : ➔ 执行拒绝策略(默认抛异常,可以自定义处理)。
  3. 线程空闲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缓存做恢复,一般这样做

  1. 正常用Redis做缓存
  2. Redis配置了RDB或AOF持久化,保障数据存储。
  3. 应用层做好容错,比如读Redis失败时,从数据库兜底,或者重新补缓存。
  4. 应用程序启动时,可以适当做一次缓存预热(预加载常用数据到缓存,提高命中率)。

示例:

typescript 复制代码
@PostConstruct
public void preloadCache() {
    // 程序启动时,把常用的设备状态、配置,提前加载到Redis里
    cabinetsService.preloadCabinetsInfo();
}

程序启动时,要把之前保存的缓存数据恢复回来,也就是常说的 ------

缓存恢复 / 缓存预热(Application Start Cache Recovery)

🛠 一、【本地缓存】启动恢复

假设你程序有个本地 ConcurrentHashMap<String, Object> localCache

那你启动的时候,应该做 这三步

  1. 读取磁盘文件(比如 JSON 文件 / 本地数据库 SQLite)
  2. 反序列化成对象
  3. 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)
    - 程序检测关键数据
    - 缓存缺失时补充预热
相关推荐
毅航1 分钟前
从单体到微服务:Spring Cloud Gateway 断言的深度剖析
java·后端·spring cloud
uhakadotcom15 分钟前
求职必备:DeepSeek + GPT-4.1 + Gemini 2.5 + Trea + Jobleap,打造完美简历,精准匹配高薪岗位
面试·架构·github
JobHu19 分钟前
springboot集成elasticSearch
后端
xdargs25 分钟前
实用工具集索引
后端
编程轨迹41 分钟前
剖析 Java 23 特性:深入探究最新功能
后端
洛小豆1 小时前
一个场景搞明白Reachability Fence,它就像一道“结账前别走”的红外感应门
java·后端·面试
郝同学的测开笔记1 小时前
云原生探索系列(十六):Go 语言锁机制
后端·云原生·go
七月丶1 小时前
🛠 用 Node.js 和 commander 快速搭建一个 CLI 工具骨架(gix 实战)
前端·后端·github
砖吐筷筷1 小时前
我理想的房间是什么样的丨去明日方舟 Only 玩 - 筷筷月报#18
前端·github
七月丶1 小时前
🔀 打造更智能的 Git 提交合并命令:gix merge 实战
前端·后端·github