持续创造,加速成长!这是我参与「日新计划 · 10 月更文挑战」的第6天,点击查看活动概况
前三篇文章说了那么那么多,可是咱们在运用缓存的场景中,大都数仍是会采用了类似 Spring Cache 的缓存办理器来做,说原因其实也没啥,由于项目中并不是一切的业务对数据有那么强的数据一致性。
前三篇:
聊一聊缓存和数据库不一致性问题的产生及主流解决方案以及扩展的考虑
用万字长文来讲讲本地锁至分布式锁的演进和Redis完成,扩展:Redlock 红锁
周四埋下的坑,周五来恶补!! Redisson 加锁、锁自动续期、解锁源码剖析
而 Spring Cache
正好能够帮咱们减轻开发担负,一个注解就搞定,不必自己去编程式操作。
Spring Cache 介绍
看到Spring
就知道这是Spring
生态中的东西,其实缓存数据的技术并不少,Spring 官方此举是引入 Spring Cache 来帮咱们办理缓存,运用注解,简化许多操作。
当然运用 Spring Cache 也有优缺陷的.
长处
- 运用注解,简化操作
- 缓存办理器,便利多种完成切换缓存源,如
Redis,Guava Cache等
- 支撑业务, 即事物回滚时,缓存一起自动回滚
缺陷
- 不支撑TTL,不能为每个 key 设置单独过期时间
expires time
, - 针对多线程没有专门的处理,所以当多线程时,是会产生数据不一致性的。(相同,一般有高并发操作的缓存数据,都会特殊处理,而不太运用这种办法)
Spring Cache 快速上手
不想那么多,先快速上个手,再接着详细说一说。
SpringBoot
惯例过程:
- 导入依靠
- 修正装备文件(这一步也能够直接写在第三步)
- 编写
xxxxConfig
或许是Enablexxxx
前期预备
这也相同的,别的我这里运用的是 Spring Cache
整合 Redis
做缓存。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
一般看到是spring-boot-starter
最初的依靠,都能够大胆猜想他们是有一个xxxProperties
装备类与之对应的。
修正装备文件:
spring:
redis:
host: xxxxx
password: xxxx
#指定缓存类型
cache:
type: redis
#指定存活时间(ms)
redis.time-to-live: 86400000
#是否缓存空值,能够防止缓存穿透
redis.cache-null-values: true
与之对应的装备类,大伙能够自己去看看,能装备些啥
别的,在这里进行装备的,在咱们的编写xxxConfig
类的时候,也相同能够在那里装备。
由于也要装备Redis
的装备,就把之前文章里面的东西都粘贴过来了~
/**
* @description:
* @author: Ning Zaichun
* @date: 2022年10月22日 23:21
*/
@EnableConfigurationProperties(CacheProperties.class)
@EnableCaching
@Configuration
public class MyRedisConfig {
private final Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer(Object.class);
{
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
serializer.setObjectMapper(objectMapper);
}
/**
* 1.原来的装备类形式
* @ConfigurationProperties(prefix = "spring.cache")
* public class CacheProperties {
* 由于这个并没有放到容器中,所以要让他生效 @EnableConfigurationProperties(CacheProperties.class)
* 由于这个和装备文件已经绑定生效了
* @return
*/
@Bean
RedisCacheConfiguration redisCacheConfiguration(CacheProperties CacheProperties) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
//由于key的序列化默许便是 StringRedisSerializer
// config = config.serializeKeysWith(RedisSerializationContext
// .SerializationPair
// .fromSerializer(new StringRedisSerializer()));
config = config.serializeValuesWith(RedisSerializationContext
.SerializationPair
.fromSerializer(serializer));
CacheProperties.Redis redisProperties = CacheProperties.getRedis();
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
//设置value 值的序列化
redisTemplate.setValueSerializer(serializer);
//key的序列化
redisTemplate.setKeySerializer(new StringRedisSerializer());
// set hash hashkey 值的序列化
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
// set hash value 值的序列化
redisTemplate.setHashValueSerializer(serializer);
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
return new StringRedisTemplate(redisConnectionFactory);
}
}
前期预备完毕,直接上手运用~
开始运用
controller– service–mapper 一路到底,我这里是连接了数据库,仅仅测验的话,直接在service 的回来成果中存一串字符串即可。
/**
* @description:
* @author: Ning Zaichun
* @date: 2022年09月06日 22:16
*/
@RestController
@RequestMapping("/cache")
@RequiredArgsConstructor
public class CacheController {
private final IUseSpringCache useSpringCache;
@GetMapping("/test")
public String getTest() {
return useSpringCache.getTest();
}
@GetMapping("/test2")
public String getTest2() {
return useSpringCache.getTest2();
}
@GetMapping("/test/clear")
public String clearTest() {
useSpringCache.clearTest();
return "clearTest";
}
@GetMapping
public List<MenuEntity> getMenuList() {
return useSpringCache.getMenuList();
}
@GetMapping("/clear")
public String updateMenu() {
MenuEntity menuEntity = new MenuEntity();
menuEntity.setCatId(33L);
menuEntity.setName("其他测验数据");
useSpringCache.updateMenuById(menuEntity);
return "成功清空缓存";
}
}
/**
* @description:
* @author: Ning Zaichun
* @date: 2022年09月21日 20:30
*/
public interface IUseSpringCache {
String getTest();
String getTest2();
void clearTest();
List<MenuEntity> getMenuList();
void updateMenuById(MenuEntity menuEntity);
}
/**
* @description:
* @author: Ning Zaichun
* @date: 2022年09月21日 20:30
*/
@Service
@RequiredArgsConstructor
public class UseSpringCacheImpl implements IUseSpringCache {
private final MenuMapper menuMapper;
@Cacheable(value = {"menu"}, key = "'getMenuList'")
@Override
public List<MenuEntity> getMenuList() {
System.out.println("查询数据库======");
List<MenuEntity> menuEntityList = menuMapper.selectList(new QueryWrapper<>());
return menuEntityList;
}
/**
* 级联更新一切相关的数据
*
* @param menuEntity
* @CacheEvict:失效形式
* @CachePut:双写形式,需求有回来值 1、一起进行多种缓存操作:@Caching
* 2、指定删去某个分区下的一切数据 @CacheEvict(value = "menu",allEntries = true)
* 3、存储同一类型的数据,都能够指定为同一分区
*/
// @Caching(evict = {
// @CacheEvict(value = "category",key = "'getLevel1Categorys'"),
// @CacheEvict(value = "category",key = "'getCatalogJson'")
// })
@CacheEvict(value = "menu", allEntries = true) //删去某个分区下的一切数据
@Transactional(rollbackFor = Exception.class)
@Override
public void updateMenuById(MenuEntity menuEntity) {
System.out.println("清空缓存======");
menuMapper.updateById(menuEntity);
}
@Cacheable(value = {"test"}, key = "#root.methodName")
@Override
public String getTest() {
System.out.println("测验查询了数据库");
return "我是测验缓存数据";
}
@Cacheable(value = {"test"}, key = "'getTest2'")
@Override
public String getTest2() {
System.out.println("测验查询了数据库2");
return "我是测验缓存数据2";
}
@Caching(evict = {
@CacheEvict(value = "test",key = "'getTest'")
})
@Override
public void clearTest() {
System.out.println("清空了test缓存");
}
}
测验
上面便是简略的运用,上面的注解啥的,立刻就开说哈
先讲讲事例中的两个删去缓存的注解
@CacheEvict(value = "menu", allEntries = true)
@Caching(evict = {
@CacheEvict(value = "test",key = "'getTest'")
})
两种办法,allEntries = true
表明直接清空掉整个分区,
而第二种办法,只会清掉getTest
的分区。
在Redis
的缓存,它的格局是这样的。
采用第二种办法时,只会整理掉getTest
的分区。
变成下面这样:
上面的事例,我仅仅运用最简略的办法运用了一下 Spring Cache
但其实注解上远不止这么一点东西,接下来慢慢说一说👇
大家也不必故意记,就大致知道Spring cache
能够解决什么问题即可。
Spring Cache 注解
只要运用public界说的办法才能够被缓存,而private办法、protected 办法或许运用default 修饰符的办法都不能被缓存。 当在一个类上运用注解时,该类中每个公共办法的回来值都将被缓存到指定的缓存项中或许从中移除。
@Cacheable
@CachePut
@CacheEvict
@Caching
@CacheConfig
@Cacheable
特点名 | 效果与描述 |
---|---|
cacheNames/value | 指定缓存的姓名,缓存运用CacheManager办理多个缓存Cache,这些Cache便是根据该特点进行区分。对缓存的真正增删改查操作在Cache中界说,每个缓存Cache都有自己仅有的姓名。 |
key | 缓存数据时的key的值,默许是运用办法一切入参的值。1、能够运用SpEL表达式表明key的值。2、能够运用字符串,3、能够运用办法名 |
keyGenerator | 缓存的生成策略(键生成器),和key二选一,效果是生成键值key,keyGenerator可自界说。 |
cacheManager | 指定缓存办理器(例如ConcurrentHashMap、Redis等)。 |
cacheResolver | 和cacheManager效果相同,运用时二选一。 |
condition | 指定缓存的条件(对参数判别,满意什么条件时才缓存),可用SpEL表达式,例如:办法入参为目标user则表达式能够写为condition = "#user.age>18" ,表明当入参目标user的特点age大于18才进行缓存。 |
unless | 否定缓存的条件(对成果判别,满意什么条件时不缓存),即满意unless指定的条件时,对调用办法获取的成果不进行缓存,例如:unless = "result==null" ,表明假如成果为null时不缓存。 |
sync | 是否运用异步形式进行缓存,默许false。 |
@Cacheable
指定了被注解办法的回来值是可被缓存的。其作业原理是便是AOP机制,实际上,Spring 首先查找的是缓存,缓存中没有再查询的数据库。
接下来就说说几种用法:
@Cacheable(value = "users")
//Spring 从4.0开始新增了value别号cacheNames比value更达意,引荐运用
@Cacheable(cacheNames = "users")
//归纳运用
@Cacheable(cacheNames = {"test"}, key = "'getTest3'",condition = "#number>12",unless = "#number<12")
测验~
当我不传值,由于不满意条件,Redis 中是不会缓存的
只要满意number>12 时才会进行缓存
下面的注解中含有的condition和unless
特点的都是相同的用法。
@CachePut
@CachePut
的注解特点就比@Cacheable
少了一个sync
,其余都相同。
@CachePut
注解你就直接理解为履行后更新缓存就好。
就比方我一个办法是缓存某个学生或许是某个用户信息。
然后我修正了我的个人信息什么之类的,这个时候就能够直接用上@CachePut
注解了。
比方:
/**
* studentCache
* 缓存键值key未指定默许为userNumber+userName组合字符串
*/
@Cacheable(cacheNames = "studentCache")
@Override
public Student getStudentById(String id) {
// 办法内部完成不考虑缓存逻辑,直接完成业务
return getFromDB(id);
}
/**
* 注解@CachePut:保证办法体内办法必定履行,履行完之后更新缓存;
* 相同的缓存userCache和key(缓存键值运用spEl表达式指定为userId字符串)以完成对该缓存更新;
* @param student
* @return 回来
*/
@CachePut(cacheNames = "studentCache", key = "(#student.id)")
@Override
public Student updateStudent(Student student) {
return updateData(student);
}
private Student updateData(Student student) {
System.out.println("real updating db..." + student.getId());
return student;
}
private Student getFromDB(String id) {
System.out.println("querying id from db..." + id);
return new Student(id,"宁在春","社会",19);
}
成果:
更新之后
@CacheEvict
@CacheEvict
注解是@Cachable
注解的反向操作,它负责从给定的缓存中移除一个值。大多数缓存框架都供给了缓存数据的有效期,运用该注解能够显式地从缓存中删去失效的缓存数据。
cacheNames/value、key、keyGenerator、cacheManager、cacheResolver、condition
这些和上面相同的特点就不说了
它还有两个其他的特点:
allEntries
:allEntries
是布尔类型的,用来表明是否需求铲除这个缓存分区中的的一切元素。默许值为false,表明不需求。
beforeInvocation
: 铲除操作默许是在对应办法履行成功后触发的(beforeInvocation = false),即办法假如由于抛出反常而未能成功回来时则不会触发铲除操作。运用beforeInvocation特点能够改变触发铲除操作的时间。当指定该特点值为true时,Spring会在调用该办法之前铲除缓存中的指定元素。
之前也简略的运用过了,就不多测验啦,让我偷个懒~
大伙想要啥骚操作的话,就得多去尝试~
@Caching
@Caching
注解特点一览:
特点名 | 效果与描述 |
---|---|
cacheable | 取值为根据@Cacheable 注解的数组,界说对办法回来成果进行缓存的多个缓存。 |
put | 取值为根据@CachePut 注解的数组,界说履行办法后,对回来方的办法成果进行更新的多个缓存。 |
evict | 取值为根据@CacheEvict 注解的数组。界说多个移除缓存。 |
总结来说,@Caching
是一个组注解,能够为一个办法界说供给根据@Cacheable
、@CacheEvict
或许@CachePut
注解的数组。
就比方:
你假如运用@CacheEvict(value = "test",key = "'getTest'")
这条注解,只能整理某一个分区的缓存,test下的getTest所缓存的数据,你没办法再整理其他分区的缓存。
运用了@Caching
就能够一次整理多个。
@Caching(evict = {
@CacheEvict(value = "test",key = "'getTest'"),
@CacheEvict(value = "test",key = "'getTest2'"),
@CacheEvict(value = "test",key = "'getTest3'"),
})
其他的也类似。
@CacheConfig
@CacheConfig
注解特点一览:cacheNames/value、keyGenerator、cacheManager、cacheResolver
.
一个类中或许会有多个缓存操作,而这些缓存操作或许是重复的。这个时候能够运用 @CacheConfig
是一个类等级的注解.
简略举个例子吧:
咱们发现在同个service类下,对不同办法添加的注解都要指定同一个缓存组件咱们能够在类头上统一抽取缓存组件,或许是缓存称号之类的~
大伙私下多试一试,就能够啦,很简略的~
其实还有一些知识的,可是说难也不难,就没有再说啦,大伙慢慢开掘吧~
注意事项
1)不建议缓存分页查询的成果。
2)根据 proxy
的 spring aop
带来的内部调用问题
这个问题不仅仅是出现在这里,其实只要牵扯到Spring AOP 切面的问题,都有这个问题,就像@Transactional(rollbackFor = Exception.class)
注解相同。
假定目标的办法是内部调用(即 this 引证)而不是外部引证,则会导致 proxy 失效,那么切面就失效,也便是说 @Cacheable、@CachePut 和 @CacheEvict 都会失效。
解决办法:
- 发动类添加@EnableAspectJAutoProxy(exposeProxy = true),办法内运用AopContext.currentProxy()获得代理类,运用业务。
-
@Autowired private ApplicationContext applicationContext; // 在办法中手动获取bean,再调用 applicationContext.getBean(xxxxServiceImpl.class);
3)@Cache
注解的办法必须为 public
4)默许情况下,@CacheEvict
标示的办法履行期间抛出反常,则不会清空缓存。
跋文
不知道这篇文章有没有帮助到你,期望看完的你,开开心心~
今日这个文章就写到了这里啦,我是 宁在春,一个宁愿永远活在有你的春天里的那个人。
假如你觉得有所收获,就给我点点赞,点点关注吧~ 哈哈,期望收到来自你的正向反应,下一篇文章再见。
写于 2022 年 10 月 23 日,作者:宁在春