1510 字
8 分钟
在 Springboot 中配置可开关的缓存

SpringBoot项目中构建可开关的Redis和Caffeine缓存系统#

在现代Web应用中,缓存是提升系统性能的重要手段。特别是在处理大规模数据时,合理的缓存策略可以显著减少数据库压力,提高响应速度。本文将详细介绍如何在SpringBoot项目中构建一个灵活可配置的Redis和Caffeine双重缓存系统,重点介绍”可开关”机制的实现。

背景与挑战#

在Aristotle知识图谱中,需要处理数百万个节点和关系的数据。面对这样的数据规模,我们遇到了两个主要挑战:

  1. 长查询时间:即使是小规模的子图扩展操作,在大数据量下也可能导致查询缓慢
  2. 查询超时:由于内存不足导致的查询超时问题

为了解决这些问题,我们设计了一个双层缓存架构:使用Caffeine作为本地缓存,Redis作为分布式缓存。

架构设计#

缓存层次结构#

┌─────────────────┐
│   应用层        │
├─────────────────┤
│   Caffeine      │ ← 本地缓存(一级缓存)
│   (本地内存)     │
├─────────────────┤
│   Redis         │ ← 分布式缓存(二级缓存)
│   (远程存储)     │
├─────────────────┤
│   Neo4j         │ ← 数据源
│   (数据库)       │
└─────────────────┘

缓存策略#

  • Caffeine缓存:用于存储频繁访问的图数据,提供最快的访问速度
  • Redis缓存:用于存储预计算的子图数据,支持分布式访问
  • 可配置开关:支持独立开启/关闭每种缓存类型

可开关机制的核心实现#

1. 配置驱动的开关控制#

配置文件设计#

spring:
  read-cache:
    enabled: true          # Caffeine缓存开关
    num-subgraphs: 500     # 缓存条目数量限制
  
  redis:
    enabled: false         # Redis缓存开关
    host: 127.0.0.1
    port: 6379

配置类实现#

Caffeine配置类:通过@Value注解读取配置,根据enabled属性决定是否创建有效缓存

@Configuration
public class CaffeineConfig {
    
    @Value("${spring.read-cache.enabled:true}")
    private boolean cacheEnabled;
    
    @Bean
    public Cache<String, GraphVO> graphCache() {
        if (!cacheEnabled) {
            // 返回空缓存,实际不存储任何数据
            return Caffeine.newBuilder().build();
        }
        // 返回正常配置的缓存
        return Caffeine.newBuilder()
                .maximumSize(500)
                .expireAfterWrite(2, TimeUnit.MINUTES)
                .build();
    }
}

Redis配置类:使用@ConditionalOnProperty注解,只有当spring.redis.enabled=true时才创建Redis相关Bean

@Configuration
@ConditionalOnProperty(value = "spring.redis.enabled", havingValue = "true")
public class RedisConfig {
    // Redis配置只在启用时才生效
}

自动配置排除:当Redis禁用时,排除Redis自动配置

@Configuration
@ConditionalOnProperty(value = "spring.redis.enabled", havingValue = "false")
@EnableAutoConfiguration(exclude = {RedisAutoConfiguration.class})
public class ExcludeAutoConfiguration {
}

2. 工具类的开关控制#

Caffeine缓存工具#

@Component
public class CaffeineCacheUtil {
    
    @Value("${spring.read-cache.enabled}")
    private boolean cacheEnabled;
    
    public Optional<GraphVO> getCache(final String key) {
        if (cacheEnabled) {
            // 执行缓存查询
            return Optional.ofNullable(graphCache.getIfPresent(key));
        }
        return Optional.empty(); // 缓存禁用时直接返回空
    }
    
    public void setCache(final String key, final GraphVO graphVO) {
        if (cacheEnabled) {
            // 执行缓存存储
            graphCache.put(key, graphVO);
        }
        // 缓存禁用时什么都不做
    }
}

Redis缓存工具#

@Component
@ConditionalOnProperty(name = "spring.redis.enabled", havingValue = "true")
public class RedisCacheUtil {
    
    @Value("${spring.redis.enabled}")
    private boolean cacheEnabled;
    
    public <T> T getCacheObject(final String key) {
        if (!cacheEnabled) {
            return null;
        }
        return redisTemplate.opsForValue().get(key);
    }
    
    public <T> void setCacheObject(final String key, final T value) {
        if (!cacheEnabled) {
            return;
        }
        redisTemplate.opsForValue().set(key, value);
    }
}

