为什么要用缓存
我们一定听说过"缓存无敌"的话,特别是在大型互联网公司,"查多写少"的场景屡见不鲜。网络上查到的很多诸如系统吞吐量提升50%、接口耗时降低80%、一个分钟级别的程序优化到毫秒级别等,多多少少和缓存有关。
举个例子:在我们程序中,很多配置数据(例如一个商品信息、一个白名单、一个第三方客户的回调接口),这些数据存在我们的DB上,数据量比较少,但是程序访问很频繁,这种情况下,将数据放一份到我们的内存缓存中将大大提升我们系统的访问效率,因为减少了数据库访问,有可能减少了数据库建连时间、网络数据传输时间、数据库磁盘寻址时间……
总的来说,下面这些场景都可以考虑使用缓存优化性能:1、查数据库2、读取文件3、网络访问,特别是调用第三方服务查询接口
Spring Cache介绍
Spring Cache 是Spring 提供的一整套的缓存解决方案,它不是具体的缓存实现,它只提供一整套的接口和代码规范、配置、注解等,用于整合各种缓存方案,比如Caffeine、Guava Cache、Ehcache。
如果我们没有使用Spring Cache,而是直接使用Guava Cache,我们的代码可能得像下面这么写:
方便复制,我把源码贴出来:
import com.alibaba.fastjson.JSON;
import com.google.common.base.Stopwatch;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.tin.example.library.BookEntity;
import com.tin.example.library.BookService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
/**
* title: CacheService
* <p>
* description:
*
* @author @【林在闪闪发光】
*/
@Service
public class MyGuavaCacheService implements InitializingBean {
private static final Logger LOGGER = LoggerFactory.getLogger(MyGuavaCacheService.class);
private static LoadingCache<String, BookEntity> CACHE;
@Autowired
private BookService bookService;
public void query() {
String bookNamePrefix = "00";
String bookNameSuffix = "号藏书";
for (int i = 0; i < 3; i++) {
String bookName = bookNamePrefix + i + bookNameSuffix;
for (int j = 0; j < 2; j++) {
queryFromCache(bookName);
}
}
//查看缓存状态
LOGGER.info("cache stats:{}", CACHE.stats().toString());
}
public void queryFromCache(String bookName) {
try {
Stopwatch stopwatch = Stopwatch.createStarted();
BookEntity bookEntity = CACHE.get(bookName);
LOGGER.info("query:{},cost:{}ms,book:{}",
bookName, stopwatch.elapsed(TimeUnit.MILLISECONDS), JSON.toJSONString(bookEntity));
} catch (Exception e) {
LOGGER.error("cache read error. bookName:{}", bookName, e);
}
}
/**
* spring容器启动时初始化guava cache
*/
@Override
public void afterPropertiesSet() throws Exception {
CACHE = CacheBuilder.newBuilder()
//并发级别=8,并发级别表示可以同时写缓存的线程数
.concurrencyLevel(8)
//设置缓存容器的初始容量为50
.initialCapacity(50)
//设置缓存最大容量为100,超过100之后就会按照LRU最近最少使用移除缓存项
.maximumSize(100)
//设置写缓存后100毫秒后过期
.expireAfterWrite(100, TimeUnit.MILLISECONDS)
//统计缓存情况,生产环境慎重使用
.recordStats()
//build方法中可以指定CacheLoader,在缓存不存在时通过CacheLoader的实现自动加载缓存
.build(new BookEntityCacheLoader());
}
/**
* 缓存不存在或者过期时触发load方法回源更新缓存
*/
public class BookEntityCacheLoader extends CacheLoader<String, BookEntity> {
@Override
public BookEntity load(String key) {
try {
return bookService.findByBookName(key);
} catch (Exception e) {
LOGGER.error("cache load error. key:{}", key);
return null;
}
}
}
}
只有一个缓存这么写看着也还行,但如果有很多缓存,每个缓存都这样写,使用起来就复杂了,和我们的业务代码严重耦合。
这个时候就用到了Spring Cache,Spring Cache并不是缓存的实现,而是缓存使用的一种方式,其基于注解和Spring高级特性提供缓存读写以及失效刷新等各种能力。
Spring Cache默认支持几个缓存实现,如下图jar包(spring-context-support 5.3.14版本)所示:
这三个包只是Spring的support(类似于适配器),真正的缓存实现是需要手动依赖jar的,后文我会举例讲到。
EhCache:纯Java进程内缓存框架,也是Hibernate、MyBatis默认的缓存提供。
Caffeine:使用Java8对Guava缓存的重写版本,从Spring5开始,Spring默认删除了Guava而使用Caffeine,支持多种缓存过期策略。
jcache:实现了JSR107规范的三方缓存都可以通过此包得到适配。
Spring Cache使用入门
Spring Cache依赖Spring的天然优势——AOP,我们只需要显式地在代码中调用第三方接口,在方法上加上注解,就可以实现把获取到的结果后把结果插入缓存内,在下一次查询的时候优先从缓存中读取数据。
使用Spring Cache也比较简单,简单总结就是3步:加依赖,开启缓存、加注解。
一、加依赖
maven:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency>
二、开启缓存
需要在启动类加上@EnableCaching注解才能启动使用Spring Cache,比如:
import com.tin.example.service.MyGuavaCacheService;
import com.tin.example.service.SpringCacheService;
import com.tin.example.util.SpringContextUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
/**
* title: Application
* <p>
* description:
*
* @author tin 林在闪闪发光
*/
@SpringBootApplication
@EnableCaching
public class Application {
private static final Logger LOGGER = LoggerFactory.getLogger(Application.class);
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
LOGGER.info("容器启动成功... ");
SpringCacheService springCacheService = SpringContextUtil.getBean(SpringCacheService.class);
springCacheService.query();
// MyGuavaCacheService myGuavaCacheService = SpringContextUtil.getBean(MyGuavaCacheService.class);
// myGuavaCacheService.query();
}
}
三、加注解
在需要缓存返回结果的方法上加上注解@Cacheable即可,比如:
我们做个测试,假设查询一本书籍耗时100毫秒
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* title: LibraryService
* <p>
* description:
*
* @author tin 林在闪闪发光
*/
@Service
public class BookService extends AbstractLibraryService {
@Autowired
private BookStore bookStore;
@Override
public BookEntity findByBookName(String bookName) {
//模拟查询耗时100毫秒
sleep4Millis(100);
if (!bookStore.hasBook()) {
return null;
}
List<BookEntity> allBook = bookStore.getBookStore();
for (BookEntity bookEntity : allBook) {
if (bookEntity == null) {
continue;
}
if (bookEntity.getBookName() != null && bookEntity.getBookName().contains(bookName)) {
return bookEntity;
}
}
return null;
}
@Cacheable("library")
@Override
public BookEntity findByBookNameWithSpringCache(String bookName) {
return findByBookName(bookName);
}
}
加上Spirng Cache缓存后,可以明显地发现第二次查询同一本书耗时0ms
这说明缓存已经生效了。
接入Caffeine缓存实现框架
上文讲到了Spring Cache支持三种缓存实现,在使用我们上面所说"三步"引入使用Spring Cache,我们究竟使用的是哪种缓存实现呢?
通过CacheManager打印看一下:
ConcurrentMapCache是Spring 内置默认的缓存实现。如果需要使用CaffeineCache,需要额外引入CaffeineCache包,同时生成一个CaffeineCacheManager的bean。
maven依赖:
CaffeineCacheManager生成:
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
/**
* title: CaffeineCacheConfig
* <p>
* description:
*
* @author tin @林在闪闪发光
*/
@Configuration
public class CaffeineCacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.initialCapacity(100)
.maximumSize(10000))
;
return cacheManager;
}
}
常用注解
Spring Cache比较常用的几个注解:@Cacheable、 @CacheConfig、@CacheEvict、@CachePut、@Caching、@EnableCaching。spring-context依赖包下也能看到注解的定义。
除了CacheConfig只能用于类上,其余的都可以用在类或者方法上,用在方法上好理解,缓存方法结果,如果用在类上,就相当于对该类的所有可以缓存的方法(需要是public方法)加上注解。
@Cacheable
@Cacheble注解表示这个方法的结果可以被缓存,调用该方法前,会先检查对应的缓存key在缓存中是否已经有值,如果有就直接返回,不调用方法,如果没有,就会调用方法,同时把结果缓存起来。
@CacheConfig
有些配置可能又是一个类通用的,这种情况就可以使用@CacheConfig了,它是一个类级别的注解,可以在类级别上配置cacheNames、keyGenerator、cacheManager、cacheResolver等。
@CachePut
@CachePut注解修饰的方法,会把方法的返回值put到缓存里面缓存起来,它只是触发put的动作,和@Cacheable不同,不会读取缓存,put到缓存的值进程内其他场景的使用者就可以使用了。
@CacheEvict
@CacheEvict注解修饰的方法,会触发缓存的evict操作,清空缓存中指定key的值。
@Caching
@Caching能够支持多个缓存注解生效。
因为Java方法上相同类型注解只能有一个有效,在我们有些场景下需要多个注解操作,特别是CacheEvict删除缓存,我们可能需要同时删除多份缓存值,这个后@Ocaching就有用途了。
我是林,一个在努力让自己变得更优秀的普通人。自己阅历有限、学识浅薄,如有发现文章不妥之处,非常欢迎向我提出,我一定细心推敲加以修改。
坚持创作不容易,你的反馈是我坚持输出的最强大动力,谢谢!