芋道源码 —— Spring Boot 缓存 Cache 入门

芋道源码 ------ Spring Boot 缓存 Cache 入门

一、概述

(一)缓存的必要性

随着系统访问量的增加,数据库往往成为性能瓶颈。为了减少数据库的压力,提高系统的响应速度,我们可以通过缓存来优化系统性能。

(二)缓存策略

常见的缓存策略包括:

  • 读写分离:将读操作分流到从节点,避免主节点压力过多。
  • 分库分表:将读写操作分摊到多个节点,避免单节点压力过多。
  • 缓存系统:使用缓存系统(如 Redis、Ehcache)来存储经常访问的数据,减少对数据库的直接访问。

(三)Spring Cache

Spring 3.1 引入了基于注解的缓存技术,通过 @Cacheable 等注解简化缓存逻辑,支持多种缓存实现。Spring Cache 的特点包括:

  • 通过少量的配置注解即可使得既有代码支持缓存。
  • 支持开箱即用,无需安装和部署额外第三方组件即可使用缓存。
  • 支持 Spring 表达式语言(SpEL),能使用对象的任何属性或者方法来定义缓存的 key 和 condition。
  • 支持 AspectJ,并通过其实现任何方法的缓存支持。
  • 支持自定义 key 和自定义缓存管理者,具有相当的灵活性和扩展性。

二、注解

(一)@Cacheable

@Cacheable 注解用于方法上,缓存方法的执行结果。执行过程如下:

  1. 判断方法执行结果的缓存。如果有,则直接返回该缓存结果。
  2. 执行方法,获得方法结果。
  3. 根据是否满足缓存的条件。如果满足,则缓存方法结果到缓存。
  4. 返回方法结果。

常用属性

  • cacheNames:缓存名。必填。[] 数组,可以填写多个缓存名。
  • key:缓存的 key 。允许空。如果为空,则默认方法的所有参数进行组合。如果非空,则需要按照 SpEL 配置。例如,@Cacheable(value = "users", key = "#id"),使用方法参数 id 的值作为缓存的 key 。
  • condition:基于方法入参,判断要缓存的条件。允许空。如果非空,则需要按照 SpEL 配置。例如,@Cacheable(condition="#id > 0"),需要传入的 id 大于零。
  • unless:基于方法返回,判断不缓存的条件。允许空。如果非空,则需要按照 SpEL 配置。例如,@Cacheable(unless="#result == null"),如果返回结果为 null ,则不进行缓存。

不常用属性

  • keyGenerator:自定义 key 生成器 KeyGenerator Bean 的名字。允许空。如果设置,则 key 失效。
  • cacheManager:自定义缓存管理器 CacheManager Bean 的名字。允许空。一般不填写,除非有多个 CacheManager Bean 的情况下。
  • cacheResolver:自定义缓存解析器 CacheResolver Bean 的名字。允许空。
  • sync:在获得不到缓存的情况下,是否同步执行方法。默认为 false ,表示无需同步。如果设置为 true ,则执行方法时,会进行加锁,保证同一时刻,有且仅有一个方法在执行,其它线程阻塞等待。

(二)@CachePut

@CachePut 注解用于方法上,缓存方法的执行结果。与 @Cacheable 不同,它的执行过程如下:

  1. 执行方法,获得方法结果。也就是说,无论是否有缓存,都会执行方法。
  2. 根据是否满足缓存的条件。如果满足,则缓存方法结果到缓存。
  3. 返回方法结果。

一般来说,@Cacheable 搭配读操作,实现缓存的被动写;@CachePut 配置写操作,实现缓存的主动写。

(三)@CacheEvict

@CacheEvict 注解用于方法上,删除缓存。相比 @CachePut,它额外多了两个属性:

  • allEntries:是否删除缓存名(cacheNames)下,所有 key 对应的缓存。默认为 false,只删除指定 key 的缓存。
  • beforeInvocation:是否在方法执行前删除缓存。默认为 false,在方法执行后删除缓存。

(四)@Caching

@Caching 注解用于方法上,可以组合使用多个 @Cacheable@CachePut@CacheEvict 注解。不太常用,可以暂时忽略。

(五)@CacheConfig

@CacheConfig 注解用于类上,共享如下四个属性的配置:

  • cacheNames
  • keyGenerator
  • cacheManager
  • cacheResolver

(六)@EnableCaching

@EnableCaching 注解用于标记开启 Spring Cache 功能,所以一定要添加。

三、Spring Boot 集成

(一)依赖

在 Spring Boot 里,提供了 spring-boot-starter-cache 库,实现 Spring Cache 的自动化配置,通过 CacheAutoConfiguration 配置类。

(二)缓存工具和框架

