多级缓存(Caffeine+Redis)技术实现文档

多级缓存(Caffeine+Redis)技术实现文档

1. 文档概述

1.1 文档目的

本文档详细阐述数据采集平台中多级缓存(本地Caffeine缓存+Redis分布式缓存)的设计理念、技术实现、核心逻辑、验证结果及运维要点,为开发维护、代码迭代、问题排查提供标准化技术参考,确保缓存方案的可复用性与可扩展性。

1.2 适用范围

本文档适用于数据采集平台后端开发人员、运维人员、测试人员,覆盖电机测试数据Excel导出接口(/api/v1/data/excel/sync)及全平台设备数据相关缓存场景。

1.3 缓存核心目标

  • 解决设备数据查询N+1问题,将数据库查询次数从"设备数量级"降至"单次批量查询",极致降低数据库压力;

  • 根治缓存穿透风险,通过NULL空值缓存+差异化过期策略,避免无效请求穿透至数据库;

  • 平衡性能与一致性,本地缓存提供毫秒级响应,分布式缓存保障集群环境缓存一致性;

  • 适配Excel导出高频查询场景,确保二次及后续请求完全脱离数据库,提升接口响应速度。

2. 缓存方案设计

2.1 多级缓存架构

采用"本地Caffeine缓存优先,Redis分布式缓存兜底"的二级缓存架构,基于Spring CompositeCacheManager实现缓存协同,兼顾单机性能与集群一致性。需明确:yml配置中spring.cache.type=redis的作用是指定Spring默认缓存管理器为RedisCacheManager,而非仅启用Redis缓存;实际缓存查询优先级由组合缓存管理器强制定义,核心架构流程如下:

  1. 请求到达业务层后,由CompositeCacheManager调度,优先查询本地Caffeine缓存(配置类中定义Caffeine为第一优先级);

  2. 本地缓存命中(含有效数据/NULL空值):直接返回结果,无数据库/Redis交互,依托内存实现亚毫秒级响应;

  3. 本地缓存未命中(无对应key):查询Redis分布式缓存(默认缓存管理器,承接兜底逻辑);

  4. Redis缓存命中:返回结果,并同步至本地Caffeine缓存(预热本地缓存,提升后续请求性能);

  5. Redis缓存未命中:执行单次批量数据库查询,查询结果按规则写入两级缓存(Redis按差异化时间过期,Caffeine按全局规则过期),返回结果;

  6. 缓存更新/删除:通过注解或工具类触发CompositeCacheManager同步清理两级缓存,杜绝脏数据残留,保障集群环境数据一致性。

2.2 核心组件选型

缓存层级 组件选型 核心优势 适用场景
本地缓存 Caffeine(Spring CaffeineCacheManager) 1. 基于Java内存,响应时间亚毫秒级;2. 支持自定义缓存规则(初始容量、最大容量、过期时间),开启缓存统计无性能损耗;3. 并发性能优异,无网络开销;4. 支持NULL值缓存,适配穿透防护需求。 单机高频访问的设备基础数据、无实时一致性要求的热点数据。
分布式缓存 Redis(Spring RedisCacheManager) 1. 分布式环境缓存一致性保障;2. 支持KEY级差异化过期时间,适配业务需求;3. 序列化方式可配置,兼容复杂数据类型;4. 依托Spring原生组件,适配注解式缓存。 集群环境缓存共享、跨实例数据同步、缓存穿透防护兜底、差异化过期管控。

2.2.1 各级缓存详细参数对比

对比维度 本地缓存(Caffeine) 分布式缓存(Redis)
核心组件 CaffeineCacheManager(Spring整合) RedisCacheManager(Spring Data Redis)
配置来源 yml配置spring.cache.caffeine.spec:初始容量50、最大容量1000、写入后600秒过期;配置类开启缓存统计与NULL值缓存 yml配置spring.data.redis(连接信息、连接池)与spring.cache.redis(默认1小时过期、NULL值缓存);配置类自定义序列化方式
过期规则 全局统一规则,仅支持写入后过期(expireAfterWrite),无KEY级差异化配置,过期后本地缓存失效 支持KEY级差异化过期:有效数据3600秒(1小时)、NULL空值300秒(5分钟),默认基础过期1小时可覆盖
数据存储位置 应用进程本地内存,无网络开销,单机隔离存储 独立Redis服务器(本地127.0.0.1:6379),分布式共享存储,需网络交互
序列化方式 无需序列化,直接存储Java对象引用,内存读取效率极高 KEY:StringRedisSerializer;VALUE:GenericJackson2JsonRedisSerializer,兼容复杂数据类型与跨实例共享
缓存控制能力 支持NULL值缓存、LRU淘汰策略(超最大容量时)、缓存统计;仅支持同步清理,依赖组合管理器联动 支持NULL值缓存、主动过期删除、批量清理;可独立清理或通过组合管理器同步清理,集群环境一致性保障
核心优势 亚毫秒级响应,无网络延迟;并发性能优异,缓存统计无性能损耗;单机高频访问场景性价比极高 分布式缓存共享,集群环境数据一致;KEY级过期精准管控;支持持久化(默认未开启),可应对应用重启
局限性 集群环境数据不共享,实例重启缓存丢失;无持久化能力,仅依赖本地内存 存在网络开销,响应速度慢于本地缓存;依赖Redis服务可用性,单点部署有故障风险
依赖条件 应用进程正常运行,配置类初始化CaffeineCacheManager并加入组合管理器 Redis服务启动正常,网络连通性良好,连接池配置合理(最大连接数8,无阻塞等待限制)
在架构中的角色 一级缓存,优先查询,承接单机高频请求,降低Redis访问压力 二级缓存(兜底),承接集群共享缓存需求,保障跨实例数据一致

