本文主要内容
- SpringCache规范的基本用法:都有哪些技术组成,代码语法是什么,怎么用?
- SpringCache整合Redis的一般步骤:SpringCache与Redis是通过什么建立起联系的,具体步骤是什么?
文章前置知识:SpringBoot、Redis
内容导览
SpringCache是一种规范
Spring 3.1开始,引入了Spring Cache,即Spring缓存抽象。
- 通过定义springframework.cache.Cache和org.springframework.cache.CacheManager接口来统一不同的缓存技术,并支持使用JCache注解简化开发过程。
- Cache接口:为缓存的组件规范定义,包含缓存的各种操作集合。
- CacheManager:指定缓存的底层实现。例如RedisCache,EhCacheCache,ConcurrentMapCache等。
源码所在位置:spring-context
Cache接口
Cache接口抽象了缓存的 get put evict 等相关操作。
接口规范的中文注释说明
public interface Cache {
//Cache名称
String getName();
//Cache负责缓存的对象
Object getNativeCache();
/**
* 获取key对应的ValueWrapper
* 没有对应的key,则返回null
* key对应的value是null,则返回null对应的ValueWrapper
*/
@Nullable
Cache.ValueWrapper get(Object key);
//返回key对应type类型的value
@Nullable
<T> T get(Object key, @Nullable Class<T> type);
//返回key对应的value,没有则缓存Callable::call,并返回
@Nullable
<T> T get(Object key, Callable<T> valueLoader);
//缓存目标key-value(替换旧值),不保证实时性
void put(Object key, @Nullable Object value);
//插入缓存,并返回该key对应的value;先调用get,不存在则用put实现
@Nullable
default Cache.ValueWrapper putIfAbsent(Object key, @Nullable Object value) {
Cache.ValueWrapper existingValue = this.get(key);
if (existingValue == null) {
this.put(key, value);
}
return existingValue;
}
//删除缓存,不保证实时性
void evict(Object key);
//立即删除缓存:返回false表示剔除前不存在制定key活不确定是否存在;返回true,表示该key之前存在
default boolean evictIfPresent(Object key) {
this.evict(key);
return false;
}
//清除所有缓存,不保证实时性
void clear();
//立即清楚所有缓存,返回false表示清除前没有缓存或不能确定是否有;返回true表示清除前有缓存
default boolean invalidate() {
this.clear();
return false;
}
public static class ValueRetrievalException extends RuntimeException {
@Nullable
private final Object key;
public ValueRetrievalException(@Nullable Object key, Callable<?> loader, Throwable ex) {
super(String.format("Value for key '%s' could not be loaded using '%s'", key, loader), ex);
this.key = key;
}
@Nullable
public Object getKey() {
return this.key;
}
}
//缓存值的一个包装器接口,实现类为SimpleValueWrapper
@FunctionalInterface
public interface ValueWrapper {
@Nullable
Object get();
}
}
抽象类AbstractValueAdaptingCache实现了Cache接口,主要抽象了对NULL值的处理逻辑。
- allowNullValues属性表示是否允许处理NULL值的缓存
- fromStoreValue方法处理NULL值的get操作,在属性allowNullValues为true的情况下,将NullValue处理为NULL
- toStoreValue方法处理NULL值得put操作,在属性allowNullValues为true的情况下,将NULL处理为NullValue,否则抛出异常
- toValueWrapper方法提供Cache接口中get方法的默认实现,从缓存中读取值,再通过fromStoreValue转化,最后包装为SimpleValueWrapper返回
- ValueWrapper get(Object key)和T get(Object key, @Nullable Classtype)方法基于上述方法实现
- ValueWrapper get(Object key)和@Nullable Classtype)方法基于上述方法实现
- lookup抽象方法用于给子类获取真正的缓存值
- ConcurrentMapCache继承了抽象类AbstractValueAdaptingCache,是Spring Cache的默认缓存实现。
- 它支持对缓存对象copy的缓存,由SerializationDelegate serialization 处理序列化,默认为 null 即基于引用的缓存。
- 缓存相关操作基于基类 AbstractValueAdaptingCache 的 null 值处理,默认允许为 null。
CacheManager接口
核心功能
- 指定底层的缓存技术(如ConcurrentHashMap、Redis等)
- 为换组分组,统一管理
- 功能点一:CacheManager 基于 name 管理一组 Cache。
- 功能点二:诸多缓存的底层实现
- ConcurrentMapCacheManager:内置默认的实现,基于Map
- AbstractCacheManager:可以定继承该抽象类,实现我们自己的底层实现。例如,RedisCacheManager是Redis依赖包提供的相关实现
缓存的实现
- EhCacheCacheManager 使用EhCache作为缓存的实现
- JCacheCacheManageer 使用JCache作为缓存的实现
- GuavaCacheManager 使用Google的GuavaCache作为缓存的实现
- RedisCacheManager 使用Redis作为缓存的实现
注解
@EnableCaching:开启SpringCache,一般在缓存核心配置类上使用
@CacheConfig
- 定义在类上,通常使用cacheNames。定义之后,该类下的所有含缓存注解的key之前都会拼接其属性值(附带两个::)。
- 适用于某一个service的实现类或者mapper。
- 注意:如果service和mapper同时使用该注解并指定cacheNames,则以service上指定的cacheNames为准
@Caching注解中包含了@Cacheable、@CachePut和@CacheEvict注解,可以同时指定多个缓存规则。
Key的命名
- 缓存数据时,默认以类名+方法名+参数以键,以方法的返回值为value进行缓存
- 当然,key可以自定义
@Cacheable
功能:
- 用于方法上,待方法运行结束时,缓存该方法的返回值
- 每次执行该方法前,会先去缓存中查有没有相同条件下,缓存的数据,有的话直接拿缓存的数据,没有的话执行方法,并将执行结果返回。
- 默认以类名+方法名+参数为key,返回值为value
- 用于查询操作的方法上
实例
@Cacheable(cacheNames = {"emp"}, key = "#id", conditon = "mid>0", unless = "#result == null")
public Employee getEmp(Integer id) {
Employee emp = employeeMapper.getEmpById(id);
return emp;
}
参数释义
- cacheNames/value(指定缓存组件的名字,可以指定多个)
- 会覆盖放在类上的@ConfigConfig{cacheNames}数据
- key:缓存数据时使用的key
- 默认使用方法参数的值(类名+方法名+参数),也可以自定义
- 可以为null
- 可以通过SpEL进行自定义
- keyGenerator(key的生成器,可以自定义,key与keyGenerator二选一)
- cacheManager(指定缓存管理器,或者使用cacheResolver指定获取解析器)
- condition:符合条件才缓存
- unless(符合条件则不缓存,可以获取方法运行结果进行判断)
- condition默认为true,unless默认为false。
- condition为false时,unless为true。不被缓存
- condition为false,unless为false。不被缓存
- condition为true,unless为true。 不被缓存
- conditon为true,unless为false。缓存
- sync(是否使用异步模式,不可与unless一起使用)
- 在一个多线程的环境中,某些操作可能被相同的参数并发地调用,这样同一个 value 值可能被多次计算(或多次访问 db),这样就达不到缓存的目的。
- 针对这些可能高并发的操作,我们可以使用 sync 参数来告诉底层的缓存提供者将缓存的入口锁住,这样就只能有一个线程计算操作的结果值,而其它线程需要等待,这样就避免了 n-1 次数据库访问。
@CachePut
@CachePut注解先调用目标方法,然后再缓存目标方法的结果。用于新增、更新操作的方法上。
@CachePut(value = "emp", key = "#result.id")
public Employee updateEmp(Employee employee) {
employeeMapper.updateEmp(employee);
return employee;
}
@CacheEvict
功能
- 删除缓存,每次调用它注解的方法,就会执行删除指定的缓存
- 用于删除操作的方法上
@CacheEvict(value = "emp", key = "#id", allEntries = true)
public void deleteEmp(Integer id) {
employeeMapper.deleteEmpById(id);
}
参数释义
- allEntries:默认为false,为true时,表示清空该cachename下的所有缓存
- beforeInvocation:默认为false,为true时,先删除缓存,再删除数据库。
SpringCache默认实现的示例
默认情况下,Spring使用ConcurrentMapCacheManager来实现缓存。
数据直接存储在内存中,当项目重启后,所有的缓存数据都消失了。对于中大型项目非常不合适。
POM
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
开启SpringCache
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.ApplicationPidFileWriter;
import org.springframework.cache.annotation.EnableCaching;
@EnableCaching //开启SpringCache
@SpringBootApplication
public class BootApp {
public static void main(String[] args) {
SpringApplication.run(BootApp.class, args);
}
}
CacheService
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
//该类下的所有含缓存注解的key之前都会拼接其属性值(附带两个::)
@CacheConfig(cacheNames = "cacheService")
@Service
public class CacheService {
/**
* 每次请求,先看缓存中有没有,如果没有再执行方法
* #p0:表示取方法入参的第一个参数
*/
//@Cacheable(key = "'id:'+#p0",condition = "#id>8") 等价于下一行代码
@Cacheable(key = "'id:'+#id",condition = "#id>8")
public String findOne(Long id) {
System.out.println("Cacheable 第二次访问,如果走了缓存,就不会显示这句");
return "Cacheable-cacheKey=cacheService::id:" + id;
}
/**
* 先走删除缓存,再执行方法
* allEntries置为true表示删除所有缓存
* beforeInvocation = true,将删除缓存行为在方法执行之前
*/
@CacheEvict(key = "'id:'+#id",beforeInvocation =true)
public String deleteOne(Long id) {
System.out.println("模拟删除了一条记录");
return "CacheEvict-cacheKey=cacheService::id:" + id;
}
/**
* 先调用目标方法,然后再缓存目标方法的结果
*/
@CachePut(key = "'id:'+#p0")
public String updateById(Long id) {
System.out.println("模拟进行了数据库更新操作");
return "CachePut-cacheKey=cacheService::id:" + id;
}
}
测试
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cache.Cache;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@SpringBootTest
class CacheServiceTest {
@Autowired
private CacheService cacheService;
@Autowired
private ConcurrentMapCacheManager concurrentMapCacheManager;
public void getCacheData(List<String> keys) {
System.out.println("————获取ConcurrentMapCacheManager中的所有缓存数据————");
Collection<String> cacheNames = concurrentMapCacheManager.getCacheNames();
for (String cacheName : cacheNames) {
System.out.println(cacheName);
Cache cache = concurrentMapCacheManager.getCache(cacheName);
assert cache != null;
for (String key : keys) {
String val = cache.get(key).get().toString();
System.out.println(">>>key=" + key + " val=" + val);
}
}
}
@Test
public void testFindOne() {
for (int i = 0; i < 5; i++) {
String result = cacheService.findOne(111L);
System.out.println(result);
}
//显示缓存信息
List<String> keyList = new ArrayList<>();
keyList.add("id:111");
getCacheData(keyList);
}
@Test
public void testDeleteOne() {
String result = cacheService.deleteOne(111L);
System.out.println(result);
//显示缓存信息
List<String> keyList = new ArrayList<>();
keyList.add("id:111");
getCacheData(keyList);
}
@Test
public void testUpdateById() {
for (int i = 0; i < 5; i++) {
String result = cacheService.updateById(111L + i);
System.out.println(result);
}
//显示缓存信息
List<String> keyList = new ArrayList<>();
keyList.add("id:111");
keyList.add("id:112");
keyList.add("id:113");
keyList.add("id:114");
keyList.add("id:115");
getCacheData(keyList);
}
}
整合Redis的一般步骤
整合Redis
Redis与SpringCache的关系?
- SpringCache是Spring对缓存的一种规范
- Redis才是真正进行缓存的具体工具
- 可以类比: JDBC规范与实现该规范的MySQL驱动(mysql.cj.jdbc.Driver)一样
如何整合SpringCache与Redis?
- 准备Redis环境:POM依赖、YML配置项、在配置类里注入RedisTemplate
- 准备SpringCache环境:POM、YML配置项
- 两者是如何建立起联系的?指定CacheManager为RedisCacheManager
- 写一个配置类继承于springframework.cache.annotation.CachingConfigurerSupport
- 重写cacheManager()或cacheResolver()方法:指定缓存的具体实现为RedisCacheManager
- 重写keyGenerator()方法:指定缓存Key的生成策略
- 重写errorHandler()方法:指定缓存出错的处理逻辑
- 在需要进行缓存操作的方法上使用合适的注解
GitHub完整示例:https://github.com/HackyleShawe/JavaDemos/tree/master/ComponentsIntegration/springcache-redis
整合Redis环境
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
spring:
application:
name: springcache-redis
redis:
host: 127.0.0.1
port: 6379
password: #Redis服务器连接密码(默认为空)
timeout: 30000 #连接超时时间(毫秒)
jedis:
pool:
max-active: 20 # 连接池最大连接数(使用负值表示没有限制)
max-wait: -1 # 连接池最大阻塞等待时间(使用负值表示没有限制)
max-idle: 10 # 连接池中的最大空闲连接
min-idle: 0 # 连接池中的最小空闲连接
写一个配置类,注入RedisTemplate
@Configuration
public class RedisConfig {
/**
* RedisTemplate配置
* 注意:对Value的序列化是以二进制的形式存储于内存,如果想要直接看到中文等非ASCII数据,则需要重写改写redisTemplate的序列化规则
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 配置连接工厂
template.setConnectionFactory(factory);
//使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式,以二进制的形式存储在内容中)
Jackson2JsonRedisSerializer<Object> jsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
// 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常
//om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
jsonRedisSerializer.setObjectMapper(om);
// 值采用json序列化
template.setValueSerializer(jsonRedisSerializer);
//使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
// 设置hash的key和value序列化模式
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(jsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
整合SpringCache
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
spring:
cache:
type: redis
redis:
time-to-live: 3600000 #设置缓存过期时间
key-prefix: CACHE_ #指定默认前缀,如果此处我们指定了前缀则使用我们指定的前缀,推荐此处不指定前缀
cache-null-values: true #是否缓存空值,防止缓存穿透
SpringCache配置类(SpringCacheRedisConfig)
- 继承于springframework.cache.annotation.CachingConfigurerSupport
- 重写cacheManager()或cacheResolver()方法:指定缓存的具体实现,因为要整合Redis,所以返回的CacheManager为RedisCacheManager
- 重写keyGenerator()方法:指定缓存Key的生成策略
- 重写errorHandler()方法:指定缓存出错的处理逻辑
@Configuration
@EnableCaching // 开启springCache
public class SpringCacheRedisConfig extends CachingConfigurerSupport {
private static final Logger LOGGER = LoggerFactory.getLogger(SpringCacheRedisConfig.class);
/**
* 缓存管理器:指定使用那种缓存的具体实现,方式一
*
* 只有CacheManger才能扫描到cacheable注解
* Value使用Jackson工具序列化
*/
@Bean
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheManager cacheManager = RedisCacheManager.RedisCacheManagerBuilder
.fromConnectionFactory(connectionFactory) //Redis链接工厂
//缓存配置 通用配置 默认存储一小时
.cacheDefaults(getCacheConfigurationWithTtl(Duration.ofHours(1)))
//配置同步修改或删除 put/evict
.transactionAware()
//对于不同的cacheName我们可以设置不同的过期时间
//.withCacheConfiguration("app:",getCacheConfigurationWithTtl(Duration.ofHours(5)))
//.withCacheConfiguration("user:",getCacheConfigurationWithTtl(Duration.ofHours(2)))
.build();
return cacheManager;
}
private RedisCacheConfiguration getCacheConfigurationWithTtl(Duration duration) {
return RedisCacheConfiguration
.defaultCacheConfig()
//设置key value的序列化方式
// 设置key为String
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
// 设置value 为自动转Json的Object
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
// 不缓存null
.disableCachingNullValues()
// 设置缓存的过期时间
.entryTtl(duration);
}
/**
* 缓存管理器:指定使用那种缓存的具体实现,方式二
*
* Value使用FastJSON序列化,需导入fastjson依赖包
*/
//@Bean
//public RedisCacheConfiguration redisCacheConfiguration() {
// FastJsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class);
// RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
// config = config
// .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(fastJsonRedisSerializer))
// .entryTtl(Duration.ofHours(2)); //默认有效时间为2h
// return config;
//}
}
缓存业务
@CacheConfig(cacheNames = "cacheDemo")
@Service
public class CacheDemoService {
/**
* 先调用目标方法,然后再缓存目标方法的结果
* #p0:表示取方法入参的第一个参数,并取属性为uid的值
*/
//@CachePut(key = "'uid:'+#p0.uid") 等价于下一行代码
@CachePut(key = "'uid:'+#userinfo.uid")
public String saveUser(UserinfoDto userinfo) {
//模拟保存、更新操作操作
System.out.println("模拟落库操作,第二次请求如果走缓存,则不会执行到这句。");
return "保存、更新操作:cacheKey=uid:"+userinfo.getUid()+" ;userinfo="+userinfo;
}
@CacheEvict(key = "'uid:' + #p0")
public String deleteUser(long uid) {
System.out.println("模拟删除操作,第二次请求如果走缓存,则不会执行到这句。");
return "删除操作:cacheKey=uid:" +uid +" ;uid="+uid;
}
/**
* 用于方法上,可以将方法的运行结果进行缓存,之后就不用调用方法了,直接从缓存中取值即可
* #p0:表示取方法入参的第一个参数
*/
//@Cacheable(key = "'uid:'+#p0") 等价于下一行代码
@Cacheable(key = "'uid:'+#uid")
//@Cacheable(cacheNames = {"uid:"}, key = "'uid:'+#uid") cacheNames为本个缓存指定单独的cacheName,
public String queryUserById(long uid) {
//模拟查库
UserinfoDto userinfo = new UserinfoDto();
userinfo.setUid(uid);
userinfo.setName("Kyle");
userinfo.setGender("man");
System.out.println("模拟查库操作,第二次请求如果走缓存,则不会执行到这句。");
return "查询结果:cacheKey=uid:" +uid+ " ;uid="+uid+" ;userinfo="+userinfo;
}
}
测试
@RestController
public class CacheDemoController {
@Autowired
private CacheDemoService cacheDemoService;
@GetMapping("/testCachePut")
public String testCachePut() {
UserinfoDto userinfo = new UserinfoDto();
userinfo.setUid(111);
userinfo.setName("Kyle");
userinfo.setGender("man");
return cacheDemoService.saveUser(userinfo);
}
@GetMapping("/testCacheEvict")
public String testCacheEvict() {
return cacheDemoService.deleteUser(111);
}
@GetMapping("/testCacheable")
public String testCacheable() {
return cacheDemoService.queryUserById(333);
}
}
删除Redis中的所有数据
测试CachePut
Redis中已存入相关数据
测试CacheEvict
缓存已删除
测试Cacheable
缓存成功
Name:
Email:
Link: