SpringBoot自定义Redis


RedisCacheConfiguration 的全局定制

在 Spring Boot 整合 Redis 的过程中,默认的缓存配置(RedisCacheConfiguration)虽然开箱即用,但在生产环境中往往存在以下痛点:

  1. 数据不可读:默认使用 JDK 序列化,Redis 中存储的是二进制乱码。
  2. Key 命名冗余 :默认使用双冒号 :: 分隔,导致在 Redis 可视化工具中出现空文件夹层级。
  3. 缺乏过期限制:默认缓存永不过期,可能导致 Redis 内存堆积。

本章将详细解析如何通过自定义 RedisCacheConfiguration Bean,一次性解决上述问题,实现规范化的缓存存储策略。

一、 RedisCacheConfiguration 的核心职责

RedisCacheConfiguration 是 Spring Data Redis 提供的一个不可变(Immutable)配置类。它的核心职责是定义缓存数据的属性格式

由于它是不可变的,任何修改方法(如 entryTtl)都不会修改当前对象,而是返回一个新的配置实例。我们主要通过以下四个维度进行定制:

  1. 序列化策略 (Serialization):Key 和 Value 的二进制转换规则。
  2. 前缀策略 (Prefixing):Key 的命名空间生成规则。
  3. 生存时间 (TTL):全局默认的过期时间。
  4. 空值处理 (Null Handling):防止缓存穿透的策略。

二、 核心定制点详解

1. 序列化定制:从 JDK 二进制到 JSON

Spring 默认使用 JdkSerializationRedisSerializer

  • 缺点 :生成的 Value 是 \xac\xed\x00\x05... 这样的二进制流,无法人工阅读,且 Java 强依赖,这就限制了其他语言(如 Python/Go)读取该缓存。
  • 解决 :通常替换为 GenericJackson2JsonRedisSerializer,将对象存储为标准的 JSON 字符串。需要注意处理 LocalDateTime 等 Java 8 时间类型的序列化问题。

2. 前缀策略定制:去除双冒号

Spring 默认生成的 Key 格式为 CacheName::Key

  • 缺点 :双冒号 :: 在 Redis Desktop Manager 等工具中会被解析为两层目录,其中一层是空的,视觉效果差。
  • 解决 :通过 computePrefixWith 方法,将分隔符修改为单冒号 :,例如 CacheName:Key

3. 过期时间定制

默认配置是 Duration.ZERO(永不过期)。建议在全局配置中设置一个合理的默认值(如 1 小时),作为兜底策略。

三、 完整代码实践

下面是一个生产级可用的配置示例。将此代码放入你的 Spring Boot 项目的配置包下即可生效。

java 复制代码
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
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;

@Configuration
@EnableCaching // 开启缓存注解支持
public class RedisConfig {

    /**
     * 自定义 RedisCacheConfiguration
     * 作用:定制 Key/Value 的序列化方式、过期时间、Key 前缀格式
     */
    @Bean
    public RedisCacheConfiguration redisCacheConfiguration() {
        // 1. 构造 JSON 序列化器
        // 使用 GenericJackson2JsonRedisSerializer 替换默认序列化
        GenericJackson2JsonRedisSerializer jsonSerializer = buildJsonSerializer();

        // 2. 构建配置
        return RedisCacheConfiguration.defaultCacheConfig()
                // 【核心 1】设置默认过期时间:1 小时
                .entryTtl(Duration.ofHours(1))
                
                // 【核心 2】定制 Key 的序列化:使用 String (默认就是 String,显式指定更清晰)
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                
                // 【核心 3】定制 Value 的序列化:使用 JSON
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jsonSerializer))
                
                // 【核心 4】定制 Key 前缀策略:CacheName + ":" + Key
                // 默认是 "::",这里改为 ":",避免 RDM 工具出现空目录
                .computePrefixWith(name -> name + ":")
                
                // 【可选】禁止缓存 Null 值 (根据业务需求决定是否开启)
                // .disableCachingNullValues()
                ;
    }

    /**
     * 辅助方法:构建支持 Java8 时间和类型信息的 JSON 序列化器
     */
    private GenericJackson2JsonRedisSerializer buildJsonSerializer() {
        ObjectMapper objectMapper = new ObjectMapper();
        
        // 1. 注册 JavaTimeModule,解决 LocalDateTime 等时间类序列化问题
        objectMapper.registerModule(new JavaTimeModule());
        
        // 2. 启用多态类型验证,将类信息 (@class) 写入 JSON
        // 这样从 Redis 读出来时,才能自动转回具体的 Java 对象,而不仅仅是 Map
        objectMapper.activateDefaultTyping(
                LaissezFaireSubTypeValidator.instance,
                ObjectMapper.DefaultTyping.NON_FINAL,
                JsonTypeInfo.As.PROPERTY
        );

        return new GenericJackson2JsonRedisSerializer(objectMapper);
    }
}