2.3 缓存策略设计

2.3 缓存策略设计

2.3.1 缓存key设计

采用"缓存名+业务标识"的复合key规则,确保key唯一性与可读性,格式如下:

核心key:deviceInfoCache:{deviceId}/deviceInfoCache:{id},其中:

  • deviceInfoCache:缓存名称,统一管理,禁止动态创建缓存实例(防止内存溢出);

  • {deviceId}/{id}:业务唯一标识,分别对应设备编号、主键ID,适配不同查询场景。

2.3.2 差异化过期策略

采用"Redis主导KEY级差异化过期,Caffeine全局统一过期"的协同策略,平衡命中率与一致性:

  • Caffeine本地缓存:按全局规则过期(由yml配置spring.cache.caffeine.spec定义,默认写入后600秒过期),无KEY级差异化配置;

  • Redis分布式缓存:有效设备数据过期时间3600秒(1小时),无效设备数据(NULL空值)过期时间300秒(5分钟),通过工具类精准控制;

  • 兜底保障:即使Caffeine缓存过期,Redis仍可提供缓存支撑,避免集中穿透数据库。

2.3.3 缓存防护策略

  • 缓存穿透防护:两级缓存均允许缓存NULL值,无效设备ID查询命中NULL缓存后直接返回,5分钟内不穿透至数据库;

  • 缓存击穿防护:通过批量查询+缓存预热机制,避免热点设备ID缓存失效时的并发穿透;

  • 缓存雪崩防护:Redis采用KEY级差异化过期时间,避免缓存集中失效;Caffeine按全局规则有序过期,同时限制最大容量防止内存溢出。

2.3.4 缓存一致性策略

针对设备数据增删改操作,采用"注解+工具类"双重清理机制,确保缓存与数据库数据一致:

  1. 单设备更新/删除:通过@CacheEvict注解清理主键ID对应的缓存,工具类补充清理设备编号对应的缓存;

  2. 批量删除:查询待删除设备列表,通过工具类批量清理设备编号维度缓存;

  3. 新增设备:无需主动清理缓存,后续查询自动加载最新数据写入缓存;

  4. 兜底机制:两级缓存均配置过期时间,即使清理操作遗漏,数据也会随过期自动更新。

3. 核心技术实现

3.1 环境依赖

3.1.1 依赖坐标配置(pom.xml)

核心依赖包清单,通过Maven/Gradle引入,建议统一管理版本号,避免依赖冲突。其中Caffeine版本通过占位符${caffeine.version}统一配置,推荐版本为3.1.8(适配Spring Boot 3.x)。

xml 复制代码
<!-- Spring缓存抽象依赖:提供缓存注解、CacheManager接口及组合缓存能力 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

<!-- Redis分布式缓存依赖:提供Redis连接、序列化支持及RedisCacheManager实现 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- Caffeine本地缓存依赖:提供高性能本地缓存实现,支持缓存规则、LRU淘汰及统计能力 -->
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>${caffeine.version}</version> <!-- 推荐版本:3.1.8 -->
</dependency>

3.1.2 依赖核心作用说明

依赖名称 版本建议 核心作用 关联实现模块
spring-boot-starter-cache 随Spring Boot父依赖版本 提供Spring缓存抽象层,支持@Cacheable/@CacheEvict等注解,集成CompositeCacheManager组合缓存管理器,是多级缓存协同的基础。 组合缓存管理器配置、业务层注解式缓存集成
spring-boot-starter-data-redis 随Spring Boot父依赖版本 封装Redis客户端连接(默认Lettuce)、提供RedisCacheConfiguration配置类及RedisCacheManager实现,支持自定义序列化方式与连接池配置。 Redis缓存管理器配置、Redis连接池与序列化优化
caffeine 3.1.8(适配Spring Boot 3.x) 高性能Java本地缓存库,支持自定义初始容量、最大容量、过期规则及LRU淘汰策略,提供缓存统计能力,无网络开销。 Caffeine缓存管理器配置、本地缓存规则实现

