简单的spring缓存 Cacheable学习
简单的spring缓存 Cacheable学习
1.需求
项目中有很多的方法查询的数据其实是不会经常变的,但是其整体的查询sql以及调用第三方数据获取数据花费的时间很长,现在考虑对此类型的接口进行优化,首先想到的是对其进行缓存操作,所以简单了解了一下spring的缓存注解使用,此处进行简单的整理,方便后续复习项目使用。
2.简介
@Cacheable 是 Spring 框架提供的一个缓存注解,主要用于声明式地启用方法级别的缓存功能。它允许开发人员在不改变业务逻辑的前提下,通过简单的注解配置来实现缓存机制,从而提高应用程序的性能。
-
主要概念
缓存(Cache): 在计算机科学中,缓存是一种用于存储经常访问的数据的技术,目的是为了加快数据的访问速度。缓存通常存储在比原始数据源(如数据库)更快的存储介质中。声明式缓存: 通过注解或 XML 配置来声明哪些方法应该被缓存,而不是通过编程方式显式地管理缓存逻辑。
-
缓存的工作原理
当一个带有 @Cacheable 注解的方法被调用时,Spring 会根据配置的缓存键(默认是基于方法参数的哈希值)去查找缓存中是否已经有该方法的执行结果。如果找到了相应的缓存条目,则直接返回缓存中的结果,不再执行方法体内的逻辑。如果没有找到,则执行方法并将其结果存储到缓存中,以便后续相同的请求可以直接从缓存中获取结果。
相对于自己编写的缓存,spring框架的自带缓存组件有以下优势:
- 简化开发: @Cacheable 注解使得开发人员无需手动编写缓存逻辑,通过简单的注解配置即可启用缓存功能。
- 透明性: 缓存操作对于业务逻辑来说是透明的,开发人员只需要关注业务逻辑的实现,而不需要关心缓存的具体实现细节。
- 集中管理: 缓存策略可以通过配置文件或注解集中管理,便于维护和调整。
- 灵活的缓存策略: 支持多种缓存管理机制,如基于时间的失效策略、基于访问次数的淘汰策略等。
- 支持多种缓存存储: 可以很容易地集成多种缓存存储方案,如 Ehcache、Redis、Caffeine 等。
- 减少数据库负载: 对于那些读操作远多于写操作的场景,使用 @Cacheable 可以显著减少对数据库的访问,提高应用的整体性能。
- 自动化的缓存管理: Spring 会自动处理缓存的存储、查找和过期等操作,减少了手动管理缓存时可能引入的错误。
3. 简单的使用
3.1 简单的注解认识
我们使用spring的缓存主要使用的是 @Cacheable
、@CachePut
和 @CacheEvict
这三个注解,那么简单的介绍一下这三个注解
-
@Cacheable
作用: 用于标记一个方法,使得在调用该方法之前,Spring 会尝试从缓存中获取数据。如果缓存中有对应键的数据,则直接返回缓存中的数据,不会执行方法本身。如果缓存中没有数据,则执行方法并将结果存储到缓存中。
用法: 通常用于那些计算成本较高、调用频繁且结果可预测的方法上。
属性:- value/cacheNames: 指定缓存的名字,可以设置一个或者多个。
- key: 定义缓存的键,默认情况下通常是基于方法参数的组合来生成键。
- unless: 可以定义一个条件表达式,表达式为真,不缓存此方法的结果,结果result获取。eg: unless="#name.length() <=10 ",名称长度小于等于10则不缓存。
- condition: 定义了一个条件表达式,只有当表达式为真时才缓存此方法的结果。 eg: condition="#name.length() <=10 ",名称长度小于等于10则缓存。
-
@CachePut
作用: 标记的方法会在每次调用后都将结果存入缓存中。与 @Cacheable 不同的是,@CachePut 总是会执行方法体内的逻辑,然后再把结果放入缓存。
用法: 适用于那些需要先执行业务逻辑再更新缓存的情况。
属性: 类似于 @Cacheable,可以指定 value/cacheNames、key 等属性。 -
@CacheEvict
作用: 用于清除缓存中的一个或多个条目。可以在方法调用前后清除缓存,这取决于 beforeInvocation 属性是否设置为 true。
用法: 通常在修改数据之后使用,以保证缓存的一致性。
属性:- value/cacheNames: 指定要清除的缓存的名字。
- key: 定义要清除的缓存键。
- allEntries: 如果设置为 true,则清除整个缓存。
- beforeInvocation: 如果设置为 true,则在方法调用前清除缓存,默认为 false,即方法调用后清除。
我们编写缓存的key以及condition条件等,使用的是 spEL
表达式方式,常用的表达式如下:
名称 | 位置 | 描述 | 示例 |
---|---|---|---|
methodName | root object | 当前被调用的方法名称 | #root.methodName |
method | root object | 当前被调用的方法 | # root.method.name |
target | root object | 当前被调用的目标对象 | #root.target |
targetClass | root object | 当前被调用的对象类 | #root.targetClass |
args | root object | 当前被调用的方法的参数列表 | #root.args[0] |
caches | root object | 当前方法调用使用的缓存列表[如 @Cacheable(value={"cache1", "cache2"}) ,则有两个cache] | #root.caches[0].name |
argument name | evaluation context | 方法参数的名字,可以直接使用 #参数名,也可以使用 #p0 或者 #a0 的形式,0代表索引 | #a0 |
result | evaluation context | 方法执行完毕的返回结果值(仅当方法执行后的判定有效) | #result |
3.2 注解的简单使用
下面简单使用注解进行信息缓存,主要测试缓存功能,则没有使用数据库查询测试,而是使用Thread.sleep进行模拟数据库查询,具体的pom以及service代码如下
-
引入缓存使用的pom文件
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency>
-
controller接口类
package cn.git.controller; import cn.git.entity.Person; import cn.git.service.CacheService; import com.alibaba.fastjson.JSONObject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @description: 缓存测试controller * @program: bank-credit-sy * @author: lixuchun * @create: 2024-09-23 */ @RestController @RequestMapping("/cache") public class CacheController { @Autowired private CacheService cacheService; /** * 根据id获取person * @param id id */ @GetMapping("/person/{id}") public String getPersonById(@PathVariable("id") String id) { return JSONObject.toJSONString(cacheService.getPersonById(id)); } /** * 根据id获取person,缓存自定义key * * @param id id */ @GetMapping("/person/cus/{id}") public String getPerson(@PathVariable("id") String id) { return JSONObject.toJSONString(cacheService.getPersonByCusId(id)); } /** * 更新person * @return */ @GetMapping("/update/{id}") public String updatePerson(@PathVariable("id") String id) { Person person = new Person(id, "张三", 18, "男"); return JSONObject.toJSONString(cacheService.updatePerson(person)); } /** * 根据id删除person * @param id id */ @GetMapping("/delete/{id}") public void deletePersonById(@PathVariable("id") String id) { cacheService.deletePersonById(id); } }
-
service实现代码如下
package cn.git.service.impl; import cn.git.entity.Person; import cn.git.service.CacheService; import com.alibaba.fastjson.JSONObject; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.CachePut; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; /** * @description: 缓存服务实现类 * @program: bank-credit-sy * @author: lixuchun * @create: 2024-09-23 */ @Service public class CacheServiceImpl implements CacheService { /** * 根据id获取person * * @param id id */ @Cacheable(cacheNames = "person", key = "#id") @Override public Person getPersonById(String id) { try { Thread.sleep(5000); } catch (Exception e) { e.printStackTrace(); } Person person = new Person(); person.setId(id); person.setAge(18); person.setSex("男"); person.setName("张三"); System.out.println("获取数据成功 -> " + JSONObject.toJSONString(person)); return person; } /** * 根据id获取person,缓存key自定义 * * @param id id */ @Cacheable(cacheNames = "person", keyGenerator = "customKeyGenerator") @Override public Person getPersonByCusId(String id) { try { Thread.sleep(2000); } catch (Exception e) { e.printStackTrace(); } Person person = new Person(); person.setId(id); person.setAge(18); person.setSex("男"); person.setName("张三"); System.out.println("获取数据成功 -> " + JSONObject.toJSONString(person)); return person; } /** * 更新person, 并返回person * 返回person值会被缓存,缓存的key为person的id * * @param person person */ @CachePut(cacheNames = "person", key = "#person.id") @Override public Person updatePerson(Person person) { person.setName("李四".concat(String.valueOf(System.currentTimeMillis()))); System.out.println("更新数据成功 -> " + JSONObject.toJSONString(person)); return person; } /** * 根据id删除person * 使用参数 allEntries=true 则可以删除所有缓存 * * @param id id */ @CacheEvict(cacheNames = "person", key = "#id") @Override public void deletePersonById(String id) { System.out.println("数据删除成功 -> " + id); } }
-
服务启动类添加启动缓存注解
package cn.git; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cache.annotation.EnableCaching; /** * @description: activiti迁移测试类 * @program: bank-credit-sy * @author: lixuchun * @create: 2024-06-07 */ @EnableCaching @SpringBootApplication(scanBasePackages = "cn.git") public class helloApplication { public static void main(String[] args) { SpringApplication.run(helloApplication.class, args); } }
4.整合redis并且实现缓存超时功能
整个spring缓存整合redis非常简单,引入整合redis必要的pom坐标文件,引入jar包后再编写redis的缓存管理器即可,具体的实现步骤如下。
-
引入必要pom坐标
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>2.3.8.RELEASE</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> <version>2.8.1</version> </dependency>
-
redis配置类以及序列化处理类
序列化类内容如下
package cn.git.config; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.ParserConfig; import com.alibaba.fastjson.serializer.SerializerFeature; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.SerializationException; import org.springframework.stereotype.Component; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; /** * @description: Redis序列化配置 * @program: bank-credit-sy * @author: lixuchun * @create: 2024-09-23 */ @Component public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T> { /** * 默认字符集 */ private final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; /** * 泛型类型 */ Class<T> clazz; /** * 配置全局类型支持 */ static { ParserConfig.getGlobalInstance().setAutoTypeSupport(true); } /** * 序列化构造函数 * * @param t */ @Override public byte[] serialize(T t) throws SerializationException { if (null == t) { return new byte[0]; } return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET); } /** * 反序列化 * * @param bytes */ @Override public T deserialize(byte[] bytes) throws SerializationException { if (null == bytes || 0 >= bytes.length) { return null; } String str = new String(bytes, DEFAULT_CHARSET); return JSON.parseObject(str, clazz); } }
-
redis 缓存管理器配置类如下
缓存超时功能主要是设置RedisCacheConfiguration.entryTtl(Duration.ofSeconds(60))
参数生效package cn.git.config; import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.CachingConfigurerSupport; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.interceptor.KeyGenerator; 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.serializer.RedisSerializationContext; import java.time.Duration; /** * @description: redis配置类 * @program: bank-credit-sy * @author: lixuchun * @create: 2024-09-23 */ @Slf4j @Configuration @EnableCaching public class RedisCacheConfig extends CachingConfigurerSupport { /** * redis序列化器 */ private static final FastJson2JsonRedisSerializer REDIS_SERIALIZER = new FastJson2JsonRedisSerializer(); /** * 自定义key生成器, 拼接名称class名称 + 方法名称 + 参数列表 * * @return */ @Bean(name = "customKeyGenerator") public KeyGenerator customKeyGenerator(){ // 三个参数分别为 当前类,方法,参数列表 return (object, method, params) -> { // 拼接名称class名称 + 方法名称 + 参数列表 StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append(object.getClass().getName()); stringBuilder.append(method.getName()); // 拼接参数信息 for(Object param: params){ stringBuilder.append(param == null ? "null" : param.toString()); } return stringBuilder.toString(); }; } /** * 配置缓存管理器 * * @param connectionFactory * @return */ @Bean public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) { RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() // 设置缓存的过期时间,单位秒 .entryTtl(Duration.ofSeconds(60)) // 设置key的序列化方式 .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(REDIS_SERIALIZER)) // 设置value的序列化方式 .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(REDIS_SERIALIZER)) // 不缓存null值 .disableCachingNullValues(); // 配置缓存管理器 return RedisCacheManager.builder(connectionFactory) .cacheDefaults(config) .transactionAware() .build(); } }
-
yml配置文件
spring: application: name: docker-hello # 应用程序名称,用于 Spring Cloud 的服务发现和服务注册 # redis配置 redis: database: 0 # Redis 数据库索引,默认为 0 host: 192.168.138.129 # Redis 服务器的 IP 地址 port: 6379 # Redis 服务器的端口号 timeout: 20000 # Redis 连接超时时间,单位为毫秒 # springboot2.x以上如此配置,由于2.x的客户端是lettuce lettuce: pool: max-active: 8 # 最大活动连接数,默认为 8 min-idle: 0 # 最小空闲连接数,默认为 0 max-idle: 8 # 最大空闲连接数,默认为 8 max-wait: 10000ms # 获取连接的最大等待时间,默认为 10000 毫秒 server: port: 8088
5.测试
Spring @Cacheable 默认使用的缓存管理器是ConcurrentMapCacheManager。如果没有指定其他的缓存管理器,Spring会自动使用这个默认的实现。如果我们直接使用spring @Cacheable注解,则使用的是本地ConcurrentMap进行缓存,我们不做测试了,效果不太好看,此处我们使用redis配置进行测试。
使用的redis可视化工具是RDM,我们连接到RDM,观察是没有数据的
下面开始进行测试
-
添加缓存信息
我们调用接口http://localhost:8088/cache/person/18
,进行缓存信息,发现执行接口时间为5s,再次执行发现直接响应数据,并且RDM中已经有缓存数据
RDM中缓存信息
-
添加自定义key缓存信息
我们调用接口http://localhost:8088/cache/person/cus/18
缓存自定义key缓存信息,观察RDM中自定义key样式
观察RDM中key样式,发现其为类名称+调用类方法名称+参数
的格式生成,对应到自定义的customKeyGenerator 缓存键生成策略,并且还会在60s后自动失效。
-
修改信息
我们调用接口http://localhost:8088/cache/update/18
修改name
信息,发现执行速度很快,然后再次调用查询信息以及观察RDM中发现数据已经被修改。
RDM中观察缓存信息
查询接口查看缓存名称信息
-
删除缓存信息
我们调用http://localhost:8088/cache/delete/18
进行缓存信息删除操作,删除后再次查询RDM发现缓存信息已经不在,调用添加缓存接口发现执行时间再次变更为5s
观察RDM信息
再次调用添加缓存接口,返回修改前固定名称张三