导读:本期聚焦于冷风创作的《Spring Data Redis注解式缓存怎么用才能避免常见坑?》,敬请观看详情。把数据库查询结果直接丢进Redis固然能提速,但方法上的@Cacheable一旦用错,就会出现缓存穿透、脏数据甚至内存暴涨。本文从注解底层拦截逻辑讲起,对比手动调用RedisTemplate与声明式注解在事务边界、序列化策略上的差异。重点说明condition与unless表达式如何过滤无效缓存,以及sync参数解决击穿的实现机制。还会给出一个订单查询接口在并发场景下注解配置不当导致热点Key失效的实例,并给出可落地的TTL与命名空间方案,帮助系统在引入缓存后依然保持数据一致与稳定吞吐。

Spring Data Redis提供的注解式缓存让开发者无需手写模板代码就能把方法返回值放入Redis,但在实际项目中,很多人只是贴了@Cacheable就认为万事大吉,结果在上线后遇到缓存与数据库不一致、高频Key被集中失效等问题。理解注解背后基于AOP的拦截原理,以及它和RedisTemplate之间的协作方式,是写好缓存层的前提。

Spring Data Redis注解式缓存怎么用才能避免常见坑?

注解式缓存的底层拦截与执行流程

Spring Data Redis的缓存抽象建立在CacheManager和Cache接口之上,当我们在方法上标注@Cacheable@CachePut@CacheEvict时,框架会通过代理在方法调用前后织入缓存逻辑。具体流程是:先根据key生成器算出缓存键,再向Redis查询是否存在;命中则直接返回,未命中才执行原方法并把返回值序列化后写入。这种声明式方式把横切逻辑从业务代码中剥离,可读性明显优于在每一个Service里手动调RedisTemplate的opsForValue方法。

不过声明式注解并非没有代价。由于依赖AOP代理,类内部方法互调会导致注解失效,例如同一个Service中方法A调用带@Cacheable的方法B,B上的缓存逻辑不会触发。此外,默认使用SimpleKeyGenerator在有多参数时会把参数拼成数组哈希,如果不自定义KeyGenerator,很容易出现不同方法因参数类型巧合而Key冲突的情况。下面代码展示了如何自定义键生成,避免歧义:

@Configuration
public class RedisConfig {

    @Bean
    public KeyGenerator orderKeyGenerator() {
        return (target, method, params) -> {
            // 使用类名+方法名+参数值拼接,降低冲突概率
            String key = target.getClass().getSimpleName() + ":"
                    + method.getName() + ":"
                    + Arrays.toString(params);
            return key;
        };
    }
}

从运维角度看,注解式缓存把读写细节隐藏了,一旦Redis连接池配置不当或序列化器选择JDK原生方式,存储的Value会带有冗长的类元信息,不仅占用空间还难以跨语言读取。因此建议在CacheManager中统一设置JSON序列化,并配合合理的连接超时与重试策略,这样即使业务方只写注解,底层也能保持高效与可观测。

condition、unless与sync参数正确用法

很多人在使用@Cacheable时忽略了condition和unless,导致把空结果或异常对象也缓存起来,进而引发缓存污染。condition用于决定是否进入缓存逻辑,只有表达式为true才会查缓存和写缓存;unless则在方法执行后判断,返回true时不写缓存。例如查询用户时如果入参id小于等于0属于非法请求,就应用condition屏蔽:

@Cacheable(value = "user", key = "#id", condition = "#id > 0", unless = "#result == null")
public UserDTO getUser(Long id) {
    return userMapper.selectById(id);
}

上述代码中,unless保证了数据库查不到的null不会被写入Redis,避免缓存穿透。而sync参数是解决缓存击穿的关键,当同一Key在并发下同时失效,多个线程会同时打到数据库;设置sync=true后,Spring会利用Redis的SETNX类语义加锁,让只有一个线程去加载,其余线程阻塞等待结果。需要注意的是,sync目前仅对@Cacheable有效,且某些旧版本在Redis集群模式下锁粒度有限,仍需配合业务逻辑限流。

在真实订单系统中,我们常看到如下误用:把整个列表查询方法加上@Cacheable却不设TTL,也未使用unless过滤空列表,结果当底层表清空时,空集合被缓存数小时,前端一直显示无数据。正确做法是在注解所在配置类统一指定entryTtl,并结合unless排除size为0的返回。这样缓存层既加速却又不会长期掩盖数据变更。

与RedisTemplate手动方案的成本与一致性对比

选择注解式还是手动RedisTemplate,本质是在开发效率和控制粒度之间权衡。注解式在简单CRUD上极快,但在需要批量淘汰、跨Key事务、或根据返回对象内部字段动态计算TTL时力不从心。手动方案虽然代码啰嗦,但能在一个管道里完成查库、写缓存、发延迟消息删除周边Key的动作,对一致性更友好。

// 手动控制示例:写入时指定独立过期时间
public ProductVO getProduct(Long pid) {
    String key = "product:" + pid;
    ValueOperations<String, String> ops = redisTemplate.opsForValue();
    String cached = ops.get(key);
    if (cached != null) {
        return JSON.parseObject(cached, ProductVO.class);
    }
    ProductVO vo = productMapper.select(pid);
    if (vo != null) {
        // 热点商品给更长TTL
        long ttl = vo.getSales() > 1000 ? 3600 : 60;
        ops.set(key, JSON.toJSONString(vo), ttl, TimeUnit.SECONDS);
    }
    return vo;
}

从一致性角度看,注解式的@CacheEvict在方法抛出异常时默认不触发,若删除缓存前数据库已回滚,就会出现缓存与库不一致。手动方案可以把删缓存动作放在事务提交成功后通过监听器执行。若坚持用注解,应开启事务同步管理器,并确认CacheManager支持事务感知。最后提醒,无论哪种方式,缓存命名空间都应按业务域划分,例如订单域用order:*,商品域用product:*,方便后续用Redis的scan命令批量治理,而不是让所有Key杂乱地躺在db0中。

Spring_Data_Redis注解式缓存RedisTemplate修改时间:2026-08-17 07:38:30

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。