3. 服务层的优雅降级#

在服务层中,我们使用@Autowired(required = false)来注入Redis缓存工具,这样当Redis禁用时不会报错:

@Service
public class GraphServiceImpl implements GraphService {
    
    @Autowired
    private CaffeineCacheUtil caffeineCache;
    
    @Autowired(required = false)  // 关键:Redis禁用时不会注入
    private RedisCacheUtil redisCache;
    
    @Override
    public GraphVO getGraphVOByFilterParams(final FilterQueryGraphDTO dto) {
        final String cacheKey = generateCacheKey(dto.getUuid(), dto.getPageNumber(), dto.getPageSize());
        
        // 1. 首先检查Caffeine缓存
        final Optional<GraphVO> cachedGraphVO = caffeineCache.getCache(cacheKey);
        if (cachedGraphVO.isPresent()) {
            return cachedGraphVO.get();
        }
        
        // 2. 然后检查Redis缓存(如果启用)
        if (redisCache != null && redisCache.getCacheObject(cacheKey) != null) {
            return redisCache.getCacheObject(cacheKey);
        }
        
        // 3. 从数据库获取数据
        final GraphVO graphVO = fetchFromDatabase(dto);
        
        // 4. 同时缓存到两个缓存(如果启用)
        caffeineCache.setCache(cacheKey, graphVO);
        if (redisCache != null) {
            redisCache.setCacheObject(cacheKey, graphVO, 30, TimeUnit.MINUTES);
        }
        
        return graphVO;
    }
}

可开关机制的优势#

1. 灵活的部署策略#

开发环境:仅使用Caffeine缓存,避免Redis依赖

spring:
  read-cache:
    enabled: true
  redis:
    enabled: false

测试环境:仅使用Redis缓存,测试分布式场景

spring:
  read-cache:
    enabled: false
  redis:
    enabled: true

生产环境:同时使用两种缓存,获得最佳性能

spring:
  read-cache:
    enabled: true
  redis:
    enabled: true

2. 故障隔离#

  • Redis故障时:系统自动降级到Caffeine缓存,保证基本功能可用
  • 内存不足时:可以临时禁用Caffeine缓存,仅使用Redis
  • 网络问题时:可以禁用Redis,避免连接超时影响性能

3. 性能调优#

  • 高并发场景:启用Caffeine缓存,减少网络开销
  • 大数据量场景:启用Redis缓存,利用分布式存储
  • 内存受限场景:调整缓存大小或禁用部分缓存

配置示例#

不同场景的配置#

仅使用Caffeine缓存#

spring:
  read-cache:
    enabled: true
    num-subgraphs: 1000
  redis:
    enabled: false

仅使用Redis缓存#

spring:
  read-cache:
    enabled: false
  redis:
    enabled: true
    host: redis-server
    port: 6379

同时使用两种缓存#

spring:
  read-cache:
    enabled: true
    size: 1GB
  redis:
    enabled: true
    host: redis-server
    port: 6379

实现要点总结#

1. 配置驱动#

  • 使用@Value注解读取配置开关
  • 使用@ConditionalOnProperty控制Bean创建
  • 使用@EnableAutoConfiguration(exclude = {...})排除不需要的自动配置

2. 优雅降级#

  • 使用@Autowired(required = false)避免依赖注入失败
  • 在方法中检查开关状态,禁用时直接返回
  • 提供多级缓存策略,确保系统可用性

3. 一致性保证#

  • 配置开关与Bean创建保持一致
  • 缓存操作与开关状态同步
  • 避免配置不一致导致的问题

总结#

通过这种可开关的缓存设计,我们实现了:

  1. 灵活性:支持独立配置每种缓存类型,适应不同部署环境
  2. 可靠性:故障时自动降级,保证系统可用性
  3. 可维护性:清晰的配置管理,便于运维和调试
  4. 可扩展性:易于添加新的缓存策略和优化

这种设计不仅适用于知识图谱项目,也可以推广到其他需要高性能缓存的SpringBoot应用中。通过合理的开关机制,我们可以根据实际需求灵活调整缓存策略,提供更好的服务体验。

在 Springboot 中配置可开关的缓存
https://doom9527.github.io/blog/posts/ariticle-05/cache-doc/
作者
Doom9527
发布于
2025-06-24
许可协议
CC BY-NC-SA 4.0