Spring Cache+Redis缓存方案详解:从代码到实践

描述:

        在现代Java开发中,缓存是提升系统性能的核心手段之一。本文通过实际代码案例,深入解析Spring Cache与Redis的集成原理,结合项目中的ModuleDatabaseInfoService接口和RedisConfig配置,探讨如何通过声明式缓存实现高效的数据库访问优化。

一、核心代码解析

1. 服务接口设计(拿查询数据源配置信息举例)

public interface ModuleDatabaseInfoService extends TestableService, DataSourceNameProvider {
    @Cacheable(CacheConstant.CACHE_DATASOURCE)
    ModuleDatabaseInfo getDataSourceById(String databaseCode);

    @CacheEvict(CacheConstant.CACHE_DATASOURCE)
    void refreshDataSource(Long databaseId);
}
  • 功能说明
    • getDataSourceById:通过@Cacheable注解缓存数据源配置信息,避免重复查询数据库。
    • refreshDataSource:通过@CacheEvict清除指定ID的缓存,确保数据一致性。
  • 缓存名称CacheConstant.CACHE_DATASOURCE定义了缓存的命名空间,用于隔离不同业务场景的缓存。

2. Redis配置核心

@Bean("jsonRedisCacheManager")
@Primary
public CacheManager customCacheManager(RedisConnectionFactory redisConnectionFactory) {
    // 1. 配置Jackson序列化器
    Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer<>(objectMapper2, Object.class);
    
    // 2. 构建Redis缓存配置
    RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
        .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer))
        .entryTtl(Duration.ofMinutes(10L))  // 默认10分钟过期
        .disableCachingNullValues();         // 禁止缓存空值
    
    // 3. 创建缓存写入器
    RedisCacheWriter redisCacheWriter = RedisCacheWriter.lockingRedisCacheWriter(
        redisConnectionFactory, 
        BatchStrategies.scan(1000)  // 使用SCAN命令批量处理
    );
    
    // 4. 返回自定义缓存管理器
    return new TtlRedisCacheManager(redisCacheWriter, config);
} 
  
  • 关键配置
    • 序列化:使用Jackson2JsonRedisSerializer将对象序列化为JSON,支持复杂类型(如LocalDateTime)。
    • 过期时间:设置全局缓存过期时间为10分钟,避免数据长期滞留。
    • 批量处理:通过BatchStrategies.scan(1000)优化大Key集合操作,避免Redis阻塞。

redisConfig类代码片段分享

/**
 * @date 2023年09月15日
 * @version: 1.0
 */
@Configuration
public class RedisConfig {

    static ObjectMapper objectMapper = Mappers.objectMapper.copy();
    static ObjectMapper objectMapper2 = Mappers.objectMapper.copy();
    static ObjectMapper objectMapper3 = Mappers.objectMapper.copy();
    @Bean(value = "redisTemplate")
    @Primary
    public RedisTemplate stringRedisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(new StringRedisSerializer());

        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);

        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(objectMapper, Object.class);
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setConnectionFactory(connectionFactory);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }




    //redis云服务不能使用keys命令,使用redis缓存可能会有问题
//    /**
//     自定义RedisCacheManager,用于在使用@Cacheable时设置ttl
//     */
//    @Bean
//    public RedisCacheManager redisCacheManager(RedisTemplate redisTemplate) {
//        RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisTemplate.getConnectionFactory());
//        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
//                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisTemplate.getValueSerializer()));
//        return new TtlRedisCacheManager(redisCacheWriter, redisCacheConfiguration);
//    }

    @Bean("jsonRedisCacheManager")
    @Primary
    public CacheManager customCacheManager(RedisConnectionFactory redisConnectionFactory) {
        // 解决查询缓存转换异常的问题
        objectMapper2.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        JavaTimeModule javaTimeModule = new JavaTimeModule();
        objectMapper2.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL,JsonTypeInfo.As.PROPERTY);
        //系列化时间格式化
        javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        //反序列化时间格式化
        javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        //注册进入jackson
        objectMapper2.registerModule(javaTimeModule);

        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(objectMapper2,
                Object.class);
        // Configure Redis cache with Jackson serializer
        RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                // jackson
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                // 默认10分钟过期时间
                .entryTtl(Duration.ofMinutes(10L))
                // 空值不做缓存
                .disableCachingNullValues();

        // redis 服务端考虑性能问题会禁用keys命令全量操作, 改用scan批量操作
        RedisCacheWriter redisCacheWriter = RedisCacheWriter.lockingRedisCacheWriter(redisConnectionFactory, BatchStrategies.scan(1000));
        // Create a cache manager
        return new TtlRedisCacheManager(redisCacheWriter,cacheConfiguration);
    }

    @Bean("listRedisCacheManager")
    public CacheManager customListCacheManager(RedisConnectionFactory redisConnectionFactory) {
        // 解决查询缓存转换异常的问题
        objectMapper3.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        JavaTimeModule javaTimeModule = new JavaTimeModule();
        //系列化时间格式化
        javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        //反序列化时间格式化
        javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        //注册进入jackson
        objectMapper3.registerModule(javaTimeModule);

        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(objectMapper3,
                Object.class);
        // Configure Redis cache with Jackson serializer
        RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                // jackson
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                // 默认10分钟过期时间
                .entryTtl(Duration.ofMinutes(10L))
                // 空值不做缓存
                .disableCachingNullValues();

        // redis 服务端考虑性能问题会禁用keys命令全量操作, 改用scan批量操作
        RedisCacheWriter redisCacheWriter = RedisCacheWriter.lockingRedisCacheWriter(redisConnectionFactory, BatchStrategies.scan(1000));
        // Create a cache manager
        return new TtlRedisCacheManager(redisCacheWriter,cacheConfiguration);
    }

    @Bean
    @Primary
    public KeyGenerator defaultKeyGenerator(){
        return new JsonCacheKeyGenerator(objectMapper);
    }
}
 
  