在 Java 后端开发中,常见的缓存工具和框架列举如下:

  • 本地缓存:Guava LocalCache、Ehcache、Caffeine 。Ehcache 的功能更加丰富,Caffeine 的性能要比 Guava LocalCache 好。
  • 分布式缓存:Redis、Memcached、Tair 。Redis 最为主流和常用。

(三)自动配置

在这些缓存方案当中,spring-boot-starter-cache 怎么知道使用哪种呢?在默认情况下,Spring Boot 会按照如下顺序,自动判断使用哪种缓存方案,创建对应的 CacheManager 缓存管理器。

private static final Map<CacheType, Class<?>> MAPPINGS;

static {
    Map<CacheType, Class<?>> mappings = new EnumMap<>(CacheType.class);
    mappings.put(CacheType.GENERIC, GenericCacheConfiguration.class);
    mappings.put(CacheType.EHCACHE, EhCacheCacheConfiguration.class);
    mappings.put(CacheType.HAZELCAST, HazelcastCacheConfiguration.class);
    mappings.put(CacheType.INFINISPAN, InfinispanCacheConfiguration.class);
    mappings.put(CacheType.JCACHE, JCacheCacheConfiguration.class);
    mappings.put(CacheType.COUCHBASE, CouchbaseCacheConfiguration.class);
    mappings.put(CacheType.REDIS, RedisCacheConfiguration.class);
    mappings.put(CacheType.CAFFEINE, CaffeineCacheConfiguration.class);
    mappings.put(CacheType.SIMPLE, SimpleCacheConfiguration.class);
    mappings.put(CacheType.NONE, NoOpCacheConfiguration.class);
    MAPPINGS = Collections.unmodifiableMap(mappings);
}

最差的情况下,会使用 SimpleCacheConfiguration。因为自动判断可能和我们希望使用的缓存方案不同,此时我们可以手动配置 spring.cache.type 指定类型。

四、Ehcache 示例

(一)引入依赖

pom.xml 文件中,引入相关依赖。

<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache</artifactId>
</dependency>

(二)应用配置文件

resources 目录下,创建 application.yaml 配置文件。配置如下:

spring:
  cache:
    type: ehcache

(三)Ehcache 配置文件

resources 目录下,创建 ehcache.xml 配置文件。配置如下:

<ehcache>
    <cache name="users"
           maxElementsInMemory="1000"
           timeToLiveSeconds="60"
           memoryStoreEvictionPolicy="LRU"/>
</ehcache>

(四)Application

创建 Application.java 类,代码如下:

@SpringBootApplication
@EnableCaching
public class Application {
}

(五)UserDO

cn.iocoder.springboot.lab21.cache.dataobject 包路径下,创建 UserDO.java 类,用户 DO 。代码如下:

@TableName(value = "users")
public class UserDO {
    private Integer id;
    private String username;
    private String password;
    private Date createTime;
    @TableLogic
    private Integer deleted;
}

(六)UserMapper

cn.iocoder.springboot.lab21.cache.mapper 包路径下,创建 UserMapper 接口。代码如下:

@Repository
@CacheConfig(cacheNames = "users")
public interface UserMapper extends BaseMapper<UserDO> {

    @Cacheable(key = "#id")
    UserDO selectById(Integer id);

    @CachePut(key = "#user.id")
    default UserDO insert0(UserDO user) {
        this.insert(user);
        return user;
    }

    @CacheEvict(key = "#id")
    int deleteById(Integer id);
}

(七)UserMapperTest

创建 UserMapperTest 测试类,我们来测试一下简单的 UserMapper 的每个操作。核心代码如下:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class UserMapperTest {

    private static final String CACHE_NAME_USER = "users";

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private CacheManager cacheManager;

    @Test
    public void testCacheManager() {
        System.out.println(cacheManager);
    }

    @Test
    public void testSelectById() {
        Integer id = 1;
        UserDO user = userMapper.selectById(id);
        System.out.println("user:" + user);
        Assert.assertNotNull("缓存为空", cacheManager.getCache(CACHE_NAME_USER).get(user.getId(), UserDO.class));
        user = userMapper.selectById(id);
        System.out.println("user:" + user);
    }

    @Test
    public void testInsert() {
        UserDO user = new UserDO();
        user.setUsername(UUID.randomUUID().toString());
        user.setPassword("nicai");
        user.setCreateTime(new Date());
        user.setDeleted(0);
        userMapper.insert0(user);
        Assert.assertNotNull("缓存为空", cacheManager.getCache(CACHE_NAME_USER).get(user.getId(), UserDO.class));
    }

    @Test
    public void testDeleteById() {
        UserDO user = new UserDO();
        user.setUsername(UUID.randomUUID().toString());
        user.setPassword("nicai");
        user.setCreateTime(new Date());
        user.setDeleted(0);
        userMapper.insert0(user);
        Assert.assertNotNull("缓存为空", cacheManager.getCache(CACHE_NAME_USER).get(user.getId(), UserDO.class));
        userMapper.deleteById(user.getId());
        Assert.assertNull("缓存不为空", cacheManager.getCache(CACHE_NAME_USER).get(user.getId(), UserDO.class));
    }
}