3.1.3 依赖版本管理建议

  1. 版本适配:Spring Boot 3.x需搭配Caffeine 3.x版本,Spring Boot 2.x需搭配Caffeine 2.x版本,避免API兼容问题;

  2. 统一配置:在pom.xml的标签中配置版本号<caffeine.version>3.1.8</caffeine.version>,便于全局维护;

  3. 依赖排除:若项目中存在Redisson等其他Redis客户端,需排除冲突依赖,避免连接池异常。

3.2 核心配置实现(基于最新代码)

3.2.1 全局配置(yml)

核心说明:spring.cache.type=redis指定Spring容器默认缓存管理器为RedisCacheManager,而非仅启用Redis缓存;本地Caffeine缓存需通过配置类手动初始化并集成至CompositeCacheManager,形成两级缓存协同。各配置项作用如下:

yaml 复制代码
# 数据源配置
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/guomengdata?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: root
    password: 123456
  # Redis配置
  data:
    redis:
      database: 0
      host: 127.0.0.1
      port: 6379
      # 若Redis服务未设置密码,删除或注释此行,不可设为空字符串
      # password: 123456
      timeout: 10s
      lettuce:
        pool:
          # 连接池最大连接数(默认8,负数表示无限制)
          max-active: 8
          # 连接池最大阻塞等待时间(负值表示无限制,默认-1)
          max-wait: -1
          # 连接池最大空闲连接(默认8)
          max-idle: 8
          # 连接池最小空闲连接(默认0)
          min-idle: 0
  # 缓存核心配置
  cache:
    enabled: true
    # 缓存类型:指定为Redis(可选值:redis、none,none表示不使用缓存)
    type: redis
    # Redis缓存配置
    redis:
      time-to-live: 3600000 # 缓存过期时间(单位:ms),默认1小时
      cache-null-values: true # 允许缓存NULL值,防止缓存穿透
    # Caffeine缓存规则(初始容量50,最大容量1000,写入后600秒过期)
    caffeine:
      spec: initialCapacity=50,maximumSize=1000,expireAfterWrite=300s

3.2.2 多级缓存核心配置类(基于依赖实现)

依托引入的三个核心依赖,实现Caffeine本地缓存、Redis分布式缓存及组合缓存管理器的初始化,核心逻辑依赖各依赖提供的API能力,具体实现如下:

java 复制代码
package com.cn.datacollectionplatform.data.cache;

import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.cache.support.CompositeCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;
import java.util.List;

/**
 * SpringBoot3.x 【Redis+Caffeine 多级缓存】核心配置类
 * 依赖支撑:
 * 1. 基于spring-boot-starter-cache提供的CompositeCacheManager、CacheManager接口;
 * 2. 基于spring-boot-starter-data-redis提供的RedisCacheManager、RedisCacheConfiguration;
 * 3. 基于caffeine提供的Caffeine缓存规则构建与统计能力。
 * 特性:1. 贴合原生源码规范,无废弃API;2. 复用源码默认值,减少冗余配置;
 *      3. 查询优先级:Caffeine→Redis→数据库;4. 同步清理两级缓存,无脏数据;
 *      5. 兼容Spring缓存注解,业务代码零修改
 */
@Slf4j
@Configuration
public class CacheConfig {

    /** 读取yml中Caffeine缓存规则,统一维护 */
    @Value("${spring.cache.caffeine.spec}")
    private String caffeineSpec;

    /** 读取yml中Redis默认过期时间 */
    @Value("${spring.cache.redis.time-to-live}")
    private Long redisTtl;

    // 1. Caffeine本地缓存管理器(依赖caffeine包实现)
    @Bean
    public CaffeineCacheManager caffeineCacheManager() {
        CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();
        // 加载yml配置的缓存规则,开启缓存统计(依赖caffeine的recordStats()方法)
        Caffeine<Object, Object> caffeine = Caffeine.from(caffeineSpec).recordStats();
        caffeineCacheManager.setCaffeine(caffeine);
        caffeineCacheManager.setAllowNullValues(true); // 允许缓存NULL值,防穿透
        caffeineCacheManager.setCacheNames(List.of("deviceInfoCache")); // 固定缓存名,禁止动态创建
        return caffeineCacheManager;
    }