二、Spring Cache与Redis的深度集成

1. 声明式缓存原理

Spring Cache通过@Cacheable@CacheEvict注解简化缓存逻辑:

  • @Cacheable:方法执行前检查缓存,命中则直接返回结果;未命中则执行方法并将结果存入缓存。
  • @CacheEvict:方法执行后清除指定缓存条目,确保数据更新后缓存同步。
1.1声明式缓存详情介绍

请参考另一篇文章:@Cacheable 和 @CacheEvict 注解的详细使用说明及参数解析,结合 Spring Cache 的核心功能和实际开发场景-CSDN博客

2. 缓存键生成策略

默认使用JsonCacheKeyGenerator生成基于JSON内容的唯一键,例如:

CACHE_DATASOURCE::DB_001

通过ObjectMapper的序列化配置,确保不同参数生成不同的键,避免缓存冲突。

3. RedisTemplate配置

@Bean(value = "redisTemplate")
@Primary
public RedisTemplate stringRedisTemplate(...) {
    redisTemplate.setKeySerializer(new StringRedisSerializer());
    redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); // 使用Jackson序列化值
}
  • 作用
    • 键序列化:使用StringRedisSerializer保证键的可读性。
    • 值序列化:通过Jackson处理复杂对象,支持反序列化时还原类型信息。

三、缓存穿透问题及解决方案

1. 问题分析

当查询不存在的数据时(如databaseCode="invalid"),如果未缓存空值,每次请求都会穿透到数据库,导致性能问题。

2. 当前配置的局限性

.disableCachingNullValues() // 禁止缓存空值

此配置虽然节省了存储空间,但无法防止缓存穿透。

3. 解决方案对比

方案 实现方式 优点 缺点
缓存空值 .enableCachingNullValues() 简单有效,拦截后续请求 占用缓存空间
布隆过滤器 额外引入布隆过滤器组件 内存占用小,拦截非法请求 需维护额外组件
限流+短TTL 缓存空值并设置短TTL(如30秒) 平衡存储与性能 需配合限流策略

4. 推荐实践

在缓存配置中启用空值缓存并设置短TTL:

.enableCachingNullValues()
.entryTtl(Duration.ofSeconds(30))

同时,在服务层添加限流逻辑:

@Cacheable(CacheConstant.CACHE_DATASOURCE)
public ModuleDatabaseInfo getDataSourceById(String databaseCode) {
    if (!rateLimiter.tryAcquire()) {
        throw new RateLimitException("请求过于频繁");
    }
    // 查询数据库逻辑
}

四、自定义缓存管理器的优势

1. TtlRedisCacheManager

通过继承RedisCacheManager,实现自定义TTL控制:

new TtlRedisCacheManager(redisCacheWriter, config)
  • 动态TTL:支持为不同缓存名称设置独立过期时间。
  • 线程安全:使用lockingRedisCacheWriter保证并发写入安全。

2. 多缓存管理器场景

项目中定义了多个CacheManager(如jsonRedisCacheManagerlistRedisCacheManager),分别处理普通对象和列表结构,提升灵活性:

@Bean("listRedisCacheManager")
public CacheManager customListCacheManager(...) {
    return new TtlRedisCacheManager(redisCacheWriter, config);
}

五、总结与最佳实践

1. 核心优势

  • 高性能:通过Redis和Spring Cache的集成,减少数据库压力。
  • 可维护性:声明式注解与集中式配置分离业务逻辑与缓存管理。
  • 扩展性:自定义缓存管理器支持灵活的TTL和序列化策略。

2. 注意事项

  • 避免缓存雪崩:为缓存项设置随机过期时间。
  • 监控与告警:通过Redis的INFO命令监控缓存命中率。
  • 测试验证:使用@SpringBootTest模拟缓存命中与失效场景。

3. 适用场景

  • 高频读取场景:如数据库连接池配置、用户权限信息。
  • 分布式系统:通过Redis共享缓存,保证多节点一致性。

        通过以上设计,项目实现了高效、灵活的缓存管理,为动态数据源的访问提供了稳定支撑。实际开发中,可根据业务需求调整序列化策略、TTL和缓存穿透防护方案,进一步优化性能与可靠性。

你可能感兴趣的:(Java,Java项目实战,Redis,spring,缓存,redis)