五、Redis 示例

(一)引入依赖

pom.xml 文件中,引入相关依赖。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <exclusions>
        <exclusion>
            <groupId>io.lettuce</groupId>
            <artifactId>lettuce-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>

(二)应用配置文件

resources 目录下,创建 application.yaml 配置文件。配置如下:

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    password:
    database: 0
    timeout: 0
    jedis:
      pool:
        max-active: 8
        max-idle: 8
        min-idle: 0
        max-wait: -1
  cache:
    type: redis

(三)Application

4.4 Application 一致。

(四)UserDO

4.5 UserDO 一致。差别在于,需要让 UserDO 实现 Serializable 接口。

(五)UserMapper

4.6 UserMapper 一致。

(六)UserMapperTest

4.7 UserMapperTest 基本一致。

六、面试回答思路和答案

(一)缓存的必要性

问题:为什么需要使用缓存?

回答:随着系统访问量的增加,数据库往往成为性能瓶颈。缓存可以减少数据库的压力,提高系统的响应速度。缓存系统(如 Redis、Ehcache)可以存储经常访问的数据,减少对数据库的直接访问。

(二)Spring Cache

问题:什么是 Spring Cache?

回答 :Spring Cache 是 Spring 3.1 引入的基于注解的缓存技术,通过 @Cacheable 等注解简化缓存逻辑,支持多种缓存实现。Spring Cache 的特点包括:通过少量的配置注解即可使得既有代码支持缓存;支持开箱即用;支持 Spring 表达式语言(SpEL);支持 AspectJ;支持自定义 key 和自定义缓存管理者。

(三)@Cacheable

问题@Cacheable 注解的使用方法和属性有哪些?

回答@Cacheable 注解用于方法上,缓存方法的执行结果。常用属性包括:cacheNames(缓存名,必填),key(缓存的 key,允许空),condition(基于方法入参,判断要缓存的条件),unless(基于方法返回,判断不缓存的条件)。

(四)@CachePut

问题@CachePut 注解的作用是什么?

回答@CachePut 注解用于方法上,缓存方法的执行结果。与 @Cacheable 不同,它的执行过程如下:执行方法,获得方法结果;根据是否满足缓存的条件,如果满足,则缓存方法结果到缓存;返回方法结果。

(五)@CacheEvict

问题@CacheEvict 注解的作用是什么?

回答@CacheEvict 注解用于方法上,删除缓存。相比 @CachePut,它额外多了两个属性:allEntries(是否删除缓存名下所有 key 对应的缓存),beforeInvocation(是否在方法执行前删除缓存)。

(六)Spring Boot 缓存集成

问题:如何在 Spring Boot 中集成缓存?

回答 :在 Spring Boot 中,可以通过引入 spring-boot-starter-cache 依赖来集成缓存。Spring Boot 会自动判断使用哪种缓存方案,也可以通过 spring.cache.type 手动指定。

(七)Ehcache 和 Redis

问题:Ehcache 和 Redis 的区别是什么?

回答:Ehcache 是一个纯 Java 的进程内缓存框架,具有快速、精干等特点,是 Hibernate 中默认的 CacheProvider 。Redis 是一个基于键值对的内存数据库,支持多种数据类型,如字符串、哈希、列表、集合、有序集合等。Ehcache 适合用于本地缓存,Redis 适合用于分布式缓存。

(八)缓存策略

问题:常见的缓存策略有哪些?

回答:常见的缓存策略包括:读写分离(将读操作分流到从节点,避免主节点压力过多),分库分表(将读写操作分摊到多个节点,避免单节点压力过多),缓存系统(使用缓存系统(如 Redis、Ehcache)来存储经常访问的数据,减少对数据库的直接访问)。

(九)缓存击穿

问题:什么是缓存击穿?如何解决?

回答:缓存击穿是指多个线程同时访问同一个缓存键,导致缓存失效,多个线程同时查询数据库,造成数据库压力。解决方法包括:使用互斥锁(如 Redis 的 SETNX 命令),保证同一时刻只有一个线程查询数据库;使用缓存预热,在系统启动时预先加载缓存。

(十)缓存穿透

问题:什么是缓存穿透?如何解决?

回答:缓存穿透是指查询一个不存在的数据,导致缓存和数据库都没有命中,直接查询数据库。解决方法包括:使用布隆过滤器,快速判断数据是否存在;对不存在的数据也进行缓存,设置较短的过期时间。

