番外-Spring-Cache缓存技术的使用

引用自:https://blog.csdn.net/scholartang/article/details/118306691

一、为什么要数据缓存

一个程序的瓶颈在于数据库,因为内存的速度是大大快于硬盘的速度的。当我们需要重复的获取相同的数据时,一次又一次的请求数据库获取远程服务,会导致大量的时间浪费在数据库查询或者远程方法调用上,导致程序性能恶化,这便是数据缓存要解决的问题。

二、Spring-Cache缓存支持

Spring定义了org.springframework.cache.CacheManager和org.springframework.cache.Cache接口用来统一不同的缓存技术。其中CacheManager是Spring提供各种缓存基数抽象接口,Cache接口包含缓存的各种技术(缓存获取,增加,删除,修改一般不会直接与该接口打交道)

三、Spring支持的CacheManager

针对不同的缓存技术,需要显示不同的CacheManager。在Spring中定义了如下CacheManager的实现

CacheManager描述
SimpleCacheManager使用简单的Collection(集合)来存储缓存,主要用于测试用途
ConcurrentMapCacheManager使用ConcurrentMap来存储缓存
NoOpCacheManager仅测试用途,不会实际存储缓存
EhCacheManager使用EhCache作为缓存技术
GuavaCacheManager使用Google Guava的GuavaCache作为缓存技术
HazelcastCacheManager使用Hazelcase作为缓存技术
JCacheCacheManager支持JCache(JSR-107)标准的实现作为缓存技术。如:apache
RedisCacheManager使用Redis作为缓存技术

四、声明式缓存注解

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 {};
}

五、开启声明式的缓存支持(使用方式)

开启声明式的缓存支持,只需要在配置类上使用@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 {

}

六、SpringBoot的支持

Spring缓存技术的关键是配置CacheManager,而SpringBoot为我们自动配置类多个CacheManager的实现。 SpringBoot的CacheManager的自动配置放置在org.springframework.boot.autoconfigure.cache 包中。
18.png
如上图可以看到,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: 

七、实战

实战演示为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中
19.png
20.png

7.7.2 第二次调用

再次调用接口时,并没有进入接口内部进行业务处理。而是从缓存中获取了结果返回
21.jpg

7.8 控制层编写修改数据接口,并在接口上应用@CacheEvict注解删除原来旧的缓存信息

在某些业务场景中会对数据进行更新,如果在更新完原数据后,缓存中的数据还是原来之前的数据;那么就相当于出现了脏读,导致数据不准确。 所以在这里演示在修改数据时对对应缓存组中的数据进行清除。

  /**
   * 修改数据接口
   */
  //cacheNames:需要清除的缓存名称组,这里可以自行定义你需要清除的缓存组
  //allEntries:清除value中的全部缓存
  @CacheEvict(cacheNames = {"TestController.getData"},allEntries = true)
  @PutMapping("/updateData")
  public void updateData(){
    log.info("调用了修改User数据接口,将清除缓存中非最新的User数据集...");
  }

7.9 测试

调用修改接口后,查看redis缓存。缓存中的数据已经被清理/删除
22.png

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 测试

在该接口被调用后,该接口返回的结果将会被缓存。对应的获取数据接口的缓存数据将会被清除
23.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值