spring cache
简介
缓存有诸多好处,但网上一搜就发现缓存框架太多了,各有各的优势,比如redis,Memcached,guava
如果我们的程序想要使用缓存,就要与这些框架耦合,.在合适的时候,在代码中调用与缓存有关的接口方法,在合适的时候插入到数据库里,在合适的时候从缓存中读取数据
想一想AOP的场景,这不就是AOP该去做的吗
springCache就是这一个框架,利用了AOP,实现了基于注解的缓存功能,并且进行了合理的抽象,业务代码不用关心底层使用了什么框架,只需要简单加一个注解,就能实现缓存功能了.而且spring cache也提供了很多默认的配置,用户可以三秒钟就使用上一个很不错的缓存功能
步骤
1 加依赖
gradle:
implementation 'org.springframework.boot:spring-boot-starter-cache'
maven:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
2 开启缓存
在启动类加上@EnableCaching
注解即可开启使用缓存。
@SpringBootApplication
@EnableCaching
public class CachingApplication {
public static void main(String[] args) {
SpringApplication.run(CachingApplication.class, args);
}
}
3 加缓存注解
在要缓存的方法上面添加@Cacheable
注解,即可缓存这个方法的返回值。
在要缓存的方法上面添加@Cacheable
注解,即可缓存这个方法的返回值。
@Override
@Cacheable("books")
public Book getByIsbn(String isbn) {
simulateSlowService();
return new Book(isbn, "Some book");
}
// Don't do this at home
private void simulateSlowService() {
try {
long time = 3000L;
Thread.sleep(time);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}
测试
@Override
public void run(String... args) {
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"));
}
测试一下,可以发现。第一次和第二次(第二次参数和第一次不同)调用getByIsbn
方法,会等待3秒,而后面四个调用,都会立即返回。
常用注解
SpringCache有几个常用注解分别为@Cacheable,@CachePut,@CacheEvict,@Caching,@CacheConfig
除了最后一个CacheConfig外,其余四个都可以用在类上或者方法级别上,如果用在类上,就是对盖类的所有public方法生效
@Cacheable
@Cacheble注解表示这个方法有了缓存功能,方法的返回值会被缓存下来,下一次调用该方法钱,会去检查缓存中是否有值,如果有就直接返回,不调用方法.如果没有,就调用方法,然后把结果缓存起来.这个注解一般用在查询方法上
@CachePut
加了@CachePut注解的方法,会把方法的返回值put到缓存里面缓存起来,供其他地方使用.它通常用在新增方法上
@CacheEvict
使用了CacheEvict注解的方法,会清空指定缓存.一般用在更新或者删除的方法上
@Caching
java的注解机制决定了一个方法上只能有一个相同的注解生效.但有时候可能一个方法会操作多个缓存(这个在删除缓存操作中比较常见,在添加操作中不太常见)
@caching就是用来解决这类情况的,大家一看它的源码就明白了
public @interface Caching {
Cacheable[] cacheable() default {};
CachePut[] put() default {};
CacheEvict[] evict() default {};
}
@CacheConfig
前面提到的四个注解,都是Spring Cache常用的注解。每个注解都有很多可以配置的属性,这个我们在下一节再详细解释。但这几个注解通常都是作用在方法上的,而有些配置可能又是一个类通用的,这种情况就可以使用@CacheConfig
了,它是一个类级别的注解,可以在类级别上配置cacheNames、keyGenerator、cacheManager、cacheResolver等。
使用其他缓存框架
默认数据是存储在concurrentHashMap中的如果要使用其他的缓存框架该怎么做呢
redis
1). 导入Spring Cache和Redis相关maven坐标
2). 在application.yml中配置缓存数据的过期时间
3). 在启动类上加入@EnableCaching注解,开启缓存注解功能
4). 在相应的查询(读)方法上加入@Cacheable注解
5). 在对数据库内容有改变(写)的方法上加入CacheEvict注解
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
备注: spring-boot-starter-data-redis 这个依赖前面已经引入了, 无需再次引入。
application.yml中设置缓存过期时间
spring:
cache:
redis:
time-to-live: 1800000 #设置缓存数据的过期时间
启动类上加入@EnableCaching注解
缓存带来的问题
双写不一致
使用缓存会带来许多问题,尤其是高并发下,缓存穿透,缓存击穿,缓存雪崩,双写不一致等问题.
其中主要聊一下双鞋不一致的问题这是一个比较常见的问题,其中一个常用的解决方案是,更新的时候,先删缓存,再更新数据库,所以@CacheEvict会有一个boforeInvocation的配置.
但使用缓存会存在缓存中的数据和数据库中不一致的情况,尤其是调用第三发接口,你不会知道它什么时候更新了数据.但使用的业务场景很多时候并不要求数据的强一致,比如首页的热点文章,我们可以让缓存一分钟失效,这样就算一分钟内,不是最新的热点排行也没关系
缓存击穿
缓存击穿是指当缓存中某个热点数据过期了,在该热点数据重新载入缓存之前,有大量的查询请求穿过缓存,直接查询数据库。这种情况会导致数据库压力瞬间骤增,造成大量请求阻塞,甚至直接挂掉。
解决缓存击穿的方法也有两种,第一种是设置key永不过期;第二种是使用分布式锁,保证同一时刻只能有一个查询请求重新加载热点数据到缓存中,这样,其他的线程只需等待该线程运行完毕,即可重新从Redis中获取数据。
第一种方式比较简单,在设置热点key的时候,不给key设置过期时间即可。不过还有另外一种方式也可以达到key不过期的目的,就是正常给key设置过期时间,不过在后台同时启一个定时任务去定时地更新这个缓存。
额外占用内存
这个是无可避免的。因为总要有一个地方去放缓存。不管是ConcurrentHashMap也好,Redis也好,Caffeine也好,总归是会占用额外的内存资源去放缓存的。但缓存的思想正是用空间去换时间,有时候占用这点额外的空间对于时间上的优化来说,是非常值得的。
这里需要注意的是,SpringCache默认使用的是ConcurrentHashMap,它不会自动回收key,所以如果使用默认的这个缓存,程序就会越来越大,并且得不到回收。最终可能导致OOM。