Spring Cache
在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ù)查詢(xún)也有兩種。一種是我們?cè)趹?yīng)用程序中代碼寫(xiě)得不好,寫(xiě)的for循環(huán),可能每次循環(huán)都用重復(fù)的參數(shù)去查詢(xún)了。這種情況,比較聰明一點(diǎn)的程序員都會(huì)對(duì)這段代碼進(jìn)行重構(gòu),用Map來(lái)把查出來(lái)的東西暫時(shí)放在內(nèi)存里,后續(xù)去查詢(xún)之前先看看Map里面有沒(méi)有,沒(méi)有再去查數(shù)據(jù)庫(kù),其實(shí)這就是一種緩存的思想。另一種重復(fù)查詢(xún)是大量的相同或相似請(qǐng)求造成的。比如資訊網(wǎng)站首頁(yè)的文章列表、電商網(wǎng)站首頁(yè)的商品列表、微博等社交媒體熱搜的文章等等,當(dāng)大量的用戶(hù)都去請(qǐng)求同樣的接口,同樣的數(shù)據(jù),如果每次都去查數(shù)據(jù)庫(kù),那對(duì)數(shù)據(jù)庫(kù)來(lái)說(shuō)是一個(gè)不可承受的壓力。所以我們通常會(huì)把高頻的查詢(xún)進(jìn)行緩存,我們稱(chēng)它為“熱點(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去做的嗎?
是的,Spring Cache就是一個(gè)這個(gè)框架。它利用了AOP,實(shí)現(xiàn)了基于注解的緩存功能,并且進(jìn)行了合理的抽象,業(yè)務(wù)代碼不用關(guān)心底層是使用了什么緩存框架,只需要簡(jiǎn)單地加一個(gè)注解,就能實(shí)現(xiàn)緩存功能了。而且Spring Cache也提供了很多默認(rèn)的配置,用戶(hù)可以3秒鐘就使用上一個(gè)很不錯(cuò)的緩存功能。
既然有這么好的輪子,干嘛不用呢?
二、如何使用Spring Cache
上面的3秒鐘,絕對(duì)不夸張。使用SpringCache分為很簡(jiǎn)單的三步:加依賴(lài),開(kāi)啟緩存,加緩存注解。
本文示例代碼使用的是官方的示例代碼,git地址:github.com/spring-guid…
gradle:
implementation 'org.springframework.boot:spring-boot-starter-cache'maven:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-cache</artifactId> </dependency>在啟動(dòng)類(lèi)加上@EnableCaching注解即可開(kāi)啟使用緩存。
@SpringBootApplication @EnableCaching public class CachingApplication {public static void main(String[] args) {SpringApplication.run(CachingApplication.class, args);} }在要緩存的方法上面添加@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è)試一下,可以發(fā)現(xiàn)。第一次和第二次(第二次參數(shù)和第一次不同)調(diào)用getByIsbn方法,會(huì)等待3秒,而后面四個(gè)調(diào)用,都會(huì)立即返回。
三、常用注解
Spring Cache有幾個(gè)常用注解,分別為@Cacheable、@CachePut、@CacheEvict、@Caching、 @CacheConfig。除了最后一個(gè)CacheConfig外,其余四個(gè)都可以用在類(lèi)上或者方法級(jí)別上,如果用在類(lèi)上,就是對(duì)該類(lèi)的所有public方法生效,下面分別介紹一下這幾個(gè)注解。
@Cacheble注解表示這個(gè)方法有了緩存的功能,方法的返回值會(huì)被緩存下來(lái),下一次調(diào)用該方法前,會(huì)去檢查是否緩存中已經(jīng)有值,如果有就直接返回,不調(diào)用方法。如果沒(méi)有,就調(diào)用方法,然后把結(jié)果緩存起來(lái)。這個(gè)注解一般用在查詢(xún)方法上。
加了@CachePut注解的方法,會(huì)把方法的返回值put到緩存里面緩存起來(lái),供其它地方使用。它通常用在新增方法上。
使用了CacheEvict注解的方法,會(huì)清空指定緩存。一般用在更新或者刪除的方法上。
Java注解的機(jī)制決定了,一個(gè)方法上只能有一個(gè)相同的注解生效。那有時(shí)候可能一個(gè)方法會(huì)操作多個(gè)緩存(這個(gè)在刪除緩存操作中比較常見(jiàn),在添加操作中不太常見(jiàn))。Spring Cache當(dāng)然也考慮到了這種情況,@Caching注解就是用來(lái)解決這類(lèi)情況的,大家一看它的源碼就明白了。
public @interface Caching {Cacheable[] cacheable() default {};CachePut[] put() default {};CacheEvict[] evict() default {}; }前面提到的四個(gè)注解,都是Spring Cache常用的注解。每個(gè)注解都有很多可以配置的屬性,這個(gè)我們?cè)谙乱还?jié)再詳細(xì)解釋。但這幾個(gè)注解通常都是作用在方法上的,而有些配置可能又是一個(gè)類(lèi)通用的,這種情況就可以使用@CacheConfig了,它是一個(gè)類(lèi)級(jí)別的注解,可以在類(lèi)級(jí)別上配置cacheNames、keyGenerator、cacheManager、cacheResolver等。
- key: key的來(lái)源可分為三類(lèi),分別是:默認(rèn)的、keyGenerator生成的、主動(dòng)指定的。
- condition:在激活注解功能前,進(jìn)行condition驗(yàn)證,如果condition結(jié)果為true,則表明驗(yàn)證通過(guò),緩存注解生效;否則緩存注解不生效。condition作用時(shí)機(jī)在:緩存注解檢查緩存中是否有對(duì)應(yīng)的key-value 之前。注:緩存注解檢查緩存中是否有對(duì)應(yīng)的key-value 在運(yùn)行目標(biāo)方法之前,所以 condition作用時(shí)機(jī)也在運(yùn)行目標(biāo)方法之前。
- cacheNames:通過(guò)cacheNames對(duì)數(shù)據(jù)進(jìn)行隔離,不同cacheName下可以有相同的key。也可稱(chēng)呼cacheName為命名空間。實(shí)際上(以spring-cache為例),可以通過(guò)設(shè)置RedisCacheConfiguration#usePrefix的true或false來(lái)控制是否使用前綴。如果否,那么最終的redis鍵就是key值;如果是,那么就會(huì)根據(jù)cacheName生成一個(gè)前綴,然后再追加上key作為最終的redis鍵.cacheName還有其它重要的功能:cacheName(就像其名稱(chēng)【命名空間】所說(shuō))實(shí)現(xiàn)了數(shù)據(jù)分區(qū)的功能,一些操作可以直接按照命名空間批量進(jìn)行。如:spring框架中的Cache實(shí)際對(duì)應(yīng)的就是一個(gè)【命名空間】,spring會(huì)先去找到數(shù)據(jù)所在的命名空間(即:先找到對(duì)應(yīng)的Cache),再由Cache結(jié)合key,最終定位到數(shù)據(jù)。
注意:若屬性cacheNames(或?qū)傩詖alue)指定了多個(gè)命名空間;當(dāng)進(jìn)行緩存存儲(chǔ)時(shí),會(huì)在這些命名空間下都存一份key-value。當(dāng)進(jìn)行緩存讀取時(shí),會(huì)按照cacheNames值里命名空間的順序,挨個(gè)挨個(gè)從命名空間中查找對(duì)應(yīng)的key,如果在某個(gè)命名空間中查找打了對(duì)應(yīng)的緩存,就不會(huì)再查找排在后面的命名空間,也不會(huì)再執(zhí)行對(duì)應(yīng)方法,直接返回緩存中的value值。
- unless:功能是:是否令注解(在方法執(zhí)行后的功能)不生效;若unless的結(jié)果為true,則(方法執(zhí)行后的功能)不生效;若unless的結(jié)果為false,則(方法執(zhí)行后的)功能生效。注:unless默認(rèn)為"",即相當(dāng)于默認(rèn)為false。unless的作用時(shí)機(jī):目標(biāo)方法運(yùn)行后。注:如果(因?yàn)橹苯訌木彺嬷蝎@取到了數(shù)據(jù),而導(dǎo)致)目標(biāo)方法沒(méi)有被執(zhí)行,那么unless字段不生效。
- allEntries:此屬性主要出現(xiàn)在@CacheEvict注解中,表示是否清除指定命名空間中的所有數(shù)據(jù),默認(rèn)為false。
- beforeInvocation:此屬性主要出現(xiàn)在@CacheEvict注解中,表示 是否在目標(biāo)方法執(zhí)行前使 此注解生效。 默認(rèn)為false,即:目標(biāo)方法執(zhí)行完畢后此注解生效。
三、常用注解的配置原理
注解使用詳細(xì)例子
這部分我們最好是結(jié)合源碼來(lái)看,才能更好地理解這些配置的運(yùn)作機(jī)制。
源碼:解析注解的時(shí)機(jī)。
這一節(jié)主要是源碼解析,有點(diǎn)晦澀,對(duì)源碼不感興趣的同學(xué)可以跳過(guò)。但如果想要弄清楚Spring Cache運(yùn)作的原理,還是推薦一看的。前面提到的幾個(gè)注解@Cacheable、@CachePut、@CacheEvict、@CacheConfig,都有一些可配置的屬性。這些配置的屬性都可以在抽象類(lèi)CacheOperation及其子類(lèi)中可以找到。它們大概是這樣的關(guān)系:
看到這里不得不佩服,這繼承用得,妙啊。
解析每個(gè)注解的代碼在SpringCacheAnnotationParser類(lèi)中可以找到,比如parseEvictAnnotation方法,里面就有這么一句:
builder.setCacheWide(cacheEvict.allEntries());明明注解里叫allEntries,但是CacheEvictOperation里卻叫cacheWide?看了下作者,都是多個(gè)作者,但第一作者都是一個(gè)叫Costin Leau的哥們,我對(duì)這個(gè)命名還是有一點(diǎn)小小的困惑。。。看來(lái)大佬們寫(xiě)代碼也會(huì)有命名不一致的問(wèn)題
那這個(gè)SpringCacheAnnotationParser是在什么時(shí)候被調(diào)用的呢?很簡(jiǎn)單,我們?cè)谶@個(gè)類(lèi)的某個(gè)方法上打個(gè)斷點(diǎn),然后debug就行了,比如parseCacheableAnnotation方法。
在debug界面,可以看到調(diào)用鏈非常長(zhǎng),前面是我們熟悉的IOC注冊(cè)Bean的一個(gè)流程,直到我們看到了一個(gè)叫做AbstractAutowireCapableBeanFactory的BeanFactory,然后這個(gè)類(lèi)在創(chuàng)建Bean的時(shí)候會(huì)去找是否有Advisor。正好Spring Cache源碼里就定義了這么一個(gè)Advisor:BeanFactoryCacheOperationSourceAdvisor。這個(gè)Advisor返回的PointCut是一個(gè)CacheOperationSourcePointcut,這個(gè)PointCut復(fù)寫(xiě)了matches方法,在里面去獲取了一個(gè)CacheOperationSource,調(diào)用它的getCacheOperations方法。這個(gè)CacheOperationSource是個(gè)接口,主要的實(shí)現(xiàn)類(lèi)是AnnotationCacheOperationSource。在findCacheOperations方法里,就會(huì)調(diào)用到我們最開(kāi)始說(shuō)的SpringCacheAnnotationParser了。
這樣就完成了基于注解的解析。
四、入口:基于AOP的攔截器
那我們實(shí)際調(diào)用方法的時(shí)候,是怎么處理的呢?我們知道,使用了AOP的Bean,會(huì)生成一個(gè)代理對(duì)象,實(shí)際調(diào)用的時(shí)候,會(huì)執(zhí)行這個(gè)代理對(duì)象的一系列的Interceptor。Spring Cache使用的是一個(gè)叫做CacheInterceptor的攔截器。我們?nèi)绻恿司彺嫦鄳?yīng)的注解,就會(huì)走到這個(gè)攔截器上。這個(gè)攔截器繼承了CacheAspectSupport類(lèi),會(huì)執(zhí)行這個(gè)類(lèi)的execute方法,這個(gè)方法就是我們要分析的核心方法了。
@Cacheable的sync
我們繼續(xù)看之前提到的execute方法,該方法首先會(huì)判斷是否是同步。這里的同步配置是用的@Cacheable的sync屬性,默認(rèn)是false。如果配置了同步的話(huà),多個(gè)線程嘗試用相同的key去緩存拿數(shù)據(jù)的時(shí)候,會(huì)是一個(gè)同步的操作。(加鎖)
我們來(lái)看看同步操作的源碼。如果判斷當(dāng)前需要同步操作
(1)、首先會(huì)去判斷當(dāng)前的condition是不是符合條件
(2)、這里的condition也是@Cacheable中定義的一個(gè)配置,它是一個(gè)EL表達(dá)式,比如我們可以這樣用來(lái)緩存id大于1的Book:
如果不符合條件,就不使用緩存,也不把結(jié)果放入緩存,直接跳到5。否則,嘗試獲取key
- (3)、在獲取key的時(shí)候,會(huì)先判斷用戶(hù)有沒(méi)有定義key,它也是一個(gè)EL表達(dá)式。如果沒(méi)有的話(huà),就用keyGenerator生成一個(gè)key:
我們可以用這種方式手動(dòng)指定根據(jù)id生成book-1,book-2這樣的key:
@Override @Cacheable(cacheNames = "books", sync = true, key = "'book-' + #id") public Book getById(Long id) {return new Book(String.valueOf(id), "some book"); }這里的key是一個(gè)Object對(duì)象,如果我們不在注解上面指定key,會(huì)使用keyGenerator生成的key。默認(rèn)的keyGenerator是SimpleKeyGenerator,它生成的是一個(gè)SimpleKey對(duì)象,方法也很簡(jiǎn)單,如果沒(méi)有入?yún)?#xff0c;就返回一個(gè)EMPTY的對(duì)象,如果有入?yún)?#xff0c;且只有一個(gè)入?yún)?#xff0c;并且不是空或者數(shù)組,就用這個(gè)參數(shù)(注意這里用的是參數(shù)本身,而不是SimpleKey對(duì)象。否則,用所有入?yún)粋€(gè)SimpleKey。
源碼:
@Override public Object generate(Object target, Method method, Object... params) {return generateKey(params); } /*** Generate a key based on the specified parameters.*/ public static Object generateKey(Object... params) {if (params.length == 0) {return SimpleKey.EMPTY;}if (params.length == 1) {Object param = params[0];if (param != null && !param.getClass().isArray()) {return param;}}return new SimpleKey(params); }看到這里你一定有一個(gè)疑問(wèn)吧,這里只用入?yún)?#xff0c;沒(méi)有類(lèi)名和方法名的區(qū)別,那如果兩個(gè)方法入?yún)⒁粯?#xff0c;豈不是key沖突了?
你的感覺(jué)沒(méi)錯(cuò),大家可以試一下這兩個(gè)方法:
// 定義兩個(gè)參數(shù)都是String的方法 @Override @Cacheable(cacheNames = "books", sync = true) public Book getByIsbn(String isbn) {simulateSlowService();return new Book(isbn, "Some book"); }@Override @Cacheable(cacheNames = "books", sync = true) public String test(String test) {return test; }// 調(diào)用這兩個(gè)方法,用相同的參數(shù)"test"
logger.info("test getByIsbn -->" + bookRepository.getByIsbn("test")); logger.info("test test -->" + bookRepository.test("test"));你會(huì)發(fā)現(xiàn)兩次生成的key相同,然后在調(diào)用test方法的時(shí)候,控制臺(tái)會(huì)報(bào)錯(cuò):
Caused by: java.lang.ClassCastException: class com.example.caching.Book cannot be cast to class java.lang.String (com.example.caching.Book is in unnamed module of loader 'app'; java.lang.String is in module java.base of loader 'bootstrap')at com.sun.proxy.$Proxy33.test(Unknown Source) ~[na:na]at com.example.caching.AppRunner.run(AppRunner.java:23) ~[main/:na]at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:795) ~[spring-boot-2.3.2.RELEASE.jar:2.3.2.RELEASE]... 5 common frames omittedBook不能強(qiáng)轉(zhuǎn)成String,因?yàn)槲覀兊谝淮握{(diào)用getByIsbn方法的時(shí)候,生成的key是test,然后換成了返回值Book對(duì)象到緩存里面。而調(diào)用test方法的時(shí)候,生成的key還是test,就會(huì)取出Book,但是test方法的返回值是String,所以會(huì)嘗試強(qiáng)轉(zhuǎn)到String,結(jié)果發(fā)現(xiàn)強(qiáng)轉(zhuǎn)失敗。
我們可以自定義一個(gè)keyGenerator來(lái)解決這個(gè)問(wèn)題:
@Component public class MyKeyGenerator implements KeyGenerator {@Overridepublic Object generate(Object target, Method method, Object... params) {return target.getClass().getName() + method.getName() + Stream.of(params).map(Object::toString).collect(Collectors.joining(","));} }然后就可以在配置里面使用這個(gè)自定義的MyKeyGenerator了,再次運(yùn)行程序,就不會(huì)出現(xiàn)上述問(wèn)題。
@Override @Cacheable(cacheNames = "books", sync = true, keyGenerator = "myKeyGenerator") public Book getByIsbn(String isbn) {simulateSlowService();return new Book(isbn, "Some book"); }@Override @Cacheable(cacheNames = "books", sync = true, keyGenerator = "myKeyGenerator") public String test(String test) {return test; }接著往下看,可以看到我們得到了一個(gè)Cache。這個(gè)Cache是在我們調(diào)用CacheAspectSupport的execute方法的時(shí)候,會(huì)new一個(gè)CacheOperationContext。在這個(gè)Context的構(gòu)造方法里,會(huì)用cacheResolver去解析注解中的Cache,生成Cache對(duì)象。默認(rèn)的cacheResolver是SimpleCacheResolver,它從CacheOperation中取得配置的cacheNames,然后用cacheManager去get一個(gè)Cache。這里的cacheManager是用于管理Cache的一個(gè)容器,默認(rèn)的cacheManager是ConcurrentMapCacheManager。聽(tīng)名字就知道是基于ConcurrentMap來(lái)做的了,底層是ConcurrentHashMap。那這里的Cache是什么東西呢?Cache就對(duì)“緩存容器”的一個(gè)抽象,包含了緩存會(huì)用到的get、put、evict、putIfAbsent等方法。不同的cacheNames會(huì)對(duì)應(yīng)不同的Cache對(duì)象,比如我們可以在一個(gè)方法上定義兩個(gè)cacheNames,雖然也可以用value,它是cacheNames的別名,但如果有多個(gè)配置的時(shí)候,更推薦用cacheNames,因?yàn)檫@樣具有更好的可讀性。
@Override @Cacheable(cacheNames = {"book", "test"}) public Book getByIsbn(String isbn) {simulateSlowService();return new Book(isbn, "Some book"); }默認(rèn)的Cache是ConcurrentMapCache,它也是基于ConcurrentHashMap的。但這里有個(gè)問(wèn)題,我們回到上面的execute方法的代碼,發(fā)現(xiàn)如果設(shè)置了sync為true,它取的是第一個(gè)Cache,而沒(méi)有管剩下的Cache。所以如果你配置了sync為true,只支持配置一個(gè)cacheNames,如果配了多個(gè),就會(huì)報(bào)錯(cuò):
@Cacheable(sync=true) only allows a single cache on...繼續(xù)往下看,發(fā)現(xiàn)調(diào)用的是Cache的get(Object, Callcable)方法。這個(gè)方法會(huì)先嘗試去緩存中用key取值,如果取不到在調(diào)用callable函數(shù),然后加到緩存里。Spring Cache也是期望Cache的實(shí)現(xiàn)類(lèi)在這個(gè)方法內(nèi)部實(shí)現(xiàn)“同步”的功能。所以我們?cè)倩剡^(guò)頭去看Cacheable中sync屬性上方的注釋,它寫(xiě)到:`使用sync為true,會(huì)有這些限制:
- 不支持unless,這個(gè)從代碼可以看到,只支持了condition,沒(méi)有支持unless;
- 只能有一個(gè)cache,因?yàn)榇a就寫(xiě)死了一個(gè)。我猜這是為了更好地支持同步,它把同步放到了Cache里面去實(shí)現(xiàn)。
- 沒(méi)有不支持其它的Cache操作,代碼里面寫(xiě)死了,只支持Cachable,我猜這也是為了支持同步。
如果sync為false呢?
繼續(xù)往下看execute的代碼,大概經(jīng)歷了下面這些步驟:
至此,我們就結(jié)合源碼解釋完了所有的配置發(fā)生作用的時(shí)機(jī)。
五、使用其它緩存框架
如果要使用其它的緩存框架,應(yīng)該怎么做呢?通過(guò)上面的源碼分析我們知道,如果要使用其它的緩存框架,我們只需要重新定義好CacheManager和CacheResolver這兩個(gè)Bean就行了。事實(shí)上,Spring會(huì)自動(dòng)檢測(cè)我們是否引入了相應(yīng)的緩存框架,如果我們引入了spring-data-redis,Spring就會(huì)自動(dòng)使用spring-data-redis提供的RedisCacheManager,RedisCache。如果我們要使用Caffeine框架。只需要引入Caffeine,Spring Cache就會(huì)默認(rèn)使用CaffeineCacheManager和CaffeineCache。
implementation 'org.springframework.boot:spring-boot-starter-cache' implementation 'com.github.ben-manes.caffeine:caffeine'Caffeine是一個(gè)性能非常高的緩存框架,它使用了Window TinyLfu回收策略,提供了一個(gè)近乎最佳的命中率。Spring Cache還支持各種配置,在CacheProperties類(lèi)里面,里面還提供了各種主流的緩存框架的特殊配置。比如Redis的過(guò)期時(shí)間等(默認(rèn)永不過(guò)期)。
private final Caffeine caffeine = new Caffeine();private final Couchbase couchbase = new Couchbase();private final EhCache ehcache = new EhCache();private final Infinispan infinispan = new Infinispan();private final JCache jcache = new JCache();private final Redis redis = new Redis();六、使用緩存帶來(lái)的問(wèn)題
使用緩存會(huì)帶來(lái)許多問(wèn)題,尤其是高并發(fā)下,包括緩存穿透、緩存擊穿、緩存雪崩、雙寫(xiě)不一致等問(wèn)題。具體的問(wèn)題介紹和常用的解決方案可以參考我的個(gè)人網(wǎng)站上的文章《緩存常見(jiàn)問(wèn)題及解決方案》。其中主要聊一下雙寫(xiě)不一致的問(wèn)題,這是一個(gè)比較常見(jiàn)的問(wèn)題,其中一個(gè)常用的解決方案是,更新的時(shí)候,先刪除緩存,再更新數(shù)據(jù)庫(kù)。所以Spring Cache的@CacheEvict會(huì)有一個(gè)beforeInvocation的配置。但使用緩存通常會(huì)存在緩存中的數(shù)據(jù)和數(shù)據(jù)庫(kù)中不一致的問(wèn)題,尤其是調(diào)用第三方接口,你不會(huì)知道它什么時(shí)候更新了數(shù)據(jù)。但使用緩存的業(yè)務(wù)場(chǎng)景很多時(shí)候并不需求數(shù)據(jù)的強(qiáng)一致,比如首頁(yè)的熱點(diǎn)文章,我們可以讓緩存一分鐘失效,這樣就算一分鐘內(nèi),不是最新的熱點(diǎn)排行也沒(méi)關(guān)系。
這個(gè)是無(wú)可避免的。因?yàn)榭傄幸粋€(gè)地方去放緩存。不管是ConcurrentHashMap也好,Redis也好,Caffeine也好,總歸是會(huì)占用額外的內(nèi)存資源去放緩存的。但緩存的思想正是用空間去換時(shí)間,有時(shí)候占用這點(diǎn)額外的空間對(duì)于時(shí)間上的優(yōu)化來(lái)說(shuō),是非常值得的。這里需要注意的是,SpringCache默認(rèn)使用的是ConcurrentHashMap,它不會(huì)自動(dòng)回收key,所以如果使用默認(rèn)的這個(gè)緩存,程序就會(huì)越來(lái)越大,并且得不到回收。最終可能導(dǎo)致OOM。
我們來(lái)模擬實(shí)驗(yàn)一下:
@Component public class MyKeyGenerator implements KeyGenerator {@Overridepublic Object generate(Object target, Method method, Object... params) {// 每次都生成不同的keyreturn UUID.randomUUID().toString();} } //調(diào)它個(gè)100w次 for (int i = 0; i < 1000000; i++) {bookRepository.test("test"); }然后把最大內(nèi)存設(shè)置成20M: -Xmx20M。
我們先來(lái)測(cè)試默認(rèn)的基于ConcurrentHashMap的緩存,發(fā)現(xiàn)它很快就會(huì)報(bào)OOM。
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "RMI TCP Connection(idle)"我們使用Caffeine,并且配置一下它的最大容量:
spring:cache:caffeine:spec: maximumSize=100再次運(yùn)行程序,發(fā)現(xiàn)正常運(yùn)行,不會(huì)報(bào)錯(cuò)。所以如果是用基于同一個(gè)JVM內(nèi)存的緩存的話(huà),個(gè)人比較推薦使用Caffeine,強(qiáng)烈不推薦用默認(rèn)的基于ConcurrentHashMap的實(shí)現(xiàn)。那什么情況適合用Redis這種需要調(diào)用第三方進(jìn)程的緩存呢?如果你的應(yīng)用程序是分布式的,一個(gè)服務(wù)器查詢(xún)出來(lái)后,希望其它服務(wù)器也能用這個(gè)緩存,那就推薦使用基于Redis的緩存。使用Spring Cache也有不好之處,就是屏蔽了底層緩存的特性。比如,很難做到不同的場(chǎng)景有不同的過(guò)期時(shí)間(但并不是做不到,也可以通過(guò)配置不同的cacheManager來(lái)實(shí)現(xiàn))。但整體上來(lái)看,還是利大于弊的,大家自己衡量,適合自己就好。
七、總結(jié)
* 每一個(gè)需要緩存的數(shù)據(jù)我們都來(lái)指定要放到那個(gè)名字的緩存。【緩存的分區(qū)(按照業(yè)務(wù)類(lèi)型分)】* 代表當(dāng)前方法的結(jié)果需要緩存,如果緩存中有,方法都不用調(diào)用,如果緩存中沒(méi)有,會(huì)調(diào)用方法。最后將方法的結(jié)果放入緩存* 默認(rèn)行為* 如果緩存中有,方法不再調(diào)用* key是默認(rèn)生成的:緩存的名字::SimpleKey::[](自動(dòng)生成key值)* 緩存的value值,默認(rèn)使用jdk序列化機(jī)制,將序列化的數(shù)據(jù)存到redis中* 默認(rèn)時(shí)間是 -1:** 自定義操作:key的生成* 指定生成緩存的key:key屬性指定,接收一個(gè)Spel* 指定緩存的數(shù)據(jù)的存活時(shí)間:配置文檔中修改存活時(shí)間* 將數(shù)據(jù)保存為json格式*** Spring-Cache的不足之處:* 1)、讀模式* 緩存穿透:查詢(xún)一個(gè)null數(shù)據(jù)。解決方案:緩存空數(shù)據(jù)* 緩存擊穿:大量并發(fā)進(jìn)來(lái)同時(shí)查詢(xún)一個(gè)正好過(guò)期的數(shù)據(jù)。解決方案:加鎖 ? 默認(rèn)是無(wú)加鎖的;使用sync = true來(lái)解決擊穿問(wèn)題* 緩存雪崩:大量的key同時(shí)過(guò)期。解決:加隨機(jī)時(shí)間。加上過(guò)期時(shí)間* 2)、寫(xiě)模式:(緩存與數(shù)據(jù)庫(kù)一致)* 1)、讀寫(xiě)加鎖。* 2)、引入Canal,感知到MySQL的更新去更新Redis* 3)、讀多寫(xiě)多,直接去數(shù)據(jù)庫(kù)查詢(xún)就行** 總結(jié):* 常規(guī)數(shù)據(jù)(讀多寫(xiě)少,即時(shí)性,一致性要求不高的數(shù)據(jù),完全可以使用Spring-Cache):寫(xiě)模式(只要緩存的數(shù)據(jù)有過(guò)期時(shí)間就足夠了)* 特殊數(shù)據(jù):特殊設(shè)計(jì)** 原理:* CacheManager(RedisCacheManager)->Cache(RedisCache)->Cache負(fù)責(zé)緩存的讀寫(xiě)文章轉(zhuǎn)自
總結(jié)
以上是生活随笔為你收集整理的Spring Cache的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Redis缓存穿透、击穿、雪崩、预热、更
- 下一篇: SpringBoot应用日志通过logs