Skip to content

缓存


@Cacheable(类比 Cache::remember)

java
@Service
public class UserService {

    @Cacheable(value = "users", key = "#id")         // 类比 Cache::remember("users.{$id}", 3600, fn())
    public User findById(Long id) {
        // 1. 首次调用:执行业务逻辑,结果缓存
        // 2. 后续调用:直接返回缓存,不执行方法体
        slowQuery();  // 模拟耗时的数据库查询
        return userMapper.selectById(id);
    }

    @Cacheable(value = "users", key = "'list_' + #page")
    public List<User> getList(int page) {
        return userMapper.selectPage(new Page<>(page, 20));
    }
}

启用缓存

java
@SpringBootApplication
@EnableCaching                      // 必须加这个注解!否则 @Cacheable 无效
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

@CacheEvict(清除缓存)

java
@Service
public class UserService {

    @CacheEvict(value = "users", key = "#id")        // 类比 Cache::forget("users." . $id)
    public void delete(Long id) {
        userMapper.deleteById(id);
    }

    @CacheEvict(value = "users", allEntries = true)  // 清除 users 所有缓存
    public void clearAll() {
        // 不执行任何操作,只触发清理
    }
}

@CachePut(更新缓存)

java
@Service
public class UserService {

    @CachePut(value = "users", key = "#user.id")     // 类比 Cache::put("users." . $user->id, $user, 3600)
    public User update(User user) {
        userMapper.updateById(user);
        return user;   // 返回值会被写入缓存
    }
}

三种注解对比

注解执行时机缓存操作类比
@Cacheable方法执行前检查缓存缓存不存在才执行方法Cache::remember()
@CachePut方法执行后把返回值写入缓存Cache::put()
@CacheEvict方法执行后清除缓存Cache::forget()
@Caching组合多个注解同时配读和写组合操作

缓存配置

yaml
# application.yml
spring:
  cache:
    type: redis           # 使用 Redis 作为缓存存储(默认是 ConcurrentHashMap)

如果 spring.cache.type 不配置,Spring Boot 默认使用 ConcurrentHashMap(内存在进程内)。适合单机开发,生产环境必须用 Redis

自定义 Redis 缓存过期时间

java
@Configuration
public class CacheConfig {

    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
        // 不同缓存名称设置不同的 TTL
        Map<String, RedisCacheConfiguration> configMap = new HashMap<>();

        configMap.put("users", RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofHours(1)));      // users 缓存 1 小时

        configMap.put("posts", RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(30)));   // posts 缓存 30 分钟

        return RedisCacheManager.builder(factory)
            .cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(10)))  // 默认 10 分钟
            .withInitialCacheConfigurations(configMap)
            .build();
    }
}

⚠️ 常见坑

1. 同一个类中调用 @Cacheable 不生效

java
@Service
public class UserService {

    public User getProfile(Long id) {
        return findById(id);  // ❌ 内部调用,@Cacheable 不生效
    }

    @Cacheable("users")
    public User findById(Long id) { ... }
}

原因:和 AOP 一样,@Cacheable 通过代理实现。内部调用 this.findById() 走的是原始对象而不是代理对象。

解决方案:把 findById 抽到另一个 Service 类中。

2. @Cacheable 默认 key 包含所有参数

java
@Cacheable("users")
public User findById(Long id) { }

// 默认 key 是 "users::1"、"users::2"
// 如果你还想要其他 key 生成策略,可指定 keyGenerator

3. 缓存穿透问题

如果 findById() 返回 null,@Cacheable 默认不缓存 null 值。这意味着查询不存在的 ID 时,每次都会查数据库。

java
@Cacheable(value = "users", unless = "#result == null")
public User findById(Long id) { ... }

4. 条件缓存

java
// 只缓存 id > 100 的查询结果
@Cacheable(value = "users", condition = "#id > 100")
public User findById(Long id) { ... }

5. SpEL 表达式中的特殊字符

java
@Cacheable(key = "'list_' + #page")  // 字符串拼接用单引号
@Cacheable(key = "#result.id")       // 方法执行后的结果
@Cacheable(key = "#root.methodName")  // 当前方法名

面向 PHP 开发者的 Spring Boot 文档