谷粒商城微服务分布式高级篇十——缓存-SpringCache

SpringCache

概述

Spring 3.1 引入了激动人心的基于注释(annotation)的缓存(cache)技术,它本质上不是一个具体的缓存实现方案(例如 EHCache 或者 OSCache),而是一个对缓存使用的抽象,通过在既有代码中添加少量它定义的各种 annotation,即能够达到缓存方法的返回对象的效果。

Spring 的缓存技术还具备相当的灵活性,不仅能够使用 SpEL(Spring Expression Language)来定义缓存的 key 和各种 condition,还提供开箱即用的缓存临时存储方案,也支持和主流的专业缓存例如 EHCache 集成。

其特点总结如下:

  • 通过少量的配置 annotation 注释即可使得既有代码支持缓存
  • 支持开箱即用 Out-Of-The-Box,即不用安装和部署额外第三方组件即可使用缓存
  • 支持 Spring Express Language,能使用对象的任何属性或者方法来定义缓存的 key 和 condition
  • 支持 AspectJ,并通过其实现任何方法的缓存支持
  • 支持自定义 key 和自定义缓存管理者,具有相当的灵活性和扩展性

一句话解释Spring Cache: 通过注解的方式,利用AOP的思想来解放缓存的管理。

基本概念

在这里插入图片描述

SpringCache

简单示例

创建Spring 项目

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.demo.cache</groupId>
    <artifactId>cache</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>cache</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

创建书籍模型

public class Book {

  private String isbn;
  private String title;

  public Book(String isbn, String title) {
    this.isbn = isbn;
    this.title = title;
  }

  public String getIsbn() {
    return isbn;
  }

  public void setIsbn(String isbn) {
    this.isbn = isbn;
  }

  public String getTitle() {
    return title;
  }

  public void setTitle(String title) {
    this.title = title;
  }

  @Override
  public String toString() {
    return "Book{" + "isbn='" + isbn + '\'' + ", title='" + title + '\'' + '}';
  }

}

创建书库

public interface BookRepository {

  Book getByIsbn(String isbn);

}

模拟延迟存储库

@Component
public class SimpleBookRepository implements BookRepository {

  @Override
  public Book getByIsbn(String isbn) {
    simulateSlowService();
    return new Book(isbn, "Some book");
  }

  //simulateSlowService故意在每个getByIsbn中插入三秒钟的延迟。稍后,将通过缓存来加速该示例。
  private void simulateSlowService() {
    try {
      long time = 3000L;
      Thread.sleep(time);
    } catch (InterruptedException e) {
      throw new IllegalStateException(e);
    }
  }

}

使用书库
CommandLineRunner注入BookRepository并使用不同参数多次调用

@Component
public class AppRunner implements CommandLineRunner {

  private static final Logger logger = LoggerFactory.getLogger(AppRunner.class);

  private final BookRepository bookRepository;

  public AppRunner(BookRepository bookRepository) {
    this.bookRepository = bookRepository;
  }

  @Override
  public void run(String... args) throws Exception {
    logger.info(".... Fetching books");
    logger.info("isbn-1234 -->" + bookRepository.getByIsbn("isbn-1234"));
    logger.info("isbn-4567 -->" + bookRepository.getByIsbn("isbn-4567"));
    logger.info("isbn-1234 -->" + bookRepository.getByIsbn("isbn-1234"));
    logger.info("isbn-4567 -->" + bookRepository.getByIsbn("isbn-4567"));
    logger.info("isbn-1234 -->" + bookRepository.getByIsbn("isbn-1234"));
    logger.info("isbn-1234 -->" + bookRepository.getByIsbn("isbn-1234"));
  }

}

此时尝试运行该应用程序,则即使您多次检索完全相同的书,也是非常的慢。以下示例输出显示了我们的(故意延迟)代码创建的三秒延迟:
在这里插入图片描述
接下来给获取书方法加上缓存@Cacheable

@Component
public class SimpleBookRepository implements BookRepository {

  @Override
  @Cacheable("books")
  public Book getByIsbn(String isbn) {
    simulateSlowService();
    return new Book(isbn, "Some book");
  }