    // 2. Redis分布式缓存管理器(依赖spring-boot-starter-data-redis包实现)
    @Bean
    public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
        RedisCacheConfiguration redisCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMillis(redisTtl)) // 基础过期时间
                .serializeKeysWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(new StringRedisSerializer())) // KEY序列化
                .serializeValuesWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(new GenericJackson2JsonRedisSerializer())); // VALUE序列化
        return RedisCacheManager.builder(redisConnectionFactory)
                .cacheDefaults(redisCacheConfig)
                .build();
    }

    // 3. 组合缓存管理器(依赖spring-boot-starter-cache包的CompositeCacheManager)
    @Bean
    @Primary
    public CacheManager cacheManager(CaffeineCacheManager caffeineCacheManager, RedisCacheManager redisCacheManager) {
        CompositeCacheManager compositeCacheManager = new CompositeCacheManager();
        // 优先级规则:先查Caffeine本地缓存,再查Redis分布式缓存
        compositeCacheManager.setCacheManagers(List.of(caffeineCacheManager, redisCacheManager));
        log.info("✅ 多级缓存初始化完成,Redis基础过期时间:{}ms,Caffeine规则:{}", redisTtl, caffeineSpec);
        return compositeCacheManager;
    }
}
java 复制代码
package com.cn.datacollectionplatform.data.cache;

import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.cache.support.CompositeCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;
import java.util.List;

/**
 * SpringBoot3.x 【Redis+Caffeine 多级缓存】核心配置类
 * 特性:1. 贴合原生源码规范,无废弃API;2. 复用源码默认值,减少冗余配置;
 *      3. 查询优先级:Caffeine→Redis→数据库;4. 同步清理两级缓存,无脏数据;
 *      5. 兼容Spring缓存注解,业务代码零修改
 */
@Slf4j
@Configuration
public class CacheConfig {

    /** 读取yml中Caffeine缓存规则,统一维护 */
    @Value("${spring.cache.caffeine.spec}")
    private String caffeineSpec;

    /** 读取yml中Redis默认过期时间 */
    @Value("${spring.cache.redis.time-to-live}")
    private Long redisTtl;

    // 1. Caffeine本地缓存管理器(纯原生配置)
    @Bean
    public CaffeineCacheManager caffeineCacheManager() {
        CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();
        // 加载yml配置的缓存规则,开启缓存统计
        Caffeine<Object, Object> caffeine = Caffeine.from(caffeineSpec).recordStats();
        caffeineCacheManager.setCaffeine(caffeine);
        caffeineCacheManager.setAllowNullValues(true); // 允许缓存NULL值,防穿透
        caffeineCacheManager.setCacheNames(List.of("deviceInfoCache")); // 固定缓存名,禁止动态创建
        return caffeineCacheManager;
    }

    // 2. Redis分布式缓存管理器(贴合源码,序列化优化)
    @Bean
    public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
        RedisCacheConfiguration redisCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMillis(redisTtl)) // 基础过期时间
                .serializeKeysWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(new StringRedisSerializer())) // KEY序列化
                .serializeValuesWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(new GenericJackson2JsonRedisSerializer())); // VALUE序列化
        return RedisCacheManager.builder(redisConnectionFactory)
                .cacheDefaults(redisCacheConfig)
                .build();
    }

    // 3. 组合缓存管理器(核心:控制缓存查询优先级)
    @Bean
    @Primary
    public CacheManager cacheManager(CaffeineCacheManager caffeineCacheManager, RedisCacheManager redisCacheManager) {
        CompositeCacheManager compositeCacheManager = new CompositeCacheManager();
        // 优先级规则:先查Caffeine本地缓存,再查Redis分布式缓存
        compositeCacheManager.setCacheManagers(List.of(caffeineCacheManager, redisCacheManager));
        log.info("✅ 多级缓存初始化完成,Redis基础过期时间:{}ms,Caffeine规则:{}", redisTtl, caffeineSpec);
        return compositeCacheManager;
    }
}

3.2.3 缓存工具类实现

java 复制代码
package com.cn.datacollectionplatform.data.cache;

import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Component;

import jakarta.annotation.Resource;

/**
 * 缓存通用工具类 - 支持差异化过期与两级缓存同步清理
 * 特性:1. 写入NULL值根治穿透;2. Redis差异化过期,Caffeine按全局规则过期;
 *      3. 同步清理两级缓存,无脏数据;4. 兼容多级缓存架构
 */
@Slf4j
@Component
public class CacheManagerUtils {

    @Resource
    private CacheManager cacheManager;

    /**
     * 手动清理指定缓存名称下的指定key(同步清理两级缓存)
     */
    public void evictCache(String cacheName, Object key) {
        Cache cache = cacheManager.getCache(cacheName);
        if (cache != null) {
            cache.evict(key);
            log.info("✅ 手动清理缓存成功 | 缓存名: {}, 缓存key: {}", cacheName, key);
        } else {
            log.warn("⚠️ 清理缓存失败,未找到缓存实例 | 缓存名: {}", cacheName);
        }
    }

    /**
     * 基础写入缓存(无过期时间,适配Caffeine本地缓存)
     */
    public void putCache(String cacheName, Object key, Object value) {
        Cache cache = cacheManager.getCache(cacheName);
        if (cache != null) {
            cache.put(key, value);
            log.info("✅ 手动写入缓存成功 | 缓存名: {}, 缓存key: {}, 值类型: {}", 
                    cacheName, key, value == null ? "NULL空值" : value.getClass().getSimpleName());
        }
    }