四、 配置前后的效果对比

假设我们有一个 @Cacheable(value = "users", key = "1001") 的缓存操作,存储一个 User 对象。

1. 修改前(默认配置)

  • Redis Key : users::1001 (双冒号)
  • Redis Value : \xac\xed\x00\x05sr\x00\x1bcom.example.User... (二进制乱码)
  • TTL: -1 (永不过期)

2. 修改后(自定义配置)

  • Redis Key : users:1001 (单冒号,目录结构清晰)
  • Redis Value:
json 复制代码
{
  "@class": "com.example.User",
  "id": 1001,
  "username": "admin",
  "createTime": [2023, 10, 1, 12, 0, 0]
}

(标准 JSON,可读性强,其他语言可读)

  • TTL: 3600 (1小时后自动删除)


进阶定制 RedisCacheManager实现动态过期时间

在上一章中,我们通过 RedisCacheConfiguration 解决了序列化和 Key 命名格式的问题。然而,在实际生产环境中,我们面临一个新的挑战:缓存的生命周期管理

默认的 RedisCacheManager 虽然支持设置全局过期时间(例如统一 1 小时),但业务场景往往更加复杂:

  • 验证码:需要 5 分钟过期。
  • 用户会话:需要 30 分钟过期。
  • 首页广告:需要 24 小时过期。
  • 系统参数:可能需要永不过期。

如果使用 Spring 原生方式,我们需要在配置类中构建一个巨大的 Map<String, RedisCacheConfiguration> 来一一指定,这既繁琐又难以维护。

本章将介绍如何通过继承并扩展 RedisCacheManager,实现基于注解后缀的动态过期时间控制 (例如:@Cacheable(value = "users#30m"))。

一、 核心思路:重写工厂方法

RedisCacheManager 的本质是一个工厂(Factory) ,负责生产 RedisCache 对象。其核心方法是 createRedisCache(String name, RedisCacheConfiguration cacheConfig)