(十一)缓存雪崩

问题:什么是缓存雪崩?如何解决?

回答:缓存雪崩是指大量缓存同时失效,导致大量请求直接查询数据库,造成数据库压力。解决方法包括:使用缓存预热,在系统启动时预先加载缓存;设置不同的过期时间,避免大量缓存同时失效。

(十二)缓存的过期策略

问题:缓存的过期策略有哪些?

回答:缓存的过期策略包括:设置固定过期时间(如 Redis 的 EXPIRE 命令),根据访问频率动态调整过期时间,根据数据的重要性动态调整过期时间。

(十三)缓存的淘汰策略

问题:缓存的淘汰策略有哪些?

回答:缓存的淘汰策略包括:先进先出(FIFO),最近最少使用(LRU),最不经常使用(LFU),随机淘汰。

(十四)缓存的序列化

问题:缓存的序列化有哪些方式?

回答:缓存的序列化方式包括:Java 序列化,JSON 序列化,Protobuf 序列化。Java 序列化简单,但性能较差;JSON 序列化性能较好,但需要手动实现序列化和反序列化;Protobuf 序列化性能最好,但需要定义.proto 文件。

(十五)缓存的分布式锁

问题:如何在分布式环境下实现缓存的锁?

回答:在分布式环境下,可以使用 Redis 的 SETNX 命令实现分布式锁。SETNX 命令可以保证同一时刻只有一个线程获取到锁,从而避免缓存击穿等问题。

(十六)缓存的事务

问题:缓存是否支持事务?

回答:缓存本身不支持事务,但可以通过与数据库事务结合来实现。例如,在数据库事务提交后,更新缓存;在数据库事务回滚后,回滚缓存。

(十七)缓存的监控

问题:如何监控缓存的使用情况?

回答:可以使用 Redis 的 INFO 命令监控缓存的使用情况,包括内存使用、命中率、过期键数等。也可以使用第三方监控工具,如 Prometheus 和 Grafana。

(十八)缓存的优化

问题:如何优化缓存的性能?

回答:优化缓存性能的方法包括:选择合适的缓存策略,使用高效的序列化方式,合理设置缓存的过期时间和淘汰策略,使用分布式锁避免缓存击穿,监控缓存的使用情况并及时调整。

(十九)缓存的命中率

问题:如何计算缓存的命中率?

回答:缓存的命中率可以通过以下公式计算:命中率 = 命中次数 /(命中次数 + 未命中次数)。可以通过监控工具获取命中次数和未命中次数。

(二十)缓存的场景

问题:缓存适用于哪些场景?

回答:缓存适用于以下场景:经常访问的数据,如用户信息、商品信息等;数据变化不频繁,如配置信息;对实时性要求不高的数据,如统计信息。

(二十一)缓存的局限性

问题:缓存有哪些局限性?

回答:缓存的局限性包括:缓存数据与数据库数据不一致,缓存击穿、缓存穿透、缓存雪崩等问题,缓存的内存有限,缓存的序列化和反序列化可能影响性能,缓存的分布式锁实现复杂。

(二十二)缓存的未来发展趋势

问题:缓存技术未来的发展趋势是什么?

回答:缓存技术未来的发展趋势包括:与数据库的深度融合,支持事务和一致性;支持更多的数据类型和查询方式;提供更好的性能和扩展性;提供更便捷的监控和管理工具。

以上就是对 Spring Boot 缓存 Cache 入门的详细讲解和面试回答思路及答案。希望对初学者有所帮助,祝大家学习愉快!

相关推荐
欢乐少年19043 分钟前
SpringBoot集成Sentry日志收集-3 (Spring Boot集成)
spring boot·后端·sentry
夏天的味道٥1 小时前
使用 Java 执行 SQL 语句和存储过程
java·开发语言·sql
冰糖码奇朵3 小时前
大数据表高效导入导出解决方案,mysql数据库LOAD DATA命令和INTO OUTFILE命令详解
java·数据库·sql·mysql
好教员好3 小时前
【Spring】整合【SpringMVC】
java·spring
浪九天4 小时前
Java直通车系列13【Spring MVC】(Spring MVC常用注解)
java·后端·spring
小斌的Debug日记4 小时前
框架基本知识总结 Day16
redis·spring
堕落年代4 小时前
Maven匹配机制和仓库库设置
java·maven
m0_748246874 小时前
maven导入spring框架
数据库·spring·maven
功德+n4 小时前
Maven 使用指南:基础 + 进阶 + 高级用法
java·开发语言·maven
uhakadotcom4 小时前
Apache CXF 中的拒绝服务漏洞 CVE-2025-23184 详解
后端·面试·github