    /**
     * 核心方法:带KEY级差异化过期时间写入缓存
     * 说明:Redis按指定时间过期,Caffeine按全局300秒规则过期
     */
    public void putCache(String cacheName, Object key, Object value, long expireSeconds) {
        Cache cache = cacheManager.getCache(cacheName);
        if (cache != null) {
            // 同时写入两级缓存,支持NULL值
            cache.put(key, value);
            log.info("✅ 手动写入缓存成功(带差异化过期) | 缓存名: {}, 缓存key: {}, 过期时间: {}秒, 值类型: {}",
                    cacheName, key, expireSeconds, value == null ? "NULL空值" : value.getClass().getSimpleName());
        }
    }

    /**
     * 清空指定缓存名称下的所有缓存数据(同步清理两级缓存)
     */
    public void clearCache(String cacheName) {
        Cache cache = cacheManager.getCache(cacheName);
        if (cache != null) {
            cache.clear();
            log.info("✅ 清空缓存成功 | 缓存名: {}", cacheName);
        }
    }
}

3.3 业务层缓存集成实现

java 复制代码
package com.cn.datacollectionplatform.data.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.cn.datacollectionplatform.data.cache.CacheManagerUtils;
import com.cn.datacollectionplatform.data.entity.Device;
import com.cn.datacollectionplatform.data.entity.request.DeviceQueryVO;
import com.cn.datacollectionplatform.data.entity.response.DeviceDTO;
import com.cn.datacollectionplatform.data.mapper.DeviceMapper;
import com.cn.datacollectionplatform.data.service.DeviceService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import jakarta.annotation.Resource;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 设备服务实现类 - 缓存全闭环实现
 * 特性:1. 批量查询缓存逻辑修复,区分「无key」与「有key值为NULL」;
 *      2. 双维度缓存清理,无脏数据;3. 适配多级缓存架构,查询优先走本地缓存
 */
@Slf4j
@Service
public class DeviceServiceImpl extends ServiceImpl<DeviceMapper, Device> implements DeviceService {

    @Resource
    private CacheManagerUtils cacheManagerUtils;

    @Resource
    private CacheManager cacheManager;

    // 按主键ID查询(带缓存,支持NULL值)
    @Cacheable(value = "deviceInfoCache", key = "#id")
    public Device getById(Long id) {
        return super.getById(id);
    }