我们的改造思路遵循 "约定优于配置" 的原则:

  1. 拦截 :在创建缓存实例前,拦截传入的 name(缓存名称)。
  2. 解析 :判断 name 中是否包含特定的分隔符(如 #),提取时间信息。
  3. 定制 :如果包含时间信息,则基于全局配置衍生出一个新的配置对象,并修改其 TTL。
  4. 还原 :将 name 还原为干净的业务名称(去除时间后缀),创建缓存实例。

二、 代码实现

下面是一个生产级的 TimeoutRedisCacheManager 实现。它支持 d (天), h (小时), m (分钟), s (秒) 等时间单位。

1. 自定义 Manager 类

java 复制代码
import cn.hutool.core.util.StrUtil; // 示例使用了 Hutool 工具类,也可使用原生 String 方法
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 java.time.Duration;

public class TimeoutRedisCacheManager extends RedisCacheManager {

    private static final String SPLIT_FLAG = "#";

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

    /**
     * 核心重写方法:在创建 Cache 对象时介入
     */
    @Override
    protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
        // 1. 如果名称为空或不包含分隔符,直接使用父类逻辑(应用全局默认配置)
        if (StrUtil.isEmpty(name) || !name.contains(SPLIT_FLAG)) {
            return super.createRedisCache(name, cacheConfig);
        }

        // 2. 切割字符串
        // 格式约定:cacheName#ttl,例如 "system_users#30m"
        String[] parts = name.split(SPLIT_FLAG);
        if (parts.length != 2) {
            return super.createRedisCache(name, cacheConfig);
        }
        
        // 3. 提取真实缓存名和时间字符串
        String realName = parts[0]; // "system_users"
        String ttlStr = parts[1];   // "30m"

        // 4. 解析时间,并修改配置
        // 注意:RedisCacheConfiguration 是不可变的,entryTtl 会返回新实例
        Duration duration = parseDuration(ttlStr);
        if (cacheConfig != null) {
            cacheConfig = cacheConfig.entryTtl(duration);
        }

        // 5. 使用真实的名称和定制后的配置创建缓存
        // 这样 Redis 中的 Key 就是 "prefix:system_users:key",不会带上 "#30m"
        return super.createRedisCache(realName, cacheConfig);
    }

    /**
     * 辅助方法:解析时间字符串
     * 支持格式:1d, 4h, 30m, 60s
     */
    private Duration parseDuration(String ttlStr) {
        String timeUnit = StrUtil.subSuf(ttlStr, -1).toLowerCase(); // 获取最后一位单位
        long timeValue = Long.parseLong(StrUtil.sub(ttlStr, 0, ttlStr.length() - 1)); // 获取数值
        
        switch (timeUnit) {
            case "d":
                return Duration.ofDays(timeValue);
            case "h":
                return Duration.ofHours(timeValue);
            case "m":
                return Duration.ofMinutes(timeValue);
            case "s":
                return Duration.ofSeconds(timeValue);
            default:
                // 默认单位为秒,或者根据业务需求抛出异常
                return Duration.ofSeconds(Long.parseLong(ttlStr));
        }
    }
}

2. 注册 Bean

将自定义的 Manager 注册到 Spring 容器中,替换默认的 CacheManager。

java 复制代码
@Bean
public RedisCacheManager redisCacheManager(RedisTemplate<String, Object> redisTemplate,
                                           RedisCacheConfiguration redisCacheConfiguration) {
    // 1. 获取连接工厂
    RedisConnectionFactory factory = Objects.requireNonNull(redisTemplate.getConnectionFactory());

    // 2. 创建无锁的 CacheWriter
    // BatchStrategies.scan(1000) 是为了在使用 evict(allEntries=true) 时
    // 使用 SCAN 命令代替 KEYS 命令,防止阻塞 Redis 主线程
    RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(factory,
            BatchStrategies.scan(1000));

    // 3. 返回自定义的 TimeoutRedisCacheManager
    return new TimeoutRedisCacheManager(cacheWriter, redisCacheConfiguration);
}

三、 使用效果演示

完成上述配置后,开发人员在编写业务代码时,可以极其灵活地控制缓存时间:

场景 A:使用默认时间

java 复制代码
// 不带后缀,使用 RedisCacheConfiguration 配置的全局默认时间(如 1 小时)
@Cacheable(value = "system_dict", key = "#type")
public List<Dict> getDictByType(String type) { ... }
  • Redis Key : system_dict:sex
  • TTL: 1 小时

场景 B:自定义短缓存

java 复制代码
// 后缀 #60s,指定 60 秒过期
@Cacheable(value = "sms_code#60s", key = "#mobile")
public String getVerifyCode(String mobile) { ... }
  • Redis Key : sms_code:13800138000 (注意:后缀被去除了)
  • TTL: 60 秒

场景 C:自定义长缓存

java 复制代码
// 后缀 #1d,指定 1 天过期
@Cacheable(value = "daily_ranking#1d", key = "#date")
public Ranking getDailyRanking(String date) { ... }
  • Redis Key : daily_ranking:20231001
  • TTL: 24 小时


深度定制 RedisCache 实现多级缓存与监控

在掌握了 RedisCacheConfiguration(定制属性)和 RedisCacheManager(定制策略)之后,我们来到了 Spring Cache 定制的"深水区"------自定义 RedisCache

