持续创造,加速成长!这是我参与「日新计划 · 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

与之对应的装备类,大伙能够自己去看看,能装备些啥

image.png

别的,在这里进行装备的,在咱们的编写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的缓存,它的格局是这样的。

image.png

采用第二种办法时,只会整理掉getTest的分区。

变成下面这样:

image.png

上面的事例,我仅仅运用最简略的办法运用了一下 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")

测验~

image.png

当我不传值,由于不满意条件,Redis 中是不会缓存的

image.png

只要满意number>12 时才会进行缓存

image.png

下面的注解中含有的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);
 }

成果:

image.png

更新之后

image.png

@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是一个类等级的注解.

简略举个例子吧:

image.png

咱们发现在同个service类下,对不同办法添加的注解都要指定同一个缓存组件咱们能够在类头上统一抽取缓存组件,或许是缓存称号之类的~

大伙私下多试一试,就能够啦,很简略的~

其实还有一些知识的,可是说难也不难,就没有再说啦,大伙慢慢开掘吧~

注意事项

1)不建议缓存分页查询的成果。

2)根据 proxyspring 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标示的办法履行期间抛出反常,则不会清空缓存。

跋文

不知道这篇文章有没有帮助到你,期望看完的你,开开心心~


今日这个文章就写到了这里啦,我是 宁在春,一个宁愿永远活在有你的春天里的那个人

假如你觉得有所收获,就给我点点赞,点点关注吧~ 哈哈,期望收到来自你的正向反应,下一篇文章再见。

yijiansanlian.webp

写于 2022 年 10 月 23 日,作者:宁在春