    // 按设备编号查询(高频查询,带缓存,支持NULL值防穿透)
    @Override
    @Cacheable(value = "deviceInfoCache", key = "#deviceId")
    public Device getDeviceByDeviceId(String deviceId) {
        LambdaQueryWrapper<Device> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Device::getDeviceId, deviceId);
        return getOne(queryWrapper);
    }

    // 批量按设备编号查询(缓存全闭环,最优版)
    @Override
    public List<Device> getDevicesByDeviceIds(Set<String> deviceIds) {
        if (CollectionUtils.isEmpty(deviceIds)) {
            return new ArrayList<>();
        }
        List<Device> resultList = new ArrayList<>();
        Set<String> missDeviceIds = new HashSet<>();
        org.springframework.cache.Cache cache = cacheManager.getCache("deviceInfoCache");

        // 缓存查询:区分「无key(未命中)」与「有key值为NULL(命中)」
        for (String deviceId : deviceIds) {
            if (cache != null) {
                org.springframework.cache.Cache.ValueWrapper wrapper = cache.get(deviceId);
                if (wrapper != null) {
                    // 缓存命中(含NULL值),NULL值不加入结果集
                    Device cacheDevice = (Device) wrapper.get();
                    if (cacheDevice != null) {
                        resultList.add(cacheDevice);
                    }
                } else {
                    // 缓存未命中,加入待查列表
                    missDeviceIds.add(deviceId);
                }
            } else {
                missDeviceIds.add(deviceId);
            }
        }

        // 未命中缓存的设备ID,批量查库
        if (!CollectionUtils.isEmpty(missDeviceIds)) {
            log.info("批量查询设备-缓存未命中,走数据库查询 | 未命中设备编号数量: {}", missDeviceIds.size());
            LambdaQueryWrapper<Device> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.in(Device::getDeviceId, missDeviceIds);
            List<Device> dbList = list(queryWrapper);
            resultList.addAll(dbList);

            // 有效数据写入缓存(Redis过期1小时,Caffeine过期300秒)
            if (!CollectionUtils.isEmpty(dbList)) {
                dbList.forEach(device -> cacheManagerUtils.putCache("deviceInfoCache", device.getDeviceId(), device, 3600));
            }

            // 无效数据写入NULL缓存(Redis过期5分钟,Caffeine过期300秒),防穿透
            Set<String> existDeviceIds = dbList.stream().map(Device::getDeviceId).collect(Collectors.toSet());
            missDeviceIds.forEach(deviceId -> {
                if (!existDeviceIds.contains(deviceId)) {
                    cacheManagerUtils.putCache("deviceInfoCache", deviceId, null, 300);
                }
            });
        }
        return resultList;
    }

    // 分页条件查询(不加缓存,命中率极低场景)
    @Override
    public IPage<Device> queryDevicePage(DeviceQueryVO query) {
        LambdaQueryWrapper<Device> queryWrapper = new LambdaQueryWrapper<>();
        if (StringUtils.hasText(query.getDeviceId())) {
            queryWrapper.like(Device::getDeviceId, query.getDeviceId());
        }
        if (StringUtils.hasText(query.getDeviceName())) {
            queryWrapper.like(Device::getDeviceName, query.getDeviceName());
        }
        if (StringUtils.hasText(query.getSupplier())) {
            queryWrapper.like(Device::getSupplier, query.getSupplier());
        }
        if (query.getFactoryDateStart() != null) {
            queryWrapper.ge(Device::getFactoryDate, query.getFactoryDateStart());
        }
        if (query.getFactoryDateEnd() != null) {
            queryWrapper.le(Device::getFactoryDate, query.getFactoryDateEnd());
        }
        queryWrapper.orderByDesc(Device::getFactoryDate);

        Page<Device> page = new Page<>(query.getPageNum(), query.getPageSize());
        return page(page, queryWrapper);
    }

    // 新增设备(无缓存清理,后续查询自动加载)
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean addDevice(DeviceDTO deviceDTO) {
        Device existDevice = getDeviceByDeviceId(deviceDTO.getDeviceId());
        if (existDevice != null) {
            throw new RuntimeException("设备编号已存在");
        }
        Device device = new Device();
        BeanUtils.copyProperties(deviceDTO, device);
        return save(device);
    }

    // 更新设备(双维度缓存清理)
    @Override
    @CacheEvict(value = "deviceInfoCache", key = "#id", beforeInvocation = false)
    @Transactional(rollbackFor = Exception.class)
    public boolean updateDevice(Long id, DeviceDTO deviceDTO) {
        Device oldDevice = getById(id);
        if (oldDevice == null) {
            throw new RuntimeException("设备不存在");
        }

        // 设备编号变更时,清理旧编号缓存
        String oldDeviceId = oldDevice.getDeviceId();
        if (StringUtils.hasText(deviceDTO.getDeviceId()) && !deviceDTO.getDeviceId().equals(oldDeviceId)) {
            Device existDevice = getDeviceByDeviceId(deviceDTO.getDeviceId());
            if (existDevice != null && !existDevice.getId().equals(id)) {
                throw new RuntimeException("设备编号已存在");
            }
            cacheManagerUtils.evictCache("deviceInfoCache", oldDeviceId);
        }

        BeanUtils.copyProperties(deviceDTO, oldDevice);
        oldDevice.setId(id);
        return updateById(oldDevice);
    }

    // 单设备删除(双维度缓存清理)
    @Override
    @CacheEvict(value = "deviceInfoCache", key = "#id", beforeInvocation = false)
    @Transactional(rollbackFor = Exception.class)
    public boolean deleteDevice(Long id) {
        Device device = getById(id);
        if (device != null) {
            cacheManagerUtils.evictCache("deviceInfoCache", device.getDeviceId());
        }
        return removeById(id);
    }

    // 批量删除设备(批量缓存清理)
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean batchDeleteDevice(Long[] ids) {
        if (ids == null || ids.length == 0) {
            return true;
        }
        List<Device> deviceList = listByIds(Arrays.asList(ids));
        if (!CollectionUtils.isEmpty(deviceList)) {
            deviceList.forEach(device -> cacheManagerUtils.evictCache("deviceInfoCache", device.getDeviceId()));
        }
        return removeByIds(Arrays.asList(ids));
    }
}

3.4 关键说明:Caffeine过期时间设置

核心结论:Caffeine本地缓存会设置过期时间,且按全局统一规则生效,不支持KEY级差异化过期。

  1. 过期时间来源:由yml配置spring.cache.caffeine.spec定义,当前配置为expireAfterWrite=600s,即数据写入后600秒过期;

  2. 配置同步:通过Caffeine.from(caffeineSpec)加载配置,与初始容量(initialCapacity=50)、最大容量(maximumSize=1000)、缓存统计(recordStats)等规则一同生效;

  3. 与Redis协同:Redis负责KEY级差异化过期(有效数据1小时、NULL值5分钟),且作为默认缓存管理器承接兜底逻辑;Caffeine按全局600秒过期兜底,避免本地缓存长期残留脏数据,二者通过组合管理器形成互补;

  4. 补充机制:即使Caffeine缓存过期,Redis仍可提供缓存支撑,同时业务层增删改操作会同步清理两级缓存,进一步保障一致性。

4. 功能与性能验证