前两者的定制局限于"配置"层面(数据存什么格式、存多久),而自定义 RedisCache 则是直接介入缓存的运行时行为(Runtime Behavior)

当你遇到以下需求时,必须重写 RedisCache

  1. 多级缓存:在访问 Redis 前,先访问本地堆内存(Caffeine/HashMap),以减少网络 IO。
  2. 读写分离/双写:写入缓存时需要同步写入其他存储介质。
  3. 埋点监控:需要精确统计缓存的命中率、读取耗时,并上报监控系统(如 Prometheus)。
  4. 容错降级:当 Redis 宕机时,不抛出异常,而是记录日志并回源查询数据库。

本章将通过实现一个简单的 "内存 + Redis 二级缓存" 组件,展示如何重写 RedisCache 的核心生命周期。

一、 核心原理:RedisCache 的职责

org.springframework.data.redis.cache.RedisCache 是 Spring 官方提供的核心实现类。它继承自 AbstractValueAdaptingCache

我们主要关注以下三个方法的重写:

  • lookup(Object key) :对应 @Cacheable 的查询操作。底层核心。
  • put(Object key, Object value) :对应 @Cacheable 的写入和 @CachePut
  • evict(Object key) :对应 @CacheEvict

二、 代码实现:构建二级缓存 (Level2RedisCache)

我们将构建一个 Level2RedisCache。它的逻辑是:

  • :先查 L1(本地内存),有则返回;无则查 L2(Redis);如果 Redis 有,则回填 L1。
  • :同时写入 L1 和 L2。
  • :同时删除 L1 和 L2。

1. 自定义 Cache 类

java 复制代码
import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheWriter;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 自定义二级缓存实现
 * L1: JVM 本地内存 (使用 ConcurrentHashMap 模拟,生产建议使用 Caffeine)
 * L2: Redis (使用父类逻辑)
 */
public class Level2RedisCache extends RedisCache {

    // 一级缓存容器
    private final Map<Object, Object> localCache = new ConcurrentHashMap<>();
    
    // 统计指标(可选)
    private final String name;

    public Level2RedisCache(String name, RedisCacheWriter cacheWriter, RedisCacheConfiguration cacheConfig) {
        super(name, cacheWriter, cacheConfig);
        this.name = name;
    }

    /**
     * 重写查询逻辑 (核心)
     * 对应 @Cacheable 注解的读取操作
     */
    @Override
    protected Object lookup(Object key) {
        // 1. 【L1】先检查本地内存
        Object localValue = localCache.get(key);
        if (localValue != null) {
            // log.debug("L1 缓存命中: key={}", key);
            return localValue;
        }

        // 2. 【L2】本地没有,调用父类方法查 Redis
        // super.lookup() 内部会处理序列化和反序列化
        Object redisValue = super.lookup(key);

        // 3. 【回填】如果 Redis 查到了,回写到本地内存
        if (redisValue != null) {
            // log.debug("L2 缓存命中并回填 L1: key={}", key);
            localCache.put(key, redisValue);
        }

        return redisValue;
    }

    /**
     * 重写写入逻辑
     * 对应 @Cacheable 的写入和 @CachePut
     */
    @Override
    public void put(Object key, Object value) {
        // 1. 【L2】先写 Redis (保证远程数据的强一致性)
        super.put(key, value);
        
        // 2. 【L1】再写本地内存
        // 注意:如果是集群环境,这里只更新了当前节点的 L1。
        // 生产环境通常需要结合 Redis Pub/Sub 通知其他节点失效 L1。
        if (value != null) {
            localCache.put(key, value);
        }
    }

    /**
     * 重写删除逻辑
     * 对应 @CacheEvict
     */
    @Override
    public void evict(Object key) {
        // 1. 【L2】删 Redis
        super.evict(key);
        
        // 2. 【L1】删本地
        localCache.remove(key);
    }

    /**
     * 重写清空逻辑
     * 对应 @CacheEvict(allEntries = true)
     */
    @Override
    public void clear() {
        super.clear();
        localCache.clear();
    }
}

2. 在 Manager 中启用自定义 Cache

有了自定义的"工人"(Cache),我们需要在"工厂"(Manager)中雇佣它。

