多级缓存(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指标,配置命中率低于阈值、缓存溢出时告警。

相关推荐
科技小花34 分钟前
数据治理平台架构演进观察:AI原生设计如何重构企业数据管理范式
数据库·重构·架构·数据治理·ai-native·ai原生
一江寒逸36 分钟前
零基础从入门到精通MySQL(中篇):进阶篇——吃透多表查询、事务核心与高级特性,搞定复杂业务SQL
数据库·sql·mysql
D4c-lovetrain37 分钟前
linux个人心得22 (mysql)
数据库·mysql
阿里小阿希1 小时前
CentOS7 PostgreSQL 9.2 升级到 15 完整教程
数据库·postgresql
荒川之神1 小时前
Oracle 数据仓库雪花模型设计(完整实战方案)
数据库·数据仓库·oracle
做个文艺程序员1 小时前
MySQL安全加固十大硬核操作
数据库·mysql·安全
不吃香菜学java2 小时前
Redis简单应用
数据库·spring boot·tomcat·maven
一个天蝎座 白勺 程序猿2 小时前
Apache IoTDB(15):IoTDB查询写回(INTO子句)深度解析——从语法到实战的ETL全链路指南
数据库·apache·etl·iotdb
不知名的老吴2 小时前
Redis的延迟瓶颈:TCP栈开销无法避免
数据库·redis·缓存
YOU OU2 小时前
三大范式和E-R图
数据库