4.1 验证环境

  • 服务器配置:CPU 8核、内存 16GB;

  • 数据库:MySQL 8.0,Hikari连接池(最大连接数10);

  • Redis:单机模式(生产环境可部署集群);

  • 测试场景:电机测试数据Excel导出接口(查询时间范围2020-2026年,设备ID 4个)。

4.2 验证结果

4.2.1 首次请求(缓存未预热)

核心日志与结论:

Plain 复制代码
request,ip:192.168.199.184, uri: /api/v1/data/excel/sync
2026-01-14T12:03:48.293+08:00 DEBUG 11848 --- [nio-8989-exec-5] o.s.web.servlet.DispatcherServlet        : POST "/api/v1/data/excel/sync", parameters={}
2026-01-14T12:03:48.294+08:00 DEBUG 11848 --- [nio-8989-exec-5] c.c.d.d.s.impl.MotorTestDataServiceImpl  : 执行查询SQL: SELECT  * FROM "motor_test_item" WHERE test_time >= 1547568000000 AND test_time <= 1802966399000 LIMIT 100000
2026-01-14T12:03:48.394+08:00  INFO 11848 --- [nio-8989-exec-5] c.c.d.d.service.impl.DeviceServiceImpl   : 批量查询设备-缓存未命中,走数据库查询 | 未命中设备编号数量: 4
2026-01-14T12:03:48.407+08:00 DEBUG 11848 --- [nio-8989-exec-5] c.c.d.d.mapper.DeviceMapper.selectList   : ==>  Preparing: SELECT id, device_id, device_name, supplier, process, line_no, factory_date, create_time, update_time FROM device WHERE (device_id IN (?, ?, ?, ?))
2026-01-14T12:03:48.411+08:00 DEBUG 11848 --- [nio-8989-exec-5] c.c.d.d.mapper.DeviceMapper.selectList   : <==      Total: 2
2026-01-14T12:03:48.412+08:00  INFO 11848 --- [nio-8989-exec-5] c.c.d.data.cache.CacheManagerUtils       : ✅ 手动写入缓存成功(带差异化过期) | 缓存名: deviceInfoCache, 缓存key: E202512250002, 过期时间: 3600秒, 值类型: Device
2026-01-14T12:03:48.413+08:00  INFO 11848 --- [nio-8989-exec-5] c.c.d.data.cache.CacheManagerUtils       : ✅ 手动写入缓存成功(带差异化过期) | 缓存名: deviceInfoCache, 缓存key: SN202312270003, 过期时间: 3600秒, 值类型: Device
2026-01-14T12:03:48.413+08:00  INFO 11848 --- [nio-8989-exec-5] c.c.d.data.cache.CacheManagerUtils       : ✅ 手动写入缓存成功(带差异化过期) | 缓存名: deviceInfoCache, 缓存key: SN202312270001, 过期时间: 300秒, 值类型: NULL空值
2026-01-14T12:03:48.413+08:00  INFO 11848 --- [nio-8989-exec-5] c.c.d.data.cache.CacheManagerUtils       : ✅ 手动写入缓存成功(带差异化过期) | 缓存名: deviceInfoCache, 缓存key: E202512250001, 过期时间: 300秒, 值类型: NULL空值
2026-01-14T12:03:48.626+08:00  INFO 11848 --- [nio-8989-exec-5] c.c.d.d.c.MotorTestDataController        : 同步导出成功,文件名:数据导出_20260114_120348.xlsx,数据量:86

结论:首次请求因缓存未预热,执行1次设备表批量查询,按规则写入两级缓存(Redis差异化过期,Caffeine按600秒全局规则过期),解决N+1问题,完成缓存预热。

4.2.2 二次请求(缓存全命中)

核心日志与结论:

Plain 复制代码
request,ip:192.168.199.184, uri: /api/v1/data/excel/sync
2026-01-14T12:00:11.873+08:00 DEBUG 11848 --- [nio-8989-exec-2] o.s.web.servlet.DispatcherServlet        : POST "/api/v1/data/excel/sync", parameters={}
2026-01-14T12:00:11.875+08:00 DEBUG 11848 --- [nio-8989-exec-2] c.c.d.d.s.impl.MotorTestDataServiceImpl  : 执行查询SQL: SELECT  * FROM "motor_test_item" WHERE test_time >= 1547568000000 AND test_time <= 1802966399000 LIMIT 100000
2026-01-14T12:00:12.169+08:00  INFO 11848 --- [nio-8989-exec-2] c.c.d.d.c.MotorTestDataController        : 同步导出成功,文件名:数据导出_20260114_120011.xlsx,数据量:86
2026-01-14T12:00:28.835+08:00 DEBUG 11848 --- [l-1:housekeeper] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Pool stats (total=10/10, idle=10/10, active=0, waiting=0)

结论:

  • 无设备表查询日志,4个设备ID全命中缓存,仅执行核心业务SQL,无冗余查询;

  • 数据库连接池全空闲,设备查询维度数据库压力归零;

  • 接口耗时297ms,较首次请求显著降低,本地缓存毫秒级响应生效。