我们需要再次回到 RedisCacheManagercreateRedisCache 方法,将返回值修改为我们的 Level2RedisCache

java 复制代码
public class MultiLevelCacheManager extends RedisCacheManager {

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

    @Override
    protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
        // 这里可以结合上一章的逻辑,先处理 cacheConfig 的 TTL
        // ... (省略 TTL 解析逻辑) ...

        // 【关键】返回自定义的 Level2RedisCache 实例
        // 这里的 super.createRedisCache 只是为了复用父类的一些初始化检查,
        // 但我们直接 new 自己的对象是最直接的。
        return new Level2RedisCache(name, getCacheWriter(), cacheConfig);
    }
}

三、 进阶扩展:添加监控埋点

除了做多级缓存,自定义 RedisCache 也是做应用层监控 的最佳切入点。我们可以在 lookup 方法中加入耗时统计。

java 复制代码
@Override
protected Object lookup(Object key) {
    long start = System.nanoTime();
    try {
        Object value = super.lookup(key);
        recordMetrics(key, value != null, System.nanoTime() - start);
        return value;
    } catch (Exception e) {
        // 容错降级:如果 Redis 挂了,记录日志,返回 null 让业务查库
        // log.error("Redis 异常", e);
        return null; 
    }
}

private void recordMetrics(Object key, boolean hit, long durationNs) {
    // 伪代码:上报给 Prometheus 或 Micrometer
    // Metrics.counter("cache.requests", "name", this.name, "hit", String.valueOf(hit)).increment();
    // Metrics.timer("cache.latency", "name", this.name).record(durationNs, TimeUnit.NANOSECONDS);
}


除了上面讨论的 RedisCacheConfiguration (配置属性)、RedisCacheManager (管理策略) 和 RedisCache (运行时行为) 这"三驾马车"之外,Spring Data Redis 体系非常庞大,还存在其他几个关键层面的定制。

如果把之前的三个层面比作"装修房子",那么剩下的层面涉及 "地基加固""进门锁匙""应急预案"

以下是另外 4 个 常见且重要的高级定制层面:


第四层:AOP 辅助层定制 (KeyGenerator & ErrorHandler)

这层定制发生在缓存逻辑执行的 "前""后"

1. Custom KeyGenerator (定制键生成器)
  • 场景 :你厌倦了在每个 @Cacheable 里写 key = "#user.id"。或者你的入参很复杂(比如是一个大对象),你想用一种通用的算法(如 SHA256)生成 Key。
  • 默认行为SimpleKeyGenerator (使用参数的 hashCode)。
  • 如何定制 :实现 KeyGenerator 接口。
java 复制代码
@Bean("md5KeyGenerator")
public KeyGenerator keyGenerator() {
    return (target, method, params) -> {
        // 自定义逻辑:拼接类名+方法名+参数MD5
        return method.getName() + "_" + Arrays.toString(params);
    };
}
// 使用:@Cacheable(keyGenerator = "md5KeyGenerator")
2. CacheErrorHandler (定制异常处理器)
  • 场景这是生产环境的保命符 。默认情况下,如果 Redis 挂了(连接超时),@Cacheable 会抛出异常,导致整个接口报错(500 Error)。大多数时候,我们希望 "缓存挂了降级查库,不要影响主流程"
  • 如何定制 :实现 CacheErrorHandler
java 复制代码
@Configuration
public class CacheConfig extends CachingConfigurerSupport {
    @Override
    public CacheErrorHandler errorHandler() {
        return new CacheErrorHandler() {
            @Override
            public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
                // 仅打印日志,不抛出异常 -> 视为未命中,自动穿透到数据库
                log.error("Redis get failed, key: {}", key, exception);
            }
            // ... handlePutError, handleEvictError, handleClearError
        };
    }
}

第五层:客户端驱动层定制 (LettuceClientConfiguration)

这层定制属于 "基础设施" 级别,涉及网络连接、线程模型和读写分离。它不在 Spring Cache 的逻辑里,而是在连接工厂(RedisConnectionFactory)的底层。

