多级缓存(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缓存;实际缓存查询优先级由组合缓存管理器强制定义,核心架构流程如下:
-
请求到达业务层后,由CompositeCacheManager调度,优先查询本地Caffeine缓存(配置类中定义Caffeine为第一优先级);
-
本地缓存命中(含有效数据/NULL空值):直接返回结果,无数据库/Redis交互,依托内存实现亚毫秒级响应;
-
本地缓存未命中(无对应key):查询Redis分布式缓存(默认缓存管理器,承接兜底逻辑);
-
Redis缓存命中:返回结果,并同步至本地Caffeine缓存(预热本地缓存,提升后续请求性能);
-
Redis缓存未命中:执行单次批量数据库查询,查询结果按规则写入两级缓存(Redis按差异化时间过期,Caffeine按全局规则过期),返回结果;
-
缓存更新/删除:通过注解或工具类触发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 缓存一致性策略
针对设备数据增删改操作,采用"注解+工具类"双重清理机制,确保缓存与数据库数据一致:
-
单设备更新/删除:通过
@CacheEvict注解清理主键ID对应的缓存,工具类补充清理设备编号对应的缓存; -
批量删除:查询待删除设备列表,通过工具类批量清理设备编号维度缓存;
-
新增设备:无需主动清理缓存,后续查询自动加载最新数据写入缓存;
-
兜底机制:两级缓存均配置过期时间,即使清理操作遗漏,数据也会随过期自动更新。
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 依赖版本管理建议
-
版本适配:Spring Boot 3.x需搭配Caffeine 3.x版本,Spring Boot 2.x需搭配Caffeine 2.x版本,避免API兼容问题;
-
统一配置:在pom.xml的标签中配置版本号<caffeine.version>3.1.8</caffeine.version>,便于全局维护;
-
依赖排除:若项目中存在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级差异化过期。
-
过期时间来源:由yml配置
spring.cache.caffeine.spec定义,当前配置为expireAfterWrite=600s,即数据写入后600秒过期; -
配置同步:通过
Caffeine.from(caffeineSpec)加载配置,与初始容量(initialCapacity=50)、最大容量(maximumSize=1000)、缓存统计(recordStats)等规则一同生效; -
与Redis协同:Redis负责KEY级差异化过期(有效数据1小时、NULL值5分钟),且作为默认缓存管理器承接兜底逻辑;Caffeine按全局600秒过期兜底,避免本地缓存长期残留脏数据,二者通过组合管理器形成互补;
-
补充机制:即使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指标,配置命中率低于阈值、缓存溢出时告警。