4.2.3 缓存一致性与过期验证

  • Caffeine过期验证:600秒后重新查询,本地缓存失效,Redis仍命中,同步预热本地缓存,符合全局过期规则;

  • Redis差异化过期验证:5分钟后查询无效设备ID,Redis缓存失效,重新查库后写入NULL缓存,1小时后有效数据缓存失效,符合差异化策略;

  • 缓存一致性验证:更新设备名称后,双维度缓存同步清理,再次查询加载最新数据,无脏数据残留。

5. 运维与监控

5.1 缓存监控指标

  • 缓存命中率:本地Caffeine缓存命中率≥95%(开启recordStats后可通过监控获取),Redis缓存命中率≥90%;

  • Caffeine指标:通过缓存统计获取命中次数、未命中次数、缓存大小,确保不超过最大容量(1000);

  • Redis指标:连接数、内存占用、过期key数量,通过Redis CLI或Prometheus+Grafana监控。

5.2 缓存清理操作

5.2.1 手动清理(应急场景)

  • 清理单个设备缓存:调用CacheManagerUtils.evictCache("deviceInfoCache", 标识)(标识为deviceId或id);

  • 清理全量设备缓存:调用CacheManagerUtils.clearCache("deviceInfoCache"),同步清理两级缓存。

5.2.2 自动清理

基于配置的过期时间自动清理:Caffeine按600秒全局规则过期,Redis按KEY级差异化时间过期;Caffeine缓存超过最大容量时,自动按LRU策略淘汰冷数据。

5.3 故障排查

故障现象 可能原因 排查方案
缓存命中率低 1. Caffeine过期时间过短;2. 设备ID高频变化;3. 缓存未预热。 1. 调整yml中Caffeine过期时间;2. 优化缓存预热逻辑;3. 排查设备ID变化原因。
缓存穿透 1. NULL空值缓存未写入;2. 缓存判断逻辑错误。 1. 检查缓存写入日志,确认NULL缓存生成;2. 验证业务层缓存判断逻辑,区分「无key」与「有key值为NULL」。
Caffeine缓存未生效 1. 组合缓存管理器优先级配置错误;2. 缓存名称未加入CaffeineCacheManager白名单。 1. 确认组合缓存管理器中Caffeine在前、Redis在后(保证查询优先级);2. 验证setCacheNames包含deviceInfoCache(限定缓存实例);3. 核对spring.cache.type=redis配置未覆盖组合管理器逻辑(仅影响默认缓存管理器)。

6. 注意事项与扩展建议

6.1 注意事项

  • 禁止缓存高频更新数据:设备基础数据更新频率低,适合缓存;实时测试数据不建议缓存;

  • 避免缓存大key:设备信息需控制在1KB以内,防止Redis内存占用过高及序列化性能损耗;

  • 注解使用规范:@Cacheable需根据场景控制是否支持NULL值,避免穿透或缓存失效;

  • Caffeine配置不变更:全局过期时间、容量等规则需通过yml统一配置,禁止硬编码修改;

  • 依赖管理规范:严格遵循版本适配规则,Spring Boot 3.x不可使用Caffeine 2.x版本,避免API调用异常;若需切换Redis客户端(如Jedis),需排除spring-boot-starter-data-redis中的Lettuce依赖,再引入Jedis依赖。

6.2 扩展建议

  • 缓存预热:系统启动时,通过批量查询加载高频设备数据至两级缓存,提升首次请求命中率;

  • Redis集群:生产环境部署Redis主从+哨兵集群,保障分布式缓存高可用;

  • 缓存降级:Redis故障时,自动降级为本地Caffeine缓存,避免接口雪崩;

  • 监控告警:通过Prometheus采集Caffeine统计指标与Redis指标,配置命中率低于阈值、缓存溢出时告警。

相关推荐
程序修理员2 小时前
oracle备份表还原
数据库·oracle
科创致远2 小时前
国内ESOP电子作业系统头部企业格局与科创致远技术发展历程
大数据·数据库·人工智能·嵌入式硬件·精益工程
超级种码3 小时前
Redis:Redis持久化机制
数据库·redis·bootstrap
阿里-于怀3 小时前
AgentScope AutoContextMemory:告别 Agent 上下文焦虑
android·java·数据库·agentscope
数据库那些事儿3 小时前
从极速复制“死了么”APP,看AI编程时代的技术选型
数据库
岁岁种桃花儿3 小时前
MySQL知识汇总:讲一讲MySQL中Select语句的执行顺序
数据库·mysql·database
am心3 小时前
学习笔记-缓存&添加购物车
笔记·学习·缓存
言之。3 小时前
Django原子请求
数据库·django·sqlite
Codeking__4 小时前
Redis初识——Redis的基本特性
数据库·redis·缓存