Spring Boot默认缓存管理
Spring框架支持透明地向应用程序添加缓存,以及对缓存进行管理,其管理缓存的核心是将缓存应用于操作数据的方法,从而减少操作数据的执行次数,同时不会对程序本身造成任何干扰。Spring Boot继承了Spring框架的缓存管理功能,下面将对Spring Boot内置的缓存方案进行讲解。
Spring的缓存机制将提供的缓存作用于Java 方法上,基于缓存中的可用信息,可以减少方法的执行次数。每次目标方法调用时,抽象使用缓存行为来检查执行方法,即检查执行方法是否给定了缓存的执行参数,如果是,则返回缓存结果,不执行具体方法;如果否,则执行方法,并将结果缓存后,返回给用户。
Spring的默认的缓存方案通过org.springframework.cache.Cache和org.springframework.cache.CacheManager接口来统一不同的缓存技术。
Cache接口:缓存的组件定义规范,包含缓存的各种操作集合。Spring中为Cache接口提供了各种缓存的实现:RedisCache,EhCache,ConcurrentMapCache等
CacheManager接口:缓存管理器,基于缓存名称对缓存进行管理,并制定了管理Cache的规则。
在项目中添加某个缓存管理组件(如Redis)后,Spring Boot项目会选择并启用对应的缓存管理器。如果项目中同时添加了多个缓存组件,且没有定义类型为CacheManager的Bean组件或者名为cacheResolver的缓存解析器,Spring Boot将尝试按以下列表的顺序查找有效的缓存组件进行缓存管理。 (1)Generic (2)JCache (EhCache 3、Hazelcast、Infinispan等) (3)EhCache 2.x (4)Hazelcast (5)Infinispan (6)Couchbase (7)Redis (8)Caffeine (9)Simple
声明式缓存注解
要想使用Spring提供的默认缓存,需要对缓存进行声明,也就是标志缓存的方法及缓存策略。对于缓存声明,Spring提供了一系列的注解,使用这些注解可以实现Spring 默认的基于注解的缓存管理。
1.@EnableCaching注解
@EnableCaching是Spring框架提供的用于开启基于注解的缓存支持的注解,当配置类上使用@EnableCaching注解,会默认提供CacheManager的实现,并通过AOP将缓存行为添加到应用程序。执行操作时,会检查是否已经存在注解对应的缓存。如果找到了,就会自动创建一个代理拦截方法调用,使用缓存的Bean执行处理。
2.@Cacheable注解
@Cacheable注解用于标注可缓存的方法,通常标注的方法为数据查询方法。标注@Cacheable注解的方法在执行时,会先查询缓存,如果查询到的缓存为空,则执行该方法,并将方法的执行结果添加到缓存;如果查询到缓存数据,则不执行该方法,而是直接使用缓存数据。
@Cacheable注解提供了多个属性,用于对缓存进行相关配置。
属性名 | 说明 |
value/cacheNames | 指定缓存的名称,必备属性,这两个属性二选一使用 |
key | 指定缓存数据的key,默认使用方法参数值,可以使用SpEL表达式 |
keyGenerator | 指定缓存数据的key的生成器,与key属性二选一使用 |
cacheManager | 指定缓存管理器 |
cacheResolver | 指定缓存解析器,与cacheManager属性二选一使用 |
condition | 指定在符合某条件下进行数据缓存 |
unless | 指定在符合某条件下不进行数据缓存 |
sync | 指定是否使用异步缓存,默认为false |
(1)value/cacheNames属性
value和cacheNames属性作用相同,用于指定缓存的名称,方法的返回结果会存放在指定名称的缓存中。这两个属于必备选项,且要二选一使用。如果@Cacheable注解只配置value或者cacheNames属性,那么属性名可以省略。
@Cacheable("book")
public Book findById(Integer id){return bookDao.findById(id).get();
}
@Cacheable注解中可以指定多个缓存的名称,以便使用多个缓存。
@Cacheable({"book","hotBook"})
public Book findById(Integer id){return bookDao.findById(id).get();
}
(2)key属性
缓存的本质是键值对存储,key用于指定唯一的标识,value用于指定缓存的数据,所以每次调用缓存方法都会转换为访问缓存的键。缓存的键通过key属性进行指定,进行数据缓存时,如果没有指定key属性,Spring Boot默认配置类SimpleKeyGenerator中的generateKey(Object... params)方法会根据方法参数生成key值。对于没有参数的方法,其key是默认创建的空参SimpleKey[]对象;对于只有一个参数的方法,其key默认是参数值;对于有多个参数的方法,其key是包含所有参数的SimpleKey对象。
如果方法有多个参数,但是部分参数对缓存没有任何用处,通常会选择手动指定key属性的值,key属性的值可以通过SpEL表达式选择所需要的参数。
@Cacheable(cacheNames="book", key="#id")
public Book findBookById(Integer id, boolean includeUsed){return bookDao.findById(id).get();
}
Cache缓存支持的SpEL表达式及说明
参数名 | 位置 | 描述 | 示例 |
methodName | root对象 | 当前被调用的方法名 | #root.methodName |
method | root对象 | 当前被调用的方法 | #root.method.name |
target | root对象 | 当前被调用的目标对象实例 | #root.target |
targetClass | root对象 | 当前被调用的目标对象的类 | #root.targetClass |
args | root对象 | 当前被调用的方法的参数列表 | #root.args[0] |
caches | root对象 | 当前被调用的方法的缓存列表 | #root.caches[0].name |
argumentName | 执行上下文 | 当前被调用的方法参数,可以用#参数名或者#a0、#p0的形式(0代表参数索引,从0开始) | #comment_id、#a0、#p0 |
result | 执行上下文 | 当前方法执行后的返回结果 | #result |
(3)keyGenerator属性
keyGenerator属性与key属性本质作用相同,都是用于指定缓存数据的key,只不过keyGenerator属性指定的不是具体的key值,而是key值的生成器规则,由其中指定的生成器生成具体的key。使用时,keyGenerator属性与key属性要二者选一。关于自定义key值生成器的定义,可以参考Spring Boot默认配置类SimpleKeyGenerator的定义方式,这里不再做具体说明。
(4)cacheManager/cacheResolver属性
cacheManager和cacheResolver属性分别用于指定缓存管理器和缓存解析器,这两个属性也是二选一使用,默认情况不需要配置,对于需要使用多个缓存管理器(如Redis、Ehcache等)的应用,可以为每个操作设置一个缓存管理器或缓存解析器。
(5)condition属性
condition属性用于对数据进行有条件的选择性存储,只有当指定条件为true时才会对查询结果进行缓存,可以使用SpEL表达式指定属性值。
@Cacheable(cacheNames="book", condition="#id > 1")
public Book findBook(Integer id){return bookDao.findById(id).get();
}
(6)unless属性
unless属性的作用与condition属性相反,当指定的条件为true时,方法的返回值不会被缓存,也可以使用SpEL表达式指定。
@Cacheable(cacheNames="book", unless = "#result==null")
public Book findBook(Integer id){return bookDao.findById(id).get();
}
(7)sync属性
在多线程程序中,某些操作可能会同时引用相同的参数,导致相同的对象被计算好几次,从而达不到缓存的目的。对于这种情况,可以使用sync属性,sync属性表示数据缓存过程中是否使用同步模式,默认值为false,通常不会使用该属性。
3.@CachePut注解
@CachePut注解的作用是更新缓存数据,当需要更新缓存且不影响方法执行时,可以使用@CachePut注解,通常用在数据更新方法上。@CachePut注解的执行顺序是,先进行方法调用,然后将方法结果更新到缓存中。
@CachePut注解也提供了多个属性,这些属性与@Cacheable注解的属性完全相同。通常不建议在同一个方法同时使用@CachePut和@Cacheable注解,这两个注解关注不同的行为,@CachePut注解会强制执行方法并进行缓存更新,使用@Cacheable 注解时,如果请求能够在缓存中获取到对应的数据,就不会执行当前被@Cacheable 注解标注的方法。
4.@CacheEvict注解
@CacheEvict注解的作用删除缓存中的数据,通常标注在数据删除方法上。@CacheEvict注解的默认执行顺序是先进行方法调用,然后将缓存清除。 @CacheEvict注解也提供了多个属性,这些属性与@Cacheable注解的属性基本相同,除此之外,还额外提供了两个特殊属性allEntries和beforeInvocation,下面对这两个属性分别进行讲解。
(1)allEntries属性 allEntries属性表示是否清除指定缓存空间中的所有缓存数据,默认值为false,即默认只删除指定key对应的缓存数据。
@CacheEvict(cacheNames = "book",allEntries = true)
public void delById(Integer id){bookDao.deleteById(id);
}
(2)beforeInvocation属性 beforeInvocation属性表示是否在方法执行之前进行缓存清除,默认值为false,即默认在执行方法后再进行缓存清除。
@CacheEvict(cacheNames = "book",beforeInvocation = true)
public void delById(Integer id){bookDao.deleteById(id);
}
5.@Caching注解
如果不同缓存之间的条件或者键表达式不同,就需要指定相同类型的多个注解,例如需要同时指定多个@CacheEvict或@CachePut,这个时候可以使用@Caching注解。@Caching注解用于针对复杂规则的数据缓存管理,@Caching注解中允许使用多个嵌套的 @Cacheable 、@CachePut 或 @CacheEvict。在@Caching注解内部包含有Cacheable、put和evict三个属性,分别对应于@Cacheable、@CachePut和@CacheEvict三个注解。
@Caching(evict = { @CacheEvict("primary"),
@CacheEvict(cacheNames="secondary", key="#date")})
public void delById(Integer id, Date date){bookDao.deleteById(id);
}
@Caching(cacheable={@Cacheable(cacheNames ="comment",key = "#id")},put = {@CachePut(cacheNames = "comment",key = "#result.author")})public Comment getComment(int comment_id){return commentRepository.findById(comment_id).get();}
6.@CacheConfig注解
@CacheConfig注解使用在类上,主要用于统筹管理类中所有使用@Cacheable、@CachePut和@CacheEvict注解标注方法中的公共属性。
@CacheConfig(cacheNames = "book")
@Service
public class BookService {@Autowiredprivate BookRepository bookRepository;@Cacheablepublic Book findById(Integer id){return bookRepository.findById(id).get();}
}
声明式缓存注解的应用
1.创建项目
2.配置依赖
<!-- springboot默认应该有,不配应该没关系-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>
org.springframework.boot
</groupId>
<artifactId>
spring-boot-starter-web
</artifactId>
</dependency>
<dependency>
<groupId>
org.springframework.boot
</groupId>
<artifactId>
spring-boot-starter-data-jpa
</artifactId>
</dependency>
<dependency>
<groupId>
mysql
</groupId>
<artifactId>
mysql-connector-java
</artifactId>
</dependency>
3.设置配置信息
spring:datasource:url:"jdbc:mysql://localhost:3306/springbootdata?characterEncoding=utf-8&serverTimezone=Asia/Shanghai"username:rootpassword:rootjpa:show-sql:true
4.创建实体类
@Entity
@Table(name="book")
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id; //图书编号
@Column(name="name")
private String name; //图书名称
private String author; //图书作者
private String press; //图书出版社
private String status; //图书状态public Integer getId() {
return id;
}public void setId(Integer id) {
this .id = id;
}public String getName() {
return name;
}public void setName(String name) {
this .name = name;
}public String getPress() {
return press;
}public void setPress(String press) {
this .press = press;
}public String getAuthor() {
return author;
}public void setAuthor(String author) {
this .author = author;
}public String getStatus() {
return status;
}public void setStatus(String status) {
this .status = status;
}public Book() {
}public Book(Integer id, String name, String author, String press, String status) {
this .id = id;
this .name = name;
this .author = author;
this .press = press;
this .status = status;
}@Override
public String toString() {
return "Book{" +
"id=" + id +
", name='" + name + ''' +
", author='" + author + ''' +
", press='" + press + ''' +
", status='" + status + ''' +
'
}';
}
}
5.创建Repository接口
@Repository
public interface BookRepository extends JpaRepository<Book,Integer> {
}
6.创建Service接口和实现类
public interface BookService {
public Book findById(Integer id);
public Book updateById(Integer id,String name);
public void delById(Integer id);
}
@Service
@CacheConfig(cacheNames = "book")
@Transactional
public class BookServiceImpl implements BookService{
@Autowired
private BookRepository bookRepository;@Cacheable(key = "#id")
public Book findById(Integer id){
//根据id查找图书信息
return bookRepository.findById(id).get();
}
@CachePut(key = "#id")
public Book updateById(Integer id,String name){
Book book=this .findById(id);
book.setName(name);
//更新图书信息
return bookRepository.save(book);
}
@CacheEvict(key = "#id")
public void delById(Integer id){
//根据id删除图书信息
bookRepository.deleteById(id);
}
}
7.创建控制器类
@RestController
@RequestMapping("book")
public class BookController {
@Autowired
private BookService bookService;@RequestMapping("/findById/{id}")
public Book findById(@PathVariable Integer id){
//根据id查询图书信息
return bookService.findById(id);
}
@RequestMapping("/editById/{id}/{name}")
public Book editById(@PathVariable Integer id,@PathVariable String name){
//根据id修改图书的名称
return bookService.updateById(id,name);
}
@RequestMapping("/delById/{id}")
public void delById(@PathVariable Integer id){
//根据id删除图书信息
bookService.delById(id);
}
}
8.在启动类上开启缓存
@SpringBootApplication
@EnableCaching
public class Chapter06Application {
public static void main(String[] args) {
SpringApplication.run(Chapter06Application.class , args);
}
}
9.测试缓存效果
启动项目,在浏览器中访问http://localhost:8080/book/findById/3,查询图书信息,控制台输出信息。
查询图书信息后,浏览器中查询到图书信息。
再次在浏览器中访问http://localhost:8080/book/findById/3,查询id为3的图书信息,控制台输出信息。
在浏览器中访问http://localhost:8080/book/editById/3/西游释厄传,将id为3的图书名称更新为“西游释厄传”,此时控制台输出信息。
更新图书信息后,浏览器中查询图书信息。
浏览器中访问http://localhost:8080/book/delById/3,删除id为3的图书信息,控制台输出信息。
在浏览器中再次访问http://localhost:8080/book/findById/3,查询id为3的图书信息,控制台输出信息。
动手试一试 Spring Boot默认缓存管理
测试发生一万次请求需要的时间(jdk11以上)
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.IntStream;public class HttpRequestBenchmark {public static void main(String[] args) throws Exception {String url = "http://localhost:8081/get/1"; // 替换为你要请求的URLint numRequests = 10000; // 请求次数HttpClient client = HttpClient.newHttpClient();HttpRequest request = HttpRequest.newBuilder().uri(new URI(url)).build();long startTime = System.nanoTime();// 使用CompletableFuture来异步发送请求,并等待所有请求完成CompletableFuture<Void>[] futures = IntStream.range(0, numRequests).mapToObj(i -> CompletableFuture.runAsync(() -> {try {HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());// 在这里可以处理响应,例如检查状态码等System.out.println("xiangying------------------");// 这里只是简单发送请求并忽略响应内容} catch (Exception e) {e.printStackTrace();}})).toArray(CompletableFuture[]::new);// 等待所有请求完成CompletableFuture.allOf(futures).join();long endTime = System.nanoTime();Duration duration = Duration.ofNanos(endTime - startTime);System.out.println("发送" + numRequests + "次HTTP请求需要的时间:" + duration.toMillis() + "毫秒");}