  // simulateSlowService故意在每个getByIsbn中插入三秒钟的延迟。稍后,您将通过缓存来加速该示例。
  private void simulateSlowService() {
    try {
      long time = 3000L;
      Thread.sleep(time);
    } catch (InterruptedException e) {
      throw new IllegalStateException(e);
    }
  }

}

第二步给启动类加上注解@EnableCaching开启缓存

@SpringBootApplication
//@EnableCaching注释触发后处理器检查每个的Spring bean的公共方法缓存注释的存在。如果找到了这样的注释,则会自动创建一个代理来拦截方法调用并相应地处理缓存行为。
@EnableCaching
public class CacheApplication {

	public static void main(String[] args) {
		SpringApplication.run(CacheApplication.class, args);
	}

}

启动服务发现第一次检索书仍然需要三秒钟。但是,同一本书的第二次和后续时间要快得多,这表明缓存正在完成其工作。

恭喜你!您刚刚在Spring托管的bean上启用了缓存。

注解

@Cacheable 的作用

  • 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存

@Cacheable 主要的参数

  • value 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 例如:
    @Cacheable(value=”mycache”) 或者@Cacheable(value={”cache1”,”cache2”}

  • key 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 例如:@Cacheable(value=”testcache”,key=”#userName”)

  • condition 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存 例如:@Cacheable(value=”testcache”,condition=”#userName.length()>2”)

@CachePut 的作用

  • 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,和 @Cacheable 不同的是,它每次都会触发真实方法的调用

@CachePut 主要的参数

  • value 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 例如:
    @Cacheable(value=”mycache”) 或者@Cacheable(value={”cache1”,”cache2”}

  • key 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 例如:@Cacheable(value=”testcache”,key=”#userName”)

  • condition 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存 例如:@Cacheable(value=”testcache”,condition=”#userName.length()>2”)

@CachEvict 的作用

  • 主要针对方法配置,能够根据一定的条件对缓存进行清空(修改数据后对缓存删除)

@CacheEvict 主要的参数

  • value 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 例如:
    @CachEvict(value=”mycache”) 或者@CachEvict(value={”cache1”,”cache2”}

  • key 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 例如:@CachEvict(value=”testcache”,key=”#userName”)

  • condition 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才清空缓存 例如:@CachEvict(value=”testcache”,condition=”#userName.length()>2”)

  • allEntries 是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存 例如:@CachEvict(value=”testcache”,allEntries=true)

  • beforeInvocation 是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存

@Caching

  • @Caching注解可以让我们在一个方法或者类上同时指定多个Spring Cache相关的注解。其拥有三个属性:cacheable、put和evict,分别用于指定@Cacheable、@CachePut和@CacheEvict。
    在这里插入图片描述

基本原理

和 spring 的事务管理类似,spring cache 的关键原理就是 spring AOP,通过 spring AOP,其实现了在方法调用前、调用后获取方法的入参和返回值,进而实现了缓存的逻辑。我们来看一下下面这个图:

在这里插入图片描述
上图显示,当客户端“Calling code”调用一个普通类 Plain Object 的 foo() 方法的时候,是直接作用在 pojo 类自身对象上的,客户端拥有的是被调用者的直接的引用。

而 Spring cache 利用了 Spring AOP 的动态代理技术,即当客户端尝试调用 pojo 的 foo()方法的时候,给他的不是 pojo 自身的引用,而是一个动态生成的代理类
在这里插入图片描述

如上图所示,这个时候,实际客户端拥有的是一个代理的引用,那么在调用 foo() 方法的时候,会首先调用 proxy 的 foo() 方法,这个时候 proxy 可以整体控制实际的 pojo.foo() 方法的入参和返回值,比如缓存结果,比如直接略过执行实际的 foo() 方法等,都是可以轻松做到的。

整合SpringCache简化缓存开发

1)、引入依赖

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

2)、写配置

  • 自动配置了哪些

CacheAuroConfiguration会导入RedisCacheConfiguration;自动配好了缓存管理器RedisCacheManager

  • 配置使用redis作为缓存

spring.cache.type=redis

3)、启动类加注解打开缓存

@EnableCaching

4)、方法加注解 @Cacheable(“categorys”)

    /**
     * 查询所有一级分类
     *
     * @return
     */

    @Override
    @Cacheable("categorys")
    public List<CategoryEntity> getLevel1Categorys() {
        System.out.println("查询所有一级分类");
        List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));

        return categoryEntities;
    }

  • 每一个需要缓存的数据我们都来指定要放到那个名字的缓存.【缓存的分区(按照业务类型分)】
  • @Cacheable(“categorys”)
  • 代表当前方法的结果需要缓存,如果缓存中有,方法不用调用.
  • 如果缓存中没有,会调用方法,最后务方法的结果放入缓存

默认行为

  • 如果缓存中有,方法不用调用
  • key默认自动生成,缓存的名字:SimpleKey[] (自主生成的key值)
  • 缓存的value值,默认使用jdk序列化机制,将序列化后的数据存到redis
  • 默认ttL时间 -1

5)、生成的缓存
在这里插入图片描述
自定义

  • 指定生成的缓存使用的key

@Cacheable(value = “categorys”, key = “#root.method.name”)

  • 指定缓存的数据的存活时间
    配置文件中修改ttl,单位为毫秒

    spring.cache.redis.time-to-live=3600000

  • 将数据保存为json格式

需要修改缓存管理器,spring cache缓存管理器原理:
CacheAutoConfiguration缓存配置类会导入RedisCacheConfiguration;
RedisCacheConfiguration自动配置了RedisdCacheManager;
RedisCacheConfiguration初始化所有的缓存;
每个缓存决定使用什么配置;
如果redisCacheConfiguration有就用已有的,没有就用默认配置;
想改缓存的配置,只需要给容器中放一个RedisCacheConfiguration即可,就会应用到当前RedisCacheManager管理的所有缓存中;

编写MyCacheConfig 注入容器

@EnableConfigurationProperties(CacheProperties.class)
@EnableCaching
@Configuration
public class MyCacheConfig {

//    @Autowired
//    CacheProperties properties;

    //1、原来和配置文件绑定的配置类是这样子的
    //@ConfigurationProperties(prefix = "spring.cache")
    //public class CachePropertiesT
    //2、要让他生效
    //@EnableConfigurationProperties(CacheProperties.class)

    @Bean
    RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {

        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();

       config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
       config =  config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericFastJsonRedisSerializer()));

        //将配置文件中的配置生效
        CacheProperties.Redis redisProperties = cacheProperties.getRedis();
        if (redisProperties.getTimeToLive() != null) {
            config = config.entryTtl(redisProperties.getTimeToLive());
        }

        if (redisProperties.getKeyPrefix() != null) {
            config = config.prefixKeysWith(redisProperties.getKeyPrefix());
        }

        if (!redisProperties.isCacheNullValues()) {
            config = config.disableCachingNullValues();
        }

        if (!redisProperties.isUseKeyPrefix()) {
            config = config.disableKeyPrefix();
        }

        return config;

    }


}

application.properties

spring.cache.type=redis
spring.cache.redis.time-to-live=3600000
#如果指定了前缀就用我们指定的前缀,如果没有就默认使用缓存的名字作为前缀
spring.cache.redis.key-prefix=CACHE_
#是否开启key前缀
spring.cache.redis.use-key-prefix=true
#是否缓存空值。防止缓存穿透
spring.cache.redis.cache-null-values=true

ok
在这里插入图片描述

总结

Spirng-Cache的不足
1)读模式:

  • 缓存穿透:查询一个null数据。解决:缓存空数据:cache-null-values=true
  • 缓存击穿:大量并发进来同时查询一个正好过期的数据。解决:加锁,默认无锁,sync=true
  • 缓存雪崩:大量的key同时过期。解决:加随机时间。加上过期时间 spring.cache.redis.time-to-live=3600000

2)写模式:(缓存与数据库一致)

  • 1:写入加锁
  • 2:引入canal,感受到mysql的变化
  • 3:读多写多,直接去数据库查询就行

常规数据(读多写少,即时性,一致性要求不高的数据,完全可以使用spring-cache)
特殊数据:特殊设计

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值