1. 读写分离策略 (ReadFrom)
  • 场景 :你搭建了 Redis Sentinel(哨兵)或 Cluster(集群),主节点压力大,希望 "写主读从"
  • 如何定制 :通过 LettuceClientConfiguration
java 复制代码
@Bean
public LettuceConnectionFactory redisConnectionFactory() {
    LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
            // 优先从从节点读取,如果没有从节点则读主节点
            .readFrom(ReadFrom.REPLICA_PREFERRED) 
            .build();
    return new LettuceConnectionFactory(new RedisStandaloneConfiguration(), clientConfig);
}
2. Netty 线程池调优
  • 场景:Redis 吞吐量极高,默认的 Lettuce 线程数(CPU 核心数)成为瓶颈,或者发生了 Netty 堆外内存溢出。
  • 如何定制 :自定义 ClientResources

第六层:操作模版层定制 (RedisTemplate)

虽然我们主要讨论 @Cacheable(基于注解),但很多业务场景需要直接操作 Redis(基于代码)。这时就需要定制 RedisTemplate

1. 脚本执行 (Scripting)
  • 场景:需要执行复杂的原子操作(如"扣减库存"或"限流算法"),Java 代码分步执行有并发问题。
  • 定制点 :封装 DefaultRedisScript<T>,加载 Lua 脚本。
2. 流水线 (Pipeline)
  • 场景 :需要一次性写入 10000 条数据,循环调用 put 会产生 10000 次网络 RTT(往返时延)。
  • 定制点 :使用 redisTemplate.executePipelined(...),将命令打包发送,批量执行。

第七层:事件监听层定制 (MessageListener)

Redis 不仅仅是存储,还是一个消息总线。

1. 键过期监听 (Keyspace Notifications)
  • 场景:当订单支付超时(30分钟未支付),Redis Key 过期删除,你需要监听到这个事件来自动取消订单。
  • 如何定制 :配置 RedisMessageListenerContainer 并开启 Redis 的 notify-keyspace-events 配置。
java 复制代码
@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
    RedisMessageListenerContainer container = new RedisMessageListenerContainer();
    container.setConnectionFactory(connectionFactory);
    return container;
}
// 然后监听 __keyevent@0__:expired 频道

Spring Data Redis 全景定制图

为了让你一眼看清整个架构,我总结了这 7 个层面:

