SpringBoot 接口缓存实现

1、概述

在 Spring Boot 中做缓存,最佳实践是使用 Spring Cache 抽象。它就像一套"统一的 API 开关",让你能用几乎相同的代码操作本地缓存(Caffeine)或分布式缓存(Redis),切换时只需改配置,业务代码无需改动。

1.1 两种方案

  1. 本地缓存 Caffeine:进程内存,速度极快,无网络开销;多实例下缓存不互通,数据不一致风险;适合静态、变化少的热点数据。
  2. Redis 缓存:分布式共享缓存;有网络 IO;多实例数据统一;适合分布式项目。

Spring Cache 抽象:一套注解,底层切换 Caffeine/Redis,业务代码几乎不用改动。

1.2 Spring Cache 核心注解

注解 作用
@Cacheable 查询:先读缓存,命中直接返回;未命中执行方法,结果写入缓存
@CachePut 更新:执行方法,把返回值更新缓存,不会拦截方法
@CacheEvict 删除:清除指定缓存 key
@Caching 组合多个缓存操作
  • key:缓存键;cacheNames:缓存分组;condition:条件缓存;unless:条件不缓存。

2、方案 1:本地缓存 Caffeine

2.1 引入依赖

复制代码
<!-- spring cache 基础 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- Caffeine本地缓存 -->
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>

2.2 开启缓存

启动类增加 @EnableCaching

复制代码
@SpringBootApplication
@EnableCaching
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}

2.3 YAML 配置 Caffeine

复制代码
spring:
  cache:
    type: caffeine
    caffeine:
      spec: maximumSize=1000,expireAfterWrite=300s,recordStats

参数说明:

  • maximumSize=1000:缓存最大条目,超过 LRU 淘汰
  • expireAfterWrite=300s:写入后 300 秒过期
  • recordStats:开启缓存统计命中率

2.4 Controller/ Service 使用示例

复制代码
// cacheNames缓存分组;key支持SpEL表达式;condition:id>0才缓存
@Cacheable(cacheNames = "userInfo", key = "#id", condition = "#id>0")
public UserDTO getUserById(Long id){
    // 未命中缓存执行DB查询逻辑
    return userMapper.selectById(id);
}

// 更新数据同步更新缓存
@CachePut(cacheNames = "userInfo", key="#user.id")
public UserDTO updateUser(User user){
    userMapper.updateById(user);
    return convert(user);
}

// 删除数据清除缓存
@CacheEvict(cacheNames = "userInfo", key="#id")
public void deleteUser(Long id){
    userMapper.deleteById(id);
}

注意:

  1. @Cacheable基于 AOP,同类内部调用不会生效;不要 this 调用本类带缓存方法。
  2. 多实例部署,Caffeine 每个实例缓存独立,写操作后其他节点缓存不会自动失效。

3、方案 2:Redis 分布式缓存

3.1 Maven 依赖

复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

3.2 application.yml 配置

复制代码
spring:
  redis:
    host: 127.0.0.1
    port: 6379
    password:
    database: 0
  cache:
    type: redis
    redis:
      time-to-live: 300s  #全局默认TTL
      cache-null-values: false #是否缓存null,防止缓存穿透

3.3 自定义 Redis 序列化(重要,默认 JDK 序列化,二进制可读性差)

需要@EnableCaching。

复制代码
package com.ybw.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

/**
 * Redis 缓存统一配置
 * <p>
 * 同时管理两套 Redis 访问通道,且序列化策略保持一致,保证数据互通:
 * <ul>
 *   <li>{@link RedisCacheManager} ------ 服务于 @Cacheable / @CachePut / @CacheEvict 注解</li>
 *   <li>{@link RedisTemplate} ------ 服务于手动编码 opsForValue / opsForHash 等操作</li>
 * </ul>
 * 两者均使用 GenericJackson2JsonRedisSerializer(@class 属性携带类型信息),
 * 因此注解写入的数据可被 redisTemplate 读取,反之亦然。
 */
@Configuration
@EnableCaching
public class CacheConfig {

    /**
     * 全局默认 TTL。
     * <p>
     * 注意:由于本类自定义了 RedisCacheManager Bean,
     * application.yml 中 spring.cache.redis.time-to-live 不会被 Spring 自动装配,
     * 必须通过 @Value 手动读取后注入到 RedisCacheConfiguration 中。
     */
    @Value("${spring.cache.redis.time-to-live:30s}")
    private Duration timeToLive;

