為什么使用緩存
前幾天我在文章《我是如何把一個(gè)15分鐘的程序優(yōu)化到了10秒的》中,提到了一些在代碼層面優(yōu)化性能的方法。其中第一個(gè)就是使用緩存。
使用緩存是一個(gè)很“高性價(jià)比”的性能優(yōu)化方式,尤其是對(duì)于有大量重復(fù)查詢的程序來(lái)說(shuō)。通常來(lái)說(shuō),在WEB后端應(yīng)用程序來(lái)說(shuō),耗時(shí)比較大的往往有兩個(gè)地方:一個(gè)是查數(shù)據(jù)庫(kù),一個(gè)是調(diào)用其它服務(wù)的API(因?yàn)槠渌?wù)最終也要去做查數(shù)據(jù)庫(kù)等耗時(shí)操作)。
重復(fù)查詢也有兩種。一種是我們?cè)趹?yīng)用程序中代碼寫得不好,寫的for循環(huán),可能每次循環(huán)都用重復(fù)的參數(shù)去查詢了。這種情況,比較聰明一點(diǎn)的程序員都會(huì)對(duì)這段代碼進(jìn)行重構(gòu),用Map來(lái)把查出來(lái)的東西暫時(shí)放在內(nèi)存里,后續(xù)去查詢之前先看看Map里面有沒(méi)有,沒(méi)有再去查數(shù)據(jù)庫(kù),其實(shí)這就是一種緩存的思想。
另一種重復(fù)查詢是大量的相同或相似請(qǐng)求造成的。比如資訊網(wǎng)站首頁(yè)的文章列表、電商網(wǎng)站首頁(yè)的商品列表、微博等社交媒體熱搜的文章等等,當(dāng)大量的用戶都去請(qǐng)求同樣的接口,同樣的數(shù)據(jù),如果每次都去查數(shù)據(jù)庫(kù),那對(duì)數(shù)據(jù)庫(kù)來(lái)說(shuō)是一個(gè)不可承受的壓力。所以我們通常會(huì)把高頻的查詢進(jìn)行緩存,我們稱它為“熱點(diǎn)”。
為什么使用Spring Cache
前面提到了緩存有諸多的好處,于是大家就摩拳擦掌準(zhǔn)備給自己的應(yīng)用加上緩存的功能。但是網(wǎng)上一搜卻發(fā)現(xiàn)緩存的框架太多了,各有各的優(yōu)勢(shì),比如Redis、Memcached、Guava、Caffeine等等。
如果我們的程序想要使用緩存,就要與這些框架耦合。聰明的架構(gòu)師已經(jīng)在利用接口來(lái)降低耦合了,利用面向?qū)ο蟮某橄蠛投鄳B(tài)的特性,做到業(yè)務(wù)代碼與具體的框架分離。
但我們?nèi)匀恍枰@式地在代碼中去調(diào)用與緩存有關(guān)的接口和方法,在合適的時(shí)候插入數(shù)據(jù)到緩存里,在合適的時(shí)候從緩存中讀取數(shù)據(jù)。
想一想「AOP」的適用場(chǎng)景,這不就是天生就應(yīng)該AOP去做的嗎?
?
關(guān)于Spring AOP,可以參考我之前的文章《關(guān)于Spring AOP的靈魂十問(wèn)》
?
是的,Spring Cache就是一個(gè)這個(gè)框架。它利用了AOP,實(shí)現(xiàn)了基于注解的緩存功能,并且進(jìn)行了合理的抽象,業(yè)務(wù)代碼不用關(guān)心底層是使用了什么緩存框架,只需要簡(jiǎn)單地加一個(gè)注解,就能實(shí)現(xiàn)緩存功能了。而且Spring Cache也提供了很多默認(rèn)的配置,用戶可以3秒鐘就使用上一個(gè)很不錯(cuò)的緩存功能。
既然有這么好的輪子,干嘛不用呢?
如何使用Spring Cache
上面的3秒鐘,絕對(duì)不夸張。使用SpringCache分為很簡(jiǎn)單的三步:加依賴,開啟緩存,加緩存注解。
?
本文示例代碼使用的是官方的示例代碼,git地址:https://github.com/spring-guides/gs-caching.git
?
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 開啟緩存
在啟動(dòng)類加上@EnableCaching注解即可開啟使用緩存。
@SpringBootApplication
@EnableCaching
public class CachingApplication {
public static void main(String[] args) {
SpringApplication.run(CachingApplication.class, args);
}}
3 加緩存注解
在要緩存的方法上面添加@Cacheable注解,即可緩存這個(gè)方法的返回值。
@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);
}
}
測(cè)試
@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"));
}
測(cè)試一下,可以發(fā)現(xiàn)。第一次和第二次(第二次參數(shù)和第一次不同)調(diào)用getByIsbn方法,會(huì)等待3秒,而后面四個(gè)調(diào)用,都會(huì)立即返回。
常用注解
Spring Cache有幾個(gè)常用注解,分別為@Cacheable、@CachePut、@CacheEvict、@Caching、 @CacheConfig。除了最后一個(gè)CacheConfig外,其余四個(gè)都可以用在類上或者方法級(jí)別上,如果用在類上,就是對(duì)該類的所有public方法生效,下面分別介紹一下這幾個(gè)注解。
@Cacheable
@Cacheble注解表示這個(gè)方法有了緩存的功能,方法的返回值會(huì)被緩存下來(lái),下一次調(diào)用該方法前,會(huì)去檢查是否緩存中已經(jīng)有值,如果有就直接返回,不調(diào)用方法。如果沒(méi)有,就調(diào)用方法,然后把結(jié)果緩存起來(lái)。這個(gè)注解「一般用在查詢方法上」。
@CachePut
加了@CachePut注解的方法,會(huì)把方法的返回值put到緩存里面緩存起來(lái),供其它地方使用。它「通常用在新增方法上」。
@CacheEvict
使用了CacheEvict注解的方法,會(huì)清空指定緩存。「一般用在更新或者刪除的方法上」。
@Caching
Java注解的機(jī)制決定了,一個(gè)方法上只能有一個(gè)相同的注解生效。那有時(shí)候可能一個(gè)方法會(huì)操作多個(gè)緩存(這個(gè)在刪除緩存操作中比較常見,在添加操作中不太常見)。
Spring Cache當(dāng)然也考慮到了這種情況,@Caching注解就是用來(lái)解決這類情況的,大家一看它的源碼就明白了。
public @interface Caching {
Cacheable[] cacheable() default {};
CachePut[] put() default {};
CacheEvict[] evict() default {};
}
@CacheConfig
前面提到的四個(gè)注解,都是Spring Cache常用的注解。每個(gè)注解都有很多可以配置的屬性,這個(gè)我們?cè)谙乱还?jié)再詳細(xì)解釋。但這幾個(gè)注解通常都是作用在方法上的,而有些配置可能又是一個(gè)類通用的,這種情況就可以使用@CacheConfig了,它是一個(gè)類級(jí)別的注解,可以在類級(jí)別上配置cacheNames、keyGenerator、cacheManager、cacheResolver等。
Spring @Cacheable指定失效時(shí)間
interface CacheNames{
String CACHE_15MINS = "sssss:cache:15m";
/** 30分鐘緩存組 */
String CACHE_30MINS = "sssss:cache:30m";
/** 60分鐘緩存組 */
String CACHE_60MINS = "sssss:cache:60m";
/** 180分鐘緩存組 */
String CACHE_180MINS = "sssss:cache:180m";
}
@Component
public class RedisCacheCustomizer
implements CacheManagerCustomizer {
/** CacheManager緩存自定義初始化比較早,盡量不要@autowired 其他spring 組件 */
@Override
public void customize(RedisCacheManager cacheManager) {
// 自定義緩存名對(duì)應(yīng)的過(guò)期時(shí)間
Map expires = ImmutableMap.builder()
.put(CacheNames.CACHE_15MINS, TimeUnit.MINUTES.toSeconds(15))
.put(CacheNames.CACHE_30MINS, TimeUnit.MINUTES.toSeconds(30))
.put(CacheNames.CACHE_60MINS, TimeUnit.MINUTES.toSeconds(60))
.put(CacheNames.CACHE_180MINS, TimeUnit.MINUTES.toSeconds(180)).build();
// spring cache是根據(jù)cache name查找緩存過(guò)期時(shí)長(zhǎng)的,如果找不到,則使用默認(rèn)值
cacheManager.setDefaultExpiration(TimeUnit.MINUTES.toSeconds(30));
cacheManager.setExpires(expires);
}
}
@Cacheable(key = "key", cacheNames = CacheNames.CACHE_15MINS)
public String demo2(String key) {
return "abc" + key;
}