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

注解式缓存的底层拦截与执行流程
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