层面 核心类/接口 作用域 典型应用场景
1. 全局配置层 RedisCacheConfiguration 属性规范 改 JSON 序列化、统一 Key 前缀
2. 管理策略层 RedisCacheManager 调度策略 动态过期时间 (#30m)
3. 运行时行为层 RedisCache 执行逻辑 多级缓存、埋点监控
4. AOP 辅助层 CacheErrorHandler 容错/切面 Redis 宕机自动降级、自定义 Key 生成
5. 驱动配置层 LettuceClientConfiguration 网络/连接 读写分离、连接池参数、Netty 调优
6. 操作模版层 RedisTemplate 直接操作 Lua 脚本、Pipeline 批量操作
7. 事件监听层 KeyExpirationEvent 异步回调 监听 Key 过期事件处理业务逻辑


本文主要重点在于前三层定制,也是最常见的三层。

在面对需求时,你应该自上而下 进行考虑:能用上一层解决的,绝不轻易下沉到下一层,以保持系统的简洁性和可维护性。


核心决策图谱

第一层:RedisCacheConfiguration (配置层)

这是最顶层,也是改动成本最低的一层。它控制的是数据的静态属性

  • 核心定义 :定义缓存的 格式 (Format)默认规则 (Defaults)
  • 适用场景
  1. 修改序列化方式:不想用 JDK 的乱码二进制,想用 JSON 格式(通用需求)。
  2. 规范 Key 的命名 :把 Spring 默认的双冒号 :: 改成单冒号 :,或者统一添加全局前缀(如 app_name:)。
  3. 设置兜底 TTL:为了防止 Redis 内存爆炸,设置一个全局默认过期时间(如 1 小时)。
  4. 空值防御 :全局禁止缓存 null 值。

一句话判别 :如果你想改变存入 Redis 的数据 "长什么样" ,或者设置 "默认存多久",选它。


第二层:RedisCacheManager (策略层)

这是中间层,它控制的是配置的分发策略

  • 核心定义 :根据 缓存名称 (CacheName) 的不同,动态应用不同的 Configuration。
  • 适用场景
  1. 差异化过期时间 :这是最经典的需求。需要支持 @Cacheable("code#5m")@Cacheable("data#1d") 共存。
  2. 差异化序列化 :极少数情况下,某些缓存想存 JSON,某些想存纯 String(#raw),需要 Manager 识别后缀并切换配置。
  3. 多租户隔离:根据缓存名动态添加租户 ID 前缀。

一句话判别 :如果你的需求是 "看菜下碟"(即不同的业务缓存需要不同的过期时间或配置),选它。


第三层:RedisCache (行为层)

这是最底层,也是"深水区"。它控制的是**"数据的读写逻辑"**。

  • 核心定义 :介入 getputevict运行时行为 (Runtime Behavior)
  • 适用场景
  1. 多级缓存:这是最硬核的理由。引入 Caffeine 做 L1 缓存,Redis 做 L2 缓存。
  2. 埋点监控:需要精确统计每次 Redis IO 的耗时,并上报给 Prometheus/SkyWalking。
  3. 容错降级 :当 Redis 连接超时或宕机时,捕获异常并打印日志,而不是让整个接口报错(虽然 CacheErrorHandler 也能做,但这里更灵活)。
  4. 读写分离/双写:写入缓存时,需要同步写入另一个存储介质。

一句话判别 :如果你需要 "改变流程" (比如读 Redis 前先读内存,或者读完 Redis 后发个消息),或者需要 "强行介入" 读写过程,选它。


决策速查表 (Cheat Sheet)

在实际开发中,对照下表进行选型:

你的需求 推荐方案 复杂度
"我想让 Redis 里存的是 JSON,不是乱码" RedisCacheConfiguration ⭐ (低)
"我想把 Key 中间的双冒号 :: 去掉" RedisCacheConfiguration ⭐ (低)
"我想让所有缓存默认 1 小时过期" RedisCacheConfiguration ⭐ (低)
"我的验证码要 5分钟过期,排行榜要 1天过期" RedisCacheManager (解析 #ttl) ⭐⭐ (中)
"我想根据缓存名决定是否存 Null 值" RedisCacheManager (解析 #nonull) ⭐⭐ (中)
"Redis 太慢了,我想加一层本地内存缓存" 自定义 RedisCache ⭐⭐⭐ (高)
"我想知道 Redis 缓存的命中率和平均耗时" 自定义 RedisCache ⭐⭐⭐ (高)
"Redis 挂了不能报错,要自动去查数据库" 自定义 RedisCache (或 ErrorHandler) ⭐⭐⭐ (高)

最佳实践建议:

对于绝大多数(90%)的 Spring Boot 业务系统,"JSON 序列化配置 (Layer 1) + 动态过期时间支持 (Layer 2)" 的组合是最完美的黄金搭档。除非你有明确的性能瓶颈需要多级缓存,否则尽量不要触碰 Layer 3,以降低系统的维护成本。

相关推荐
高松燈2 小时前
K8s学习文档(1) -- 集群搭建(手把手教学)
后端
踏浪无痕2 小时前
Java 17 升级避坑:如何安全处理反射访问限制
后端·面试·架构
Go高并发架构_王工2 小时前
Redis命令执行原理与源码分析:深入理解内部机制
数据库·redis·后端
唐叔在学习2 小时前
buildozer打包详解:细说那些我踩过的坑
android·后端·python
okseekw2 小时前
Java动态代理实战:手把手教你实现明星经纪人模式
java·后端
清晓粼溪2 小时前
SpringCloud-04-Circuit Breaker断路器
后端·spring·spring cloud
woniu_maggie2 小时前
SAP导入WPS编辑的Excel文件报错处理
后端
想个名字太难2 小时前
springboot 源码分析(自动装配原理)
java·spring boot·spring