易筋SpringBoot 2.1 | 第廿六篇:SpringBoot访问缓存抽象Cache

写作时间:2019-09-28
Spring Boot: 2.1 ,JDK: 1.8, IDE: IntelliJ IDEA

概述

如果读数据的次数比写数据的次数多的过,比如10:1, 100:1… ,读数据越多,缓存的作用就越大。访问数据库IO的效率很低,放到服务器内存降低对数据库的压力。

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

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

其特点总结如下:

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

本章介绍Spring的缓存,和Redis的缓存

几个重要概念&缓存注解

名称解释
Cache缓存接口,定义缓存操作。实现有:RedisCache、EhCacheCache、ConcurrentMapCache等
CacheManager缓存管理器,管理各种缓存(cache)组件
@Cacheable主要针对方法配置,能够根据方法的请求参数对其结果进行缓存
@CachePut主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,和 @Cacheable 不同的是,它每次都会触发真实方法的调用
@CacheEvict清空缓存
@EnableCaching开启基于注解的缓存
keyGenerator缓存数据时key生成策略
serialize缓存数据时value序列化策略
@CacheConfig统一配置本类的缓存注解的属性

@Cacheable/@CachePut/@CacheEvict 主要的参数

名称解释
value缓存的名称,在 spring 配置文件中定义,必须指定至少一个
例如:@Cacheable(value=”mycache”) 或者@Cacheable(value={”cache1”,”cache2”}
key缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合
例如:@Cacheable(value=”testcache”,key=”#id”)
condition缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存/清除缓存
例如:@Cacheable(value=”testcache”,condition=”#userName.length()>2”)
unless否定缓存。当条件结果为TRUE时,就不会缓存。@Cacheable(value=”testcache”,unless=”#userName.length()>2”)
allEntries
@CacheEvict
是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存
例如:@CachEvict(value=”testcache”,allEntries=true)
beforeInvocation
@CacheEvict
是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存
例如:@CachEvict(value=”testcache”,beforeInvocation=true)

1.工程建立

下载已经创建好的Starbucks项目
重命名根文件夹名字的CachePureDemo,用Idea打开工程。

1. 修改Service用cache

zgpeace.spring.starbucks.service.CoffeeService

package zgpeace.spring.starbucks.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.ExampleMatcher;
import org.springframework.stereotype.Service;
import zgpeace.spring.starbucks.model.Coffee;
import zgpeace.spring.starbucks.repository.CoffeeRepository;

import java.util.List;
import java.util.Optional;

import static org.springframework.data.domain.ExampleMatcher.GenericPropertyMatchers.exact;

@Slf4j
@Service
@CacheConfig(cacheNames = "coffee")
public class CoffeeService {
  @Autowired
  private CoffeeRepository coffeeRepository;

  @Cacheable
  public List<Coffee> findAllCoffee() {
    return coffeeRepository.findAll();
  }

  @CacheEvict
  public void reloadCoffee() {
  }
}

注释:

  • @CacheConfig(cacheNames = “coffee”) :配置缓存名字
  • @Cacheable 第一次读取缓存
  • @CacheEvict 清除缓存

1. Controller 访问缓存

zgpeace.spring.starbucks.StarbucksApplication

package zgpeace.spring.starbucks;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import zgpeace.spring.starbucks.model.Coffee;
import zgpeace.spring.starbucks.model.CoffeeOrder;
import zgpeace.spring.starbucks.model.OrderState;
import zgpeace.spring.starbucks.repository.CoffeeRepository;
import zgpeace.spring.starbucks.service.CoffeeOrderService;
import zgpeace.spring.starbucks.service.CoffeeService;

import java.util.Optional;

@Slf4j
@EnableTransactionManagement
@EnableJpaRepositories
@SpringBootApplication
@EnableCaching(proxyTargetClass = true)
public class StarbucksApplication implements ApplicationRunner {
  @Autowired
  private CoffeeService coffeeService;

  @Override
  public void run(ApplicationArguments args) throws Exception {
    log.info("Count: {}", coffeeService.findAllCoffee().size());
    for (int i = 0; i < 10; i++) {
      log.info("Reading from cache.");
      coffeeService.findAllCoffee();
    }
    coffeeService.reloadCoffee();
    log.info("Reading after refresh.");
    coffeeService.findAllCoffee().forEach(
        c -> log.info("Coffee {}", c.getName()));
  }

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

注释:

  1. 第一次读取数据来自数据库
  2. 接下来10次从缓存中读取
  3. 清除缓存
  4. 最后一次读取来自数据库

log 记录

Hibernate: 
    select
        coffee0_.id as id1_0_,
        coffee0_.create_time as create_t2_0_,
        coffee0_.update_time as update_t3_0_,
        coffee0_.name as name4_0_,
        coffee0_.price as price5_0_ 
    from
        t_coffee coffee0_
Reading from cache.
Reading from cache.
Reading from cache.
Reading from cache.
Reading from cache.
...
Reading after refresh.
Hibernate: 
    select
        ...
    from
        t_coffee coffee0_
Coffee espresso
...
Coffee macchiato

2. 通过 Docker 启动 Redis

如果已经存在 Redis的镜像,直接启动就好

  • docker start redis

如果不存在 Redis的镜像,请参考上一篇
第廿五篇:SpringBoot访问Docker中的Redis

2.工程建立

下载已经创建好的Starbucks项目
重命名根文件夹名字的CacheWithRedisDemo,用Idea打开工程,运行后实际为JPA操作数据。
接下来就改造为Redis操作Redis数据, 已经从Redis缓存中读取数据。

pom.xml增加, redis, cache配置


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

1.application配置

src > main > resources > application.yml

spring:
  jpa:
    hibernate:
      ddl-auto: none
    properties:
      hibernate:
        show_sql: true
        format_sql: true
  cache:
    type: redis
    cache-names: "coffee"
    redis:
      time-to-live: 5000
      cache-null-values: false
  redis:
    host: localhost
management:
  endpoints:
    web:
      exposure:
        include: "*"
  

解析:

  1. cache.type配置缓存为redis
  2. cache.names 为 coffee
  3. cache.redis.time-to-lieve 缓存过期时间为5秒
  4. cache.redis.cache-null-values 不缓存空数据
  5. redis.hos 为 localhost
  6. management.endpoints.web.exposure.include 为监控actuator的参数,表示可以看到的数据节点为info, health

2. Service config cache

zgpeace.spring.starbucks.service.CoffeeService

package zgpeace.spring.starbucks.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.ExampleMatcher;
import org.springframework.stereotype.Service;
import zgpeace.spring.starbucks.model.Coffee;
import zgpeace.spring.starbucks.repository.CoffeeRepository;

import java.util.List;
import java.util.Optional;

import static org.springframework.data.domain.ExampleMatcher.GenericPropertyMatchers.exact;

@Slf4j
@Service
@CacheConfig(cacheNames = "coffee")
public class CoffeeService {
  @Autowired
  private CoffeeRepository coffeeRepository;

  @Cacheable
  public List<Coffee> findAllCoffee() {
    return coffeeRepository.findAll();
  }

  @CacheEvict
  public void reloadCoffee() {

  }
}

Controller Redis CRUD

zgpeace.spring.starbucks.StarbucksApplication

package zgpeace.spring.starbucks;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import zgpeace.spring.starbucks.model.Coffee;
import zgpeace.spring.starbucks.model.CoffeeOrder;
import zgpeace.spring.starbucks.model.OrderState;
import zgpeace.spring.starbucks.repository.CoffeeRepository;
import zgpeace.spring.starbucks.service.CoffeeOrderService;
import zgpeace.spring.starbucks.service.CoffeeService;

import java.util.Optional;

@Slf4j
@EnableTransactionManagement
@EnableJpaRepositories
@SpringBootApplication
@EnableCaching(proxyTargetClass = true)
public class StarbucksApplication implements ApplicationRunner {
  @Autowired
  private CoffeeService coffeeService;

  @Override
  public void run(ApplicationArguments args) throws Exception {
    log.info("Count: {}", coffeeService.findAllCoffee().size());
    for (int i = 0; i < 5; i++) {
      log.info("Reading from cache.");
      coffeeService.findAllCoffee();
    }
    Thread.sleep(5_000);
    log.info("Reading after refresh.");
    coffeeService.findAllCoffee().forEach(c -> log.info("Coffee {}", c.getName()));
  }

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

运行数据入库和查询日志如下:

HHH000397: Using ASTQueryTranslatorFactory
Hibernate: 
    select
        coffee0_.id as id1_0_,
        coffee0_.create_time as create_t2_0_,
        coffee0_.update_time as update_t3_0_,
        coffee0_.name as name4_0_,
        coffee0_.price as price5_0_ 
    from
        t_coffee coffee0_
Count: 5
Reading from cache.
Reading from cache.
...
Reading after refresh.
Hibernate: 
    select
        coffee0_.id as id1_0_,
        coffee0_.create_time as create_t2_0_,
        coffee0_.update_time as update_t3_0_,
        coffee0_.name as name4_0_,
        coffee0_.price as price5_0_ 
    from
        t_coffee coffee0_
Coffee espresso
Coffee latte
...

注释: 程序运行必要条件,必须启动Docker > 加载Redis镜像到容器(docker start redis)。

  1. 第一次从Redis中读取数据
  2. 接下来5次从cache中读取
  3. sleep 5秒钟,因为Redis设置cache有效时间为5s
  4. cache失效,从Redis中读取数据

基本原理

和 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() 方法等,都是可以轻松做到的。

总结

恭喜你,学会了Spring Cache,Redis Cache操作数据。
代码下载:

https://github.com/zgpeace/Spring-Boot2.1/tree/master/Nosql/JedisDemo

参考

https://www.ibm.com/developerworks/cn/opensource/os-cn-spring-cache/

https://www.javazhiyin.com/4618.html

https://github.com/zgpeace/Spring-Boot2.1/tree/master/db/DemoDBStarbucks

https://blog.wuwii.com/springboot-actuator.html

https://github.com/geektime-geekbang/geektime-spring-family/tree/master/Chapter%204/jedis-demo

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值