1.为什么要数据缓存?
一个程序的瓶颈在于数据库,因为内存的速度是大大快于硬盘的速度的。当我们需要重复的获取相同的数据时,一次又一次的请求数据库获取远程服务,会导致大量的时间浪费在数据库查询或者远程方法调用上,导致程序性能恶化,这便是数据缓存要解决的问题。
2.Spring-Cache缓存支持
Spring定义了
org.springframework.cache.CacheManager
和org.springframework.cache.Cache
接口用来统一不同的缓存技术。其中CacheManager
是Spring提供各种缓存基数抽象接口,Cache接口包含缓存的各种技术(缓存获取
,增加
,删除
,修改
一般不会直接与该接口打交道)
3.Spring支持的CacheManager
针对不同的缓存技术,需要显示不同的CacheManager。在Spring中定义了如下CacheManager的实现
CacheManager | 描述 |
---|---|
SimpleCacheManager | 使用简单的Collection(集合)来存储缓存,主要用于测试用途 |
ConcurrentMapCacheManager | 使用ConcurrentMap来存储缓存 |
NoOpCacheManager | 仅测试用途,不会实际存储缓存 |
EhCacheManager | 使用EhCache作为缓存技术 |
GuavaCacheManager | 使用Google Guava的GuavaCache作为缓存技术 |
HazelcastCacheManager | 使用Hazelcast作为缓存技术 |
JCacheCacheManager | 支持JCache(JSR-107)标准的实现作为缓存技术。如apache |
RedisCacheManager | 使用Redis作为缓存技术 |
4.声明式缓存注解
Spring提供4个注解来声明缓存规则(
注解式的AOP方式实现
)
注解 | 解释 |
---|---|
@Cacheable | 在方法执行前Spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据。调用方法并将方法的返回值存储到缓存中 |
@CachePut | 无论怎样,都会将方法的返回值放到缓存中。@CachePut的属性与@Cacheable保持一致 |
@CacheEvict | 将一条或者多条数据从缓存中删除 |
@Caching | 通过@Caching注解组合多个注解策略在一个方法上 |
@Cacheable,@CachePut,@CacheEvict,@Caching都有value属性,置顶的事要使用的缓存名称。key属性指定的是数据在缓存中的存储键
4.1.注解属性介绍
4.1.1.@Cacheable
/**
* 该注解作用在方法上可以对该方法的结果进行缓存
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Cacheable {
/**
* 存储方法调用结果的缓存的名称
*/
@AliasFor("cacheNames")
String[] value() default {};
/**
* 存储方法调用结果的缓存的名称
*/
@AliasFor("value")
String[] cacheNames() default {};
/**
* 缓存key
*/
String key() default "";
/**
* 自动生成key的规则
*/
String keyGenerator() default "";
/**
* 缓存管理器
*/
String cacheManager() default "";
/**
* 缓存解析器
*/
String cacheResolver() default "";
/**
* 用于否决方法缓存的表达式
*/
String condition() default "";
/**
* 用于否决方法缓存的表达式
*/
String unless() default "";
/**
* 是否异步缓存
*/
boolean sync() default false;
}
4.1.2.@CachePut
/**
* 该注解作用在方法上可以对该方法的结果进行缓存
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface CachePut {
/**
* 存储方法调用结果的缓存的名称
*/
@AliasFor("cacheNames")
String[] value() default {};
/**
* 存储方法调用结果的缓存的名称
*/
@AliasFor("value")
String[] cacheNames() default {};
/**
* 缓存key
*/
String key() default "";
/**
* 自动生成key的规则
*/
String keyGenerator() default "";
/**
* 缓存管理器
*/
String cacheManager() default "";
/**
* 缓存解析器
*/
String cacheResolver() default "";
/**
* 用于否决方法缓存的表达式
*/
String condition() default "";
/**
* 用于否决方法缓存的表达式
*/
String unless() default "";
}
4.1.3.@CacheEvict
/**
* 指示方法(或类上的所有方法)触发,用于清空缓存操作
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface CacheEvict {
/**
* 存储方法调用结果的缓存的名称
*/
@AliasFor("cacheNames")
String[] value() default {};
/**
* 存储方法调用结果的缓存的名称
*/
@AliasFor("value")
String[] cacheNames() default {};
/**
* 缓存key
*/
String key() default "";
/**
* 自动生成key的规则
*/
String keyGenerator() default "";
/**
* 缓存管理器
*/
String cacheManager() default "";
/**
* 缓存解析器
*/
String cacheResolver() default "";
/**
* 触发的条件,只有满足条件的情况才会清楚缓存,默认为空,支持SpEL。
*/
String condition() default "";
/**
* true表示清除value中的全部缓存,默认为false。
*/
boolean allEntries() default false;
/**
* 是否在调用方法前进行缓存清除,默认为否
*/
boolean beforeInvocation() default false;
}
4.1.4.@Caching
/**
* 多个缓存注释(不同或相同类型)的组注释。这个注释可以用作创建自定义的元注释,使用属性覆盖的组合注释。
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Caching {
Cacheable[] cacheable() default {};
CachePut[] put() default {};
CacheEvict[] evict() default {};
}
5.开启声明式的缓存支持(使用方式)
开启声明式的缓存支持,只需要在配置类上使用@EnableCaching注解即可
package com.config;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;
/**
* @Author ScholarTang
* @Date 2021/6/14 7:15 下午
* @Desc 数据缓存配置类
*/
@Configuration
@EnableCaching
public class SpringCacheConfig {
}
6.SpringBoot的支持
Spring缓存技术的关键是配置CacheManager
,而SpringBoot为我们自动配置类多个CacheManager的实现。
SpringBoot的CacheManager的自动配置放置在org.springframework.boot.autoconfigure.cache
包中。
如上图可以看到,SpringBoot为我们自动装配了
EhCacheCacheConfiguration(使用EhCache)
GenericCacheConfiguration(使用Collection)
HazelcastCacheConfiguration(使用Hazelcast)
InfinispanCacheConfiguration(使用Infinispan)
JCacheCacheConfiguration(使用JCache)
NoOpCacheConfiguration(不使用存储)
RedisCacheConfiguration(使用Redis)
SimpleCacheConfiguration(使用ConcurrentMap)
在不做任何额外配置的情况下,默认使用的是SimpleCacheConfiguration,即使用ConcurrentMapCacheManager。
6.1.SpringBoot支持以spring.cache
为前缀的属性来配置缓存
spring:
cache:
# 程序启动是创建缓存名称
cache-names:
# ehaache 配置文件地址
ehcache:
config:
# infinispan 配置文件地址
infinispan:
# jcache 配置文件地址
jcache:
config:
# 当多个jcache实现在类路径中的时候,指定jcache实现
provider:
7.实战
实战演示为SpringBoot中的Cache配合Redis使用,在通常的情况下我们也可以使用RedisTemplate进行缓存存储和获取以及删除操作。但是本身上来说如果需要设置缓存存储获取的地方过多的话也会产生大量的重复代码。注解式缓存声明式通过AOP的方式进行缓存处理的,这样更方便缓存管理,从而也减少了大量重复代码。
但是对于某些复杂的业务场景还是建议使用RedisTemplate进行特殊处理。
7.1.环境准备
IDEA脚手架构建SpringBoot工程
7.2.pom依赖配置
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.75</version>
</dependency>
</dependencies>
7.3.yml配置
server:
port: 8067
spring:
# redis 相关配置
redis:
database: 0
host: 127.0.0.1
port: 6379
password: redis
lettuce:
pool:
max-active: 100
max-wait:
max-idle: 8
min-idle: 0
7.4.缓存配置类
package com.scholartang.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.CacheManager;
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.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
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.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.lang.reflect.Method;
import java.time.Duration;
/**
* @Author ScholarTang
* @Date 2021/6/28 11:35 上午
* @Desc redis缓存配置
*/
@EnableCaching
@Configuration
public class RedisCacheConfig extends CachingConfigurerSupport {
/**
* 构建连接工厂
*/
@Autowired
private LettuceConnectionFactory redisConnectionFactory;
/**
* redisTemplate的序列化执行器
* @param om
* @return
*/
@Bean
public GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer (ObjectMapper om) {
GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(om);
return serializer;
}
/**
* 自定义redisTemplate
* @param redisConnectionFactory
* @param serializer
* @return
*/
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory,
GenericJackson2JsonRedisSerializer serializer) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
RedisSerializer stringSerializer = new StringRedisSerializer();
//key序列化
redisTemplate.setKeySerializer(stringSerializer);
//value序列化
redisTemplate.setValueSerializer(serializer);
//Hash key序列化
redisTemplate.setHashKeySerializer(stringSerializer);
//Hash value序列化
redisTemplate.setHashValueSerializer(serializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
/**
* 缓存管理配置,使用redis
* @return
*/
@Override
@Bean
public CacheManager cacheManager() {
// 重新配置缓存
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
// 缓存过期时间
redisCacheConfiguration = redisCacheConfiguration.entryTtl(Duration.ofMinutes((60L * 20)))
.disableCachingNullValues()
//.disableKeyPrefix() 禁用关键前缀
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer((new GenericJackson2JsonRedisSerializer())));
return RedisCacheManager.builder(RedisCacheWriter
.nonLockingRedisCacheWriter(redisConnectionFactory))
.cacheDefaults(redisCacheConfiguration).build();
}
/**
* 注解式缓存,key的默认生成规则:包名+方法名+参数列表
* @return
*/
@Override
@Bean
public KeyGenerator keyGenerator() {
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... params) {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName());
sb.append(method.getName());
for (Object obj : params) {
sb.append(obj.toString());
}
return sb.toString();
}
};
}
}
7.5.实体类
package com.scholartang.model.po;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import java.io.Serializable;
/**
* @Author ScholarTang
* @Date 2021/6/28 10:59 上午
* @Desc user clazz
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class User implements Serializable {
private Integer id;
private String userName;
private String age;
}
7.6.控制层编写获取数据接口,并在接口上应用【@Cacheable/@CachePut】注解缓存或获取接口返回结果。
@Cacheable与@CachePut的用法是一样的,这里只演示@Cacheable
/**
* 获取数据的接口
* @return
*/
//cacheNames:缓存名称为 【TestController.getData】
//keyGenerator:缓存key的生成规则 【包名+类名+方法名+方法参数列表】
//unless:添加缓存的条件 【当范围结果为null是不添加缓存】
@Cacheable(cacheNames = "TestController.getData",keyGenerator = "keyGenerator",unless = "#result == null")
@GetMapping("/getData")
public Object getData() {
log.info("进入了接口内部,业务处理获取数据...");
/*
获取数据的方式,在这里我进行简单的处理。
直接创建一堆对象返回,但是在实际的场景中可能会从数据库中去查询数据返回
*/
List<User> users = new ArrayList<>();
Collections.addAll(users, new User(1, "张三", 23), new User(2, "李四", 24), new User(3, "王五", 25));
return users;
}
7.7.测试
7.7.1.第一次调用
缓存中没有,进入接口内部。接口内部进行业务处理返回结果。当结果不为空时,将结果存储到redis中
7.7.2.第二次调用
再次调用接口时,并没有进入接口内部进行业务处理。而是从缓存中获取了结果返回
7.8.控制层编写修改数据接口,并在接口上应用@CacheEvict注解删除原来旧的缓存信息
在某些业务场景中会对数据进行更新,如果在更新完原数据后,缓存中的数据还是原来之前的数据;那么就相当于出现了脏读,导致数据不准确。
所以在这里演示在修改数据时对对应缓存组中的数据进行清除。
/**
* 修改数据接口
*/
//cacheNames:需要清除的缓存名称组,这里可以自行定义你需要清除的缓存组
//allEntries:清除value中的全部缓存
@CacheEvict(cacheNames = {"TestController.getData"},allEntries = true)
@PutMapping("/updateData")
public void updateData(){
log.info("调用了修改User数据接口,将清除缓存中非最新的User数据集...");
}
7.9.测试
调用修改接口后,查看redis缓存。缓存中的数据已经被清理/删除
7.10.控制层编写接口,并在接口上应用@Caching注解
演示@Caching的使用,当该接口被调用后,缓存该接口的结果。并将获取数据接口的缓存清除
/**
* 该接口主要演示@Caching的使用
* ps:当该接口被调用后,缓存该接口的结果。并将获取数据接口的缓存清除
* @return
*/
@Caching(
cacheable = {
@Cacheable(
cacheNames = "TestController.cachingTest",
keyGenerator = "keyGenerator",
unless = "#result == null")
},
evict = {
@CacheEvict(
cacheNames = {"TestController.getData"},
allEntries = true)
})
@GetMapping("/cachingTest")
public Object cachingTest() {
List<User> users = new ArrayList<>();
Collections.addAll(
users, new User(1, "张三1", 231), new User(2, "李四1", 241), new User(3, "王五1", 251));
return users;
}
7.11.测试
在该接口被调用后,该接口返回的结果将会被缓存。对应的获取数据接口的缓存数据将会被清除