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

文章目录

  • SpringCache
    • 概述
    • 基本概念
    • 简单示例
    • 注解
    • 基本原理
    • 整合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


<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.0modelVersion>
    <parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.3.1.RELEASEversion>
        <relativePath/> 
    parent>
    <groupId>com.demo.cachegroupId>
    <artifactId>cacheartifactId>
    <version>0.0.1-SNAPSHOTversion>
    <name>cachename>
    <description>Demo project for Spring Bootdescription>
<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>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

创建书籍模型

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 + ‘’’ + ‘}’;
}

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

创建书库

public interface BookRepository {
     

Book getByIsbn(String isbn);

}

  • 1
  • 2
  • 3
  • 4
  • 5

模拟延迟存储库

@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);
}
}

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

使用书库
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”));
}

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

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

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

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

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

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

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

恭喜你!您刚刚在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.bootgroupId>
	   <artifactId>spring-boot-starter-cacheartifactId>
	dependency>
	<dependency>
	   <groupId>org.springframework.bootgroupId>
	   <artifactId> spring-boot-starter-data-redisartifactId>
	dependency>

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 每一个需要缓存的数据我们都来指定要放到那个名字的缓存.【缓存的分区(按照业务类型分)】
  • @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;

}

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46

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

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

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)
特殊数据:特殊设计

                                

你可能感兴趣的:(Gulimall)