已经描述了Spring Cache的基本使用。

本文优化方向

SpringCache主动尝试连接Redis,如果无法连接,缓存降级为默认的JVM缓存。

追加TTL

如果缓存异常,打印错误日志

上代码

注释关SpringChche读取配置文件的缓存。SpringCache自动装配机制,可能导致我们配置类不生效。造成Redis连接异常、无法启动等问题

spring:
  cache:
    # 指定缓存名称,
    cache-names: myCache
    # 指定缓存种类,可选redis,IDEA会提示你的
    # type: redis

添加配置类

import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurer;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.cache.interceptor.CacheErrorHandler;
import org.springframework.cache.interceptor.SimpleCacheErrorHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
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 org.springframework.util.StringUtils;

import java.time.Duration;

/**
 * 缓存配置类
 * 1. 自动检测Redis连接,如果Redis不可用则降级为本地Map缓存
 * 2. 支持通过 cacheName#ttl 格式设置过期时间(单位:秒)
 * 3. 配置 JSON 序列化,方便调试
 * 4. 配置 CacheErrorHandler,运行时缓存异常不影响业务
 *
 * @author zanglikun
 * @since 2026/3/5 16:30
 */
@Slf4j
@Configuration
@EnableCaching
public class CacheConfig implements CachingConfigurer {

    // 用于设置Redis缓存Key失效时间的连接符。
    // 主要用改装于:@Cacheable(cacheNames = "myCache#300") 这里的300就是你的时间数字。单位是秒
    private static final String TTL_SEPARATOR = "#";

    @Bean
    @Primary
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        try {
            // 尝试获取连接以检查Redis是否可用
            redisConnectionFactory.getConnection().close();
            
            log.info("Redis连接成功,启用 CustomRedisCacheManager (支持 #ttl 后缀)");
            
            // 配置Redis缓存: 默认过期1小时 + JSON序列化
            RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
                    .entryTtl(Duration.ofHours(1))
                    .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                    .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
            
            // 使用自定义的 RedisCacheManager
            return new CustomRedisCacheManager(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory), defaultCacheConfig);
            
        } catch (Exception e) {
            log.error("Redis连接失败 ({} ),SpringCache自动降级为 ConcurrentMapCacheManager (本地内存缓存)", e.getMessage());
            // 降级为本地内存缓存
            return new ConcurrentMapCacheManager();
        }
    }

    /**
     * 运行时缓存异常处理策略
     * 当Redis操作(Get/Put/Evict)失败时,仅打印日志,不中断业务逻辑
     */
    @Override
    public CacheErrorHandler errorHandler() {
        return new SimpleCacheErrorHandler() {
            @Override
            public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
                log.error("Cache Get Error: cacheName={}, key={}, msg={}", cache.getName(), key, exception.getMessage());
                // 视为缓存未命中,继续查库
            }

            @Override
            public void handleCachePutError(RuntimeException exception, Cache cache, Object key, Object value) {
                log.error("Cache Put Error: cacheName={}, key={}, msg={}", cache.getName(), key, exception.getMessage());
                // 写入缓存失败,但业务数据已更新,不影响主流程
            }

            @Override
            public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) {
                log.error("Cache Evict Error: cacheName={}, key={}, msg={}", cache.getName(), key, exception.getMessage());
                // 缓存清理失败,可能会导致短暂的数据不一致,但保证业务可用性
            }

            @Override
            public void handleCacheClearError(RuntimeException exception, Cache cache) {
                log.error("Cache Clear Error: cacheName={}, msg={}", cache.getName(), exception.getMessage());
            }
        };
    }

    /**
     * 自定义 RedisCacheManager,支持解析 cacheName#ttl
     */
    public static class CustomRedisCacheManager extends RedisCacheManager {

        public CustomRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
            super(cacheWriter, defaultCacheConfiguration);
        }

        @Override
        protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
            String cacheName = name;

            // 解析 TTL。因为你会在@Cacheable(cacheNames = "myCache#300") 这里逻辑解析你的 300就是你的时间数字。单位是秒
            if (StringUtils.hasText(name) && name.contains(TTL_SEPARATOR)) {
                String[] parts = name.split(TTL_SEPARATOR);
                cacheName = parts[0];
                if (parts.length > 1) {
                    try {
                        long ttl = Long.parseLong(parts[1]);
                        // 设置自定义 TTL
                        cacheConfig = cacheConfig.entryTtl(Duration.ofSeconds(ttl));
                    } catch (NumberFormatException e) {
                        log.warn("解析缓存TTL失败,name: {}, error: {}", name, e.getMessage());
                    }
                }
            }
            return super.createRedisCache(cacheName, cacheConfig);
        }
    }
}

使用

    @Cacheable(cacheNames = "calculateData")
    // 默认缓存1小时
    public String calculateData(String type) {
    }

    @Cacheable(cacheNames = "calculateData#86400")
    // 指定缓存1天
    public String calculateData(String type) {
    }
特殊说明:
上述文章均是作者实际操作后产出。烦请各位,请勿直接盗用!转载记得标注原文链接:www.zanglikun.com
第三方平台不会及时更新本文最新内容。如果发现本文资料不全,可访问本人的Java博客搜索:标题关键字。以获取最新全部资料 ❤

免责声明:
本站文章旨在总结学习互联网技术过程中的经验与见解。任何人不得将其用于违法或违规活动!所有违规内容均由个人自行承担,与作者无关。