    // ==================== Spring Cache 注解体系 ====================

    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        //1、RedisCacheConfiguration 配置,只对@cache相关注解生效
        RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(timeToLive)
                .serializeKeysWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(new GenericJackson2JsonRedisSerializer(buildObjectMapper())))
                .disableCachingNullValues();
        //2、RedisCacheManager 配置,对@cache相关注解生效
        return RedisCacheManager.builder(redisConnectionFactory)
                .cacheDefaults(cacheConfig)
                .build();
    }

    // ==================== 手动操作 Redis 体系 ====================

    /**
     * 配置 RedisTemplate,key 使用 String 序列化,value 使用 Jackson JSON 序列化。
     * <p>
     * 序列化器与 cacheManager 保持一致(GenericJackson2JsonRedisSerializer + @class 类型信息),
     * 确保两套通道写入的数据可互相读取。
     *
     * @param factory Redis 连接工厂
     * @return 配置好的 RedisTemplate,key 为 String,value 为任意 Object
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        StringRedisSerializer stringSerializer = new StringRedisSerializer();
        GenericJackson2JsonRedisSerializer jsonSerializer =
                new GenericJackson2JsonRedisSerializer(buildObjectMapper());

        // key / hashKey 一律用 String 序列化,保证 Redis 中 key 可读、可命令行操作
        template.setKeySerializer(stringSerializer);
        template.setHashKeySerializer(stringSerializer);
        // value / hashValue 用 Jackson JSON 序列化,携带 @class 类型信息
        template.setValueSerializer(jsonSerializer);
        template.setHashValueSerializer(jsonSerializer);

        template.afterPropertiesSet();
        return template;
    }

    // ==================== 公共工具方法 ====================

    /**
     * 构建统一的 Jackson ObjectMapper,消除重复配置。
     * <p>
     * 统一要点:
     * <ul>
     *   <li>activateDefaultTyping:JSON 中以 @class 属性携带运行时类型,
     *       保证 User / List&lt;User&gt; / Map 等可正确反序列化</li>
     *   <li>JavaTimeModule:支持 LocalDateTime / LocalDate 等 JSR-310 类型</li>
     *   <li>关闭 WRITE_DATES_AS_TIMESTAMPS:日期序列化为 ISO-8601 字符串而非时间戳数组</li>
     * </ul>
     *
     * @return 配置好的 ObjectMapper
     */
    private ObjectMapper buildObjectMapper() {
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.activateDefaultTyping(
                LaissezFaireSubTypeValidator.instance,
                ObjectMapper.DefaultTyping.NON_FINAL,
                JsonTypeInfo.As.PROPERTY);
        om.registerModule(new JavaTimeModule());
        om.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        return om;
    }
}

4、方案 3:多级缓存(Caffeine 本地 + Redis 二级缓存,高性能)

  • 流程:请求 → Caffeine 本地缓存命中直接返回 → 未命中查询 Redis → Redis 命中返回写入 Caffeine → Redis 未命中查询 DB。
  • Spring Cache 原生不支持多级缓存,需要自己封装,不能直接注解完成。

多级缓存注意点

  • 写操作时:删除 Redis,同时删除本机 Caffeine;其他机器本地缓存依旧残留脏数据
  • 解决手段:Redis 发布订阅,更新缓存后广播消息,所有实例删除本地缓存。

5、使用选型建议

场景 缓存选型
单实例部署、热点不变基础数据 Caffeine 本地缓存
分布式集群、多实例共享数据 Redis 缓存
高 QPS 接口,追求极致性能 Caffeine+Redis 多级缓存
相关推荐
名字还没想好☜1 小时前
Go 时间格式化为什么用 2006-01-02:time.Format/Parse 的参考时间、时区与解析踩坑
开发语言·后端·golang·go
摇滚侠1 小时前
《SpringBoot 3:入门与应用实战》第 13 章 整合 MyBatis 使用 MyBatis-Plus 阅读笔记 40
spring boot·笔记·mybatis
jyOverQ4 小时前
RabbitMQ 消息积压怎么办?Prefetch、消费者并发与扩容
分布式·后端·rabbitmq
东风破_11 小时前
从跨域到 WebSocket:前端跨域方案、SSE 与双向实时通信详解
前端·后端
莫得感情 o12 小时前
Redis 05 · 持久化:RDB 与 AOF 怎么保证数据不丢
redis·缓存
行百里er13 小时前
Gateway 上别硬塞 MVC:DeferredImportSelector 看菜下锅
spring boot·后端·开源
curd_boy13 小时前
【Redis】Redis从缓存到AI向量平台
人工智能·redis·缓存
kiracrimson13 小时前
从缓存的角度看链表与线性表的差异
数据结构·链表·缓存
名字还没想好☜13 小时前
Java 遍历时删元素抛 ConcurrentModificationException:fail-fast 原理与三种正确删法
后端