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,以降低系统的维护成本。

相关推荐
苏三说技术1 小时前
Claude Code从失控到起飞,只用了这些技巧
后端
长栎2 小时前
写 for 循环写了十年,你却从没用过迭代器模式最狠的那一面
后端
LiaCode2 小时前
Redis 在生产项目的使用
前端·后端
用户559822481222 小时前
Docker Compose Down 导致容器数据误删——ext4 日志恢复全记录
后端
LiaCode2 小时前
一天学完 redis 的爽翻版核心知识总结
前端·后端
大刚测试开发实战2 小时前
如何内网穿透访问本地私有化部署的TestHub
前端·后端·github
xiaodaoluanzha2 小时前
迄今為止,最簡單的編程語言 Nolang
前端·后端
Csvn2 小时前
Docker 容器管理入门 — 从镜像到容器编排
后端
用户762352425913 小时前
ShardingJDBC
后端
行者全栈架构师3 小时前
IDEA 中 Maven 项目的 15 个红色报错快速解决方法
java·后端