Spring缓存注解Cacheable使用

wen java案例 2

精通Spring缓存注解@Cacheable:从原理到实战的完整指南

文章导读目录

  1. @Cacheable注解的核心概念
    • 什么是缓存?为什么需要缓存?
    • @Cacheable的工作原理与执行流程
  2. @Cacheable的详细使用方式
    • 基础配置:value、key、condition与unless
    • 条件缓存与动态key的实战技巧
    • 自定义缓存管理器与缓存名称空间
  3. 与@CachePut、@CacheEvict的配合策略
    • 更新缓存的最佳实践
    • 清除缓存的时机与方案
  4. 常见问题与性能优化
    • 缓存穿透、雪崩、击穿的应对
    • 方法内部调用缓存失效的解决方案
  5. 真实企业级案例解析

    用户信息缓存 + 商品详情缓存

    Spring缓存注解Cacheable使用

  6. Q&A 高频问答环节

@Cacheable注解的核心概念

什么是缓存?为什么需要缓存?

缓存是存储在内存中的临时数据副本,用于减少对数据库或外部服务的重复访问,在高并发场景下,缓存能将响应时间从几十毫秒降低到微秒级,同时显著降低数据库负载,Spring缓存框架抽象了不同缓存实现(Redis、EhCache、Caffeine等),而@Cacheable是最常用的注解之一。

@Cacheable的工作原理与执行流程

当一个方法被注解为@Cacheable后,调用该方法前,Spring会先检查缓存中是否存在匹配的key:

  • 如果缓存命中,直接返回缓存结果(方法不执行)。
  • 如果缓存未命中,执行方法体,然后将返回结果存入缓存。

伪代码逻辑:

// 伪代码展示内部流程
if (cache.containsKey(key)) {
    return cache.get(key);
} else {
    result = method.execute(params);
    cache.put(key, result);
    return result;
}

@Cacheable的详细使用方式

基础配置

@Cacheable(value = "users", key = "#userId", unless = "#result == null")
public User getUserById(Long userId) {
    // 查询数据库
    return userRepository.findById(userId).orElse(null);
}
  • value/cacheNames:缓存名称,对应缓存分区(如Redis的key前缀)。
  • key:支持SpEL表达式,如#p0#userId#root.methodName
  • condition:满足条件才缓存(布尔表达式)。
  • unless:满足条件不缓存(通常用于null值排除)。

动态key与条件缓存实战

@Cacheable(value = "products", 
           key = "#productId + '_' + T(java.util.Locale).getDefault().getLanguage()",
           condition = "#productId > 1000")
public Product getProduct(Long productId) {
    return productService.loadById(productId);
}
  • 动态key:使用SpEL拼接实现多参数组合。
  • condition:只有productId > 1000时才启用缓存。

自定义缓存管理器

@Configuration
@EnableCaching
public class CacheConfig {
    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager manager = new CaffeineCacheManager();
        manager.setCaffeine(Caffeine.newBuilder()
                .maximumSize(500)
                .expireAfterWrite(10, TimeUnit.MINUTES));
        return manager;
    }
}

通过自定义CacheManager,可精细控制缓存的过期策略、最大容量等。


与@CachePut、@CacheEvict的配合策略

@CachePut:更新缓存而不影响方法执行

@CachePut(value = "users", key = "#user.id")
public User updateUser(User user) {
    return userRepository.save(user);
}

每次都会执行方法,并将返回值更新到缓存中,适用于数据修改后的缓存同步。

@CacheEvict:清除缓存的最佳时机

@CacheEvict(value = "users", key = "#id")
public void deleteUser(Long id) {
    userRepository.deleteById(id);
}
  • allEntries = true:清空整个缓存分区。
  • beforeInvocation = true:在方法执行前清除(可用于异常场景)。
  • 常用策略:更新数据时先@CachePut更新,删除时用@CacheEvict

常见问题与性能优化

缓存穿透、雪崩、击沉的应对

  • 穿透:查询不存在的数据,方案:缓存空值并使用unless = "#result == null"
  • 雪崩:大量缓存同时过期,方案:设置随机过期时间(如expireAfterWrite(300 + new Random().nextInt(100), TimeUnit.SECONDS))。
  • 击穿:热点数据过期瞬间高并发,方案:使用@Cacheable(sync = true)开启同步锁。

方法内部调用缓存失效

@Service
public class ServiceA {
    @Cacheable("data")
    public String getData() { ... }
    public void caller() {
        // 直接调用时,缓存失效
        String result = this.getData(); // ❌ 不走缓存
    }
}

解决方案

  1. 注入自身代理对象@Autowired private ServiceA self;,调用self.getData()
  2. 或将@Cacheable抽取到单独服务类。

真实企业级案例解析

案例:用户信息缓存 + 商品详情缓存

@Service
public class UserInfoService {
    // 用户信息缓存:key为userId,除非结果为null
    @Cacheable(value = "user:info", key = "#userId", unless = "#result == null")
    public UserInfo getUserInfo(Long userId) {
        return userDao.findById(userId);
    }
    // 商品缓存:支持多语言
    @Cacheable(value = "product:detail", 
               key = "#productId + '_' + #locale.toString()",
               condition = "#productId != null && #locale != null")
    public ProductDetail getProductDetail(Long productId, Locale locale) {
        return productDao.findDetail(productId, locale);
    }
}

该案例在电商系统中十分常见,用户信息与商品详情是缓存命中率极高的热点数据。


Q&A 高频问答环节

Q1:@Cacheable的key为空会怎样?
A:如果key计算为null,Spring会抛出异常,建议在SpEL中增加判空:key = "#userId ?: 'default'"

Q2:多个@Cacheable注解可以叠加吗?
A:Spring允许使用@Caching组合多个缓存操作:

@Caching(cacheable = {
    @Cacheable("users"),
    @Cacheable("admins")
})
public User getUser(Long id) { ... }

Q3:如何为不同缓存设置不同的过期时间?
A:在自定义CacheManager中使用RedisCacheConfiguration,为每个cacheNames单独配置:

Map<String, RedisCacheConfiguration> configs = new HashMap<>();
configs.put("users", RedisCacheConfiguration.defaultCacheConfig()
    .entryTtl(Duration.ofMinutes(30)));
configs.put("products", RedisCacheConfiguration.defaultCacheConfig()
    .entryTtl(Duration.ofHours(1)));

Q4:@Cacheable能否与事务结合使用?
A:可以,但要注意事务提交时机,缓存通常应在事务成功提交后更新,避免回滚导致数据不一致,可使用@Transactional(propagation = Propagation.REQUIRED)配合使用。

Q5:如何监控缓存命中率?
A:可通过AOP自定义注解统计调用次数,或集成actuator暴露缓存统计端点(如/actuator/caches)。

抱歉,评论功能暂时关闭!