当前位置: 首页 > article >正文

芋道源码 —— 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 入门的详细讲解和面试回答思路及答案。希望对初学者有所帮助,祝大家学习愉快!


http://www.kler.cn/a/578476.html

相关文章:

  • 搭建农产品管理可视化,助力农业智能化
  • scala函数的至简原则
  • Android Retrofit + RxJava + OkHttp 网络请求高效封装方案
  • 线性表相关代码(顺序表+单链表)
  • C++蓝桥杯基础篇(九)
  • UE4 World, Level, LevelStreaming从入门到深入
  • 【Linux系统编程】初识系统编程
  • RMAN备份bug-审计日志暴涨(select action from gv$session)
  • ECC升级到S/4 HANA的功能差异 物料、采购、库存管理对比指南
  • 软件网络安全测试用例
  • 【深度学习】Pytorch:更换激活函数
  • docker-compose Install reranker(fastgpt支持) GPU模式
  • 阿里云扩容操作步骤
  • 云效、流水线、Gradle缓存问题、build.gradle配置snapshot
  • 从0开始的操作系统手搓教程28:实现Syscall架构体系
  • 8.大模型微调学习案例:基于 Hugging Face、8位量化与 LoRA 适配器的方案
  • LVS+Nginx接入层架构图
  • 利用FatJar彻底解决Jar包冲突(一)
  • STM32F407 NVIC和外部中断
  • 深度学习分类回归(衣帽数据集)