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