缓存

Spring Boot 提供了统一的缓存抽象(Spring Cache),通过注解驱动,屏蔽底层缓存实现(Caffeine、Redis、EhCache 等),业务代码无需感知具体缓存技术。

快速开始

引入依赖并开启缓存:

<!-- 本地缓存:Caffeine -->
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>
 
<!-- 分布式缓存:Redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
@SpringBootApplication
@EnableCaching  // 开启缓存,底层通过 AOP 代理实现
public class Application {}

缓存注解本质上是 AOP 切面,与方法执行时的代理机制相同,自调用(同类内部调用)无效。

核心注解

@Cacheable — 读取缓存

方法执行前先查缓存,命中则直接返回,未命中才执行方法并将结果存入缓存:

@Service
public class UserService {
 
    // 缓存名 "users",key 为方法参数 id
    @Cacheable(value = "users", key = "#id")
    public User getById(Long id) {
        return userRepository.findById(id).orElseThrow();
    }
 
    // 多条件 key
    @Cacheable(value = "search", key = "#keyword + '-' + #page")
    public List<User> search(String keyword, int page) {
        return userRepository.search(keyword, page);
    }
 
    // 条件缓存:只缓存 id > 0 的结果
    @Cacheable(value = "users", key = "#id", condition = "#id > 0")
    public User getByIdConditional(Long id) { ... }
 
    // 排除缓存:结果为 null 时不缓存
    @Cacheable(value = "users", key = "#id", unless = "#result == null")
    public User getNullable(Long id) { ... }
}

@CachePut — 更新缓存

总是执行方法,并将结果写入缓存(不会短路)。用于更新操作:

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

@CacheEvict — 删除缓存

// 删除单条
@CacheEvict(value = "users", key = "#id")
public void delete(Long id) {
    userRepository.deleteById(id);
}
 
// 清空整个缓存
@CacheEvict(value = "users", allEntries = true)
public void clearAll() { ... }
 
// 方法执行前删除(beforeInvocation = true,即使方法异常也会删除)
@CacheEvict(value = "users", key = "#id", beforeInvocation = true)
public void deleteBeforeMethod(Long id) { ... }

@Caching — 组合多个缓存操作

@Caching(
    cacheable = @Cacheable(value = "users", key = "#id"),
    evict = {
        @CacheEvict(value = "userList", allEntries = true),
        @CacheEvict(value = "userSearch", allEntries = true)
    }
)
public User getAndClearList(Long id) { ... }

@CacheConfig — 类级别默认配置

@Service
@CacheConfig(cacheNames = "users")  // 类内所有方法默认使用 "users" 缓存
public class UserService {
 
    @Cacheable(key = "#id")  // 无需重复指定 value
    public User getById(Long id) { ... }
}

Key 表达式

Spring Cache 使用 SpEL 生成 key:

表达式说明
#id参数名
#p0#a0第 0 个参数
#user.id参数对象的属性
#result方法返回值(@CachePut/@CacheEvict 可用)
#root.method.name方法名
#root.target.class.name目标类名

自定义 Key 生成器:

@Bean
public KeyGenerator customKeyGenerator() {
    return (target, method, params) ->
        target.getClass().getSimpleName() + ":" + method.getName() + ":" +
        Arrays.stream(params).map(Object::toString).collect(Collectors.joining(","));
}
 
// 使用自定义生成器
@Cacheable(value = "users", keyGenerator = "customKeyGenerator")
public User getById(Long id) { ... }

缓存实现选型

Caffeine(本地缓存,推荐单机场景)

spring:
  cache:
    type: caffeine
    caffeine:
      spec: maximumSize=1000,expireAfterWrite=10m

按缓存名单独配置:

@Bean
public CacheManager cacheManager() {
    CaffeineCacheManager manager = new CaffeineCacheManager();
    manager.registerCustomCache("users",
        Caffeine.newBuilder()
            .maximumSize(500)
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .recordStats()
            .build());
    manager.registerCustomCache("config",
        Caffeine.newBuilder()
            .maximumSize(100)
            .expireAfterWrite(1, TimeUnit.HOURS)
            .build());
    return manager;
}

Redis(分布式缓存,推荐集群场景)

spring:
  cache:
    type: redis
    redis:
      time-to-live: 10m   # 全局 TTL
      use-key-prefix: true
      key-prefix: "app:"

Redis 连接配置参见 Redis集成

自定义序列化(默认 JDK 序列化,可读性差):

@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory factory) {
    ObjectMapper om = new ObjectMapper();
    om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
        ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
 
    RedisSerializer<Object> serializer = new GenericJackson2JsonRedisSerializer(om);
 
    RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
        .entryTtl(Duration.ofMinutes(10))
        .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer))
        .disableCachingNullValues();
 
    // 按缓存名定制 TTL
    Map<String, RedisCacheConfiguration> configs = new HashMap<>();
    configs.put("users", defaultConfig.entryTtl(Duration.ofMinutes(30)));
    configs.put("config", defaultConfig.entryTtl(Duration.ofHours(24)));
 
    return RedisCacheManager.builder(factory)
        .cacheDefaults(defaultConfig)
        .withInitialCacheConfigurations(configs)
        .build();
}

两级缓存(本地 + 分布式)

本地缓存速度快,Redis 保证多实例一致性,结合使用效果最佳:

@Bean
public CacheManager twoLevelCacheManager(RedisConnectionFactory factory) {
    // 可通过 CompositeCacheManager 组合多个 CacheManager
    CompositeCacheManager composite = new CompositeCacheManager(
        caffeineCacheManager(),
        redisCacheManager(factory)
    );
    composite.setFallbackToNoOpCache(false);
    return composite;
}

缓存穿透、击穿、雪崩

问题场景应对方案
穿透大量请求查询不存在的数据,缓存未命中直达数据库缓存空值(unless 条件去掉 null 判断);布隆过滤器前置拦截
击穿热点 key 过期瞬间,大量并发请求同时击穿到数据库互斥锁重建;逻辑过期(后台异步刷新)
雪崩大量 key 同时过期,数据库压力骤增TTL 加随机偏移;永不过期 + 异步刷新;限流降级

缓存穿透防护示例:

@Cacheable(value = "users", key = "#id", unless = "false")  // 允许缓存 null
public User getById(Long id) {
    return userRepository.findById(id).orElse(null);
    // 返回 null 也会被缓存,防止穿透
}

注意事项

  • 自调用失效:与 AOP 代理相同,同类内部方法调用不走切面
  • 序列化:缓存对象须实现 Serializable(JDK 序列化)或能被 JSON 序列化(Jackson)
  • 事务与缓存@CacheEvict 默认在方法返回后执行,事务提交前可能导致缓存提前失效;可配合 @TransactionalafterCommit 钩子处理
  • 缓存与事务一致性:与 事务管理 配合时,建议在事务提交后再刷新缓存