1 硬編碼
在學習Spring Cache之前,筆者經常會硬編碼的方式使用緩存。
舉個例子,為了提升用戶信息的查詢效率,我們對用戶信息使用了緩存,示例代碼如下:
@Autowire privateUserMapperuserMapper; @Autowire privateStringCommandstringCommand; //查詢用戶 publicUsergetUserById(LonguserId){ StringcacheKey="userId_"+userId; Useruser=stringCommand.get(cacheKey); if(user!=null){ returnuser; } user=userMapper.getUserById(userId); if(user!=null){ stringCommand.set(cacheKey,user); returnuser; } //修改用戶 publicvoidupdateUser(Useruser){ userMapper.updateUser(user); StringcacheKey="userId_"+userId.getId(); stringCommand.set(cacheKey,user); } //刪除用戶 publicvoiddeleteUserById(LonguserId){ userMapper.deleteUserById(userId); StringcacheKey="userId_"+userId.getId(); stringCommand.del(cacheKey); } }
相信很多同學都寫過類似風格的代碼,這種風格符合面向過程的編程思維,非常容易理解。但它也有一些缺點:
代碼不夠優(yōu)雅。業(yè)務邏輯有四個典型動作:存儲,讀取,修改,刪除。每次操作都需要定義緩存Key ,調用緩存命令的API,產生較多的重復代碼;
緩存操作和業(yè)務邏輯之間的代碼耦合度高,對業(yè)務邏輯有較強的侵入性。
侵入性主要體現(xiàn)如下兩點:
開發(fā)聯(lián)調階段,需要去掉緩存,只能注釋或者臨時刪除緩存操作代碼,也容易出錯;
某些場景下,需要更換緩存組件,每個緩存組件有自己的API,更換成本頗高。
2 緩存抽象
首先需要明確一點:Spring Cache不是一個具體的緩存實現(xiàn)方案,而是一個對緩存使用的抽象(Cache Abstraction )。
2.1 Spring AOP
Spring AOP是基于代理模式(proxy-based )。
通常情況下,定義一個對象,調用它的方法的時候,方法是直接被調用的。
Pojopojo=newSimplePojo(); pojo.foo();
將代碼做一些調整,pojo對象的引用修改成代理類。
ProxyFactoryfactory=newProxyFactory(newSimplePojo()); factory.addInterface(Pojo.class); factory.addAdvice(newRetryAdvice()); Pojopojo=(Pojo)factory.getProxy(); //thisisamethodcallontheproxy! pojo.foo();
調用pojo的foo方法的時候,實際上是動態(tài)生成的代理類調用foo方法。
代理類在方法調用前可以獲取方法的參數(shù),當調用方法結束后,可以獲取調用該方法的返回值,通過這種方式就可以實現(xiàn)緩存的邏輯。
2.2 緩存聲明
緩存聲明,也就是標識需要緩存的方法以及緩存策略 。
Spring Cache 提供了五個注解。
@Cacheable:根據方法的請求參數(shù)對其結果進行緩存,下次同樣的參數(shù)來執(zhí)行該方法時可以直接從緩存中獲取結果,而不需要再次執(zhí)行該方法;
@CachePut:根據方法的請求參數(shù)對其結果進行緩存,它每次都會觸發(fā)真實方法的調用;
@CacheEvict:根據一定的條件刪除緩存;
@Caching:組合多個緩存注解;
@CacheConfig:類級別共享緩存相關的公共配置。
我們重點講解:@Cacheable,@CachePut,@CacheEvict三個核心注解。
2.2.1 @Cacheable注解
@Cacheble注解表示這個方法有了緩存的功能。
@Cacheable(value="user_cache",key="#userId",unless="#result==null") publicUsergetUserById(LonguserId){ Useruser=userMapper.getUserById(userId); returnuser; }
上面的代碼片段里,getUserById方法和緩存user_cache 關聯(lián)起來,若方法返回的User對象不為空,則緩存起來。第二次相同參數(shù)userId調用該方法的時候,直接從緩存中獲取數(shù)據,并返回。
▍ 緩存key的生成
我們都知道,緩存的本質是key-value存儲模式,每一次方法的調用都需要生成相應的Key, 才能操作緩存。
通常情況下,@Cacheable有一個屬性key可以直接定義緩存key,開發(fā)者可以使用SpEL語言定義key值。
若沒有指定屬性key,緩存抽象提供了 KeyGenerator來生成key ,默認的生成器代碼見下圖:
它的算法也很容易理解:
如果沒有參數(shù),則直接返回SimpleKey.EMPTY ;
如果只有一個參數(shù),則直接返回該參數(shù);
若有多個參數(shù),則返回包含多個參數(shù)的SimpleKey 對象。
當然Spring Cache也考慮到需要自定義Key生成方式,需要我們實現(xiàn)org.springframework.cache.interceptor.KeyGenerator 接口。
Objectgenerate(Objecttarget,Methodmethod,Object...params);
然后指定@Cacheable的keyGenerator屬性。
@Cacheable(value="user_cache",keyGenerator="myKeyGenerator",unless="#result==null") publicUsergetUserById(LonguserId)
▍ 緩存條件
有的時候,方法執(zhí)行的結果是否需要緩存,依賴于方法的參數(shù)或者方法執(zhí)行后的返回值。
注解里可以通過condition屬性,通過Spel表達式返回的結果是true 還是false 判斷是否需要緩存。
@Cacheable(cacheNames="book",condition="#name.length()32") public?Book?findBook(String?name)
上面的代碼片段里,當參數(shù)的長度小于32,方法執(zhí)行的結果才會緩存。
除了condition,unless屬性也可以決定結果是否緩存,不過是在執(zhí)行方法后。
@Cacheable(value="user_cache",key="#userId",unless="#result==null") publicUsergetUserById(LonguserId){
上面的代碼片段里,當返回的結果為null則不緩存。
2.2.2 @CachePut注解
@CachePut注解作用于緩存需要被更新的場景,和 @Cacheable 非常相似,但被注解的方法每次都會被執(zhí)行。
返回值是否會放入緩存,依賴于condition和unless,默認情況下結果會存儲到緩存。
@CachePut(value="user_cache",key="#user.id",unless="#result!=null") publicUserupdateUser(Useruser){ userMapper.updateUser(user); returnuser; }
當調用updateUser方法時,每次方法都會被執(zhí)行,但是因為unless屬性每次都是true,所以并沒有將結果緩存。當去掉unless屬性,則結果會被緩存。
2.2.3 @CacheEvict注解
@CacheEvict 注解的方法在調用時會從緩存中移除已存儲的數(shù)據。
@CacheEvict(value="user_cache",key="#id") publicvoiddeleteUserById(Longid){ userMapper.deleteUserById(id); }
當調用deleteUserById方法完成后,緩存key等于參數(shù)id的緩存會被刪除,而且方法的返回的類型是Void ,這和@Cacheable明顯不同。
2.3 緩存配置
Spring Cache是一個對緩存使用的抽象,它提供了多種存儲集成。
要使用它們,需要簡單地聲明一個適當?shù)腃acheManager - 一個控制和管理Cache的實體。
我們以Spring Cache默認的緩存實現(xiàn)Simple 例子,簡單探索下CacheManager的機制。
CacheManager非常簡單:
publicinterfaceCacheManager{ @Nullable CachegetCache(Stringname); CollectiongetCacheNames(); }
在CacheConfigurations配置類中,可以看到不同集成類型有不同的緩存配置類。
通過SpringBoot的自動裝配機制,創(chuàng)建CacheManager的實現(xiàn)類ConcurrentMapCacheManager。
而ConcurrentMapCacheManager的getCache方法,會創(chuàng)建ConcurrentCacheMap。
ConcurrentCacheMap實現(xiàn)了org.springframework.cache.Cache接口。
從Spring Cache的Simple 的實現(xiàn),緩存配置需要實現(xiàn)兩個接口:
org.springframework.cache.CacheManager
org.springframework.cache.Cache
3 入門例子
首先我們先創(chuàng)建一個工程spring-cache-demo。
caffeine和Redisson分別是本地內存和分布式緩存Redis框架中的佼佼者,我們分別演示如何集成它們。
3.1 集成caffeine
3.1.1 maven依賴
org.springframework.boot spring-boot-starter-cache com.github.ben-manes.caffeine caffeine 2.7.0
3.1.2 Caffeine緩存配置
我們先創(chuàng)建一個緩存配置類MyCacheConfig。
@Configuration @EnableCaching publicclassMyCacheConfig{ @Bean publicCaffeinecaffeineConfig(){ return Caffeine.newBuilder() .maximumSize(10000). expireAfterWrite(60,TimeUnit.MINUTES); } @Bean publicCacheManagercacheManager(Caffeinecaffeine){ CaffeineCacheManagercaffeineCacheManager=newCaffeineCacheManager(); caffeineCacheManager.setCaffeine(caffeine); returncaffeineCacheManager; } }
首先創(chuàng)建了一個Caffeine對象,該對象標識本地緩存的最大數(shù)量是10000條,每個緩存數(shù)據在寫入60分鐘后失效。
另外,MyCacheConfig類上我們添加了注解:**@EnableCaching** 。
3.1.3 業(yè)務代碼
根據緩存聲明 這一節(jié),我們很容易寫出如下代碼。
@Cacheable(value="user_cache",unless="#result==null") publicUsergetUserById(Longid){ returnuserMapper.getUserById(id); } @CachePut(value="user_cache",key="#user.id",unless="#result==null") publicUserupdateUser(Useruser){ userMapper.updateUser(user); returnuser; } @CacheEvict(value="user_cache",key="#id") publicvoiddeleteUserById(Longid){ userMapper.deleteUserById(id); }
這段代碼與硬編碼里的代碼片段明顯精簡很多。
當我們在Controller層調用 getUserById方法時,調試的時候,配置mybatis日志級別為DEBUG,方便監(jiān)控方法是否會緩存。
第一次調用會查詢數(shù)據庫,打印相關日志:
Preparing:select*FROMusertwheret.id=? Parameters:1(Long) Total:1
第二次調用查詢方法的時候,數(shù)據庫SQL日志就沒有出現(xiàn)了, 也就說明緩存生效了。
3.2 集成Redisson
3.2.1 maven依賴
org.Redisson Redisson 3.12.0
3.2.2 Redisson緩存配置
@Bean(destroyMethod="shutdown") publicRedissonClientRedisson(){ Configconfig=newConfig(); config.useSingleServer() .setAddress("redis://127.0.0.1:6201").setPassword("ts112GpO_ay"); returnRedisson.create(config); } @Bean CacheManagercacheManager(RedissonClientRedissonClient){ Mapconfig=newHashMap (); //create"user_cache"springcachewithttl=24minutesandmaxIdleTime=12minutes config.put("user_cache", newCacheConfig( 24*60*1000, 12*60*1000)); returnnewRedissonSpringCacheManager(RedissonClient,config); }
可以看到,從Caffeine切換到Redisson,只需要修改緩存配置類,定義CacheManager 對象即可。而業(yè)務代碼并不需要改動。
Controller層調用 getUserById方法,用戶ID為1的時候,可以從Redis Desktop Manager里看到:用戶信息已被緩存,user_cache緩存存儲是Hash數(shù)據結構。
因為Redisson默認的編解碼是FstCodec , 可以看到key的名稱是:xF6x01。
在緩存配置代碼里,可以修改編解碼器。
publicRedissonClientRedisson(){ Configconfig=newConfig(); config.useSingleServer() .setAddress("redis://127.0.0.1:6201").setPassword("ts112GpO_ay"); config.setCodec(newJsonJacksonCodec()); returnRedisson.create(config); }
再次調用 getUserById方法 ,控制臺就變成:
可以觀察到:緩存key已經變成了:["java.lang.Long",1],改變序列化后key和value已發(fā)生了變化。
3.3 從列表緩存再次理解緩存抽象
列表緩存在業(yè)務中經常會遇到。通常有兩種實現(xiàn)形式:
整體列表緩存;
按照每個條目緩存,通過redis,memcached的聚合查詢方法批量獲取列表,若緩存沒有命中,則從數(shù)據庫重新加載,并放入緩存里。
那么Spring cache整合Redisson如何緩存列表數(shù)據呢?
@Cacheable(value="user_cache") publicListgetUserList(List idList){ returnuserMapper.getUserByIds(idList); }
執(zhí)行getUserList方法,參數(shù)id列表為:[1,3] 。
執(zhí)行完成之后,控制臺里可以看到:列表整體直接被緩存起來,用戶列表緩存和用戶條目緩存并沒有共享 ,他們是平行的關系。
這種情況下,緩存的顆粒度控制也沒有那么細致。
類似這樣的思考,很多開發(fā)者也向Spring Framework研發(fā)團隊提過。
官方的回答也很明確:對于緩存抽象來講,它并不關心方法返回的數(shù)據類型,假如是集合,那么也就意味著需要把集合數(shù)據在緩存中保存起來。
還有一位開發(fā)者,定義了一個@CollectionCacheable 注解,并做出了原型,擴展了Spring Cache的列表緩存功能。
@Cacheable("myCache") publicStringfindById(Stringid){ //accessDBbackendreturnitem } @CollectionCacheable("myCache") publicMapfindByIds(Collection ids){ //accessDBbackend,returnmapofidtoitem }
官方也未采納,因為緩存抽象并不想引入太多的復雜性 。
寫到這里,相信大家對緩存抽象有了更進一步的理解。當我們想實現(xiàn)更復雜的緩存功能時,需要對Spring Cache做一定程度的擴展。
4 自定義二級緩存
4.1 應用場景
筆者曾經在原來的項目,高并發(fā)場景下多次使用多級緩存。多級緩存是一個非常有趣的功能點,值得我們去擴展。
多級緩存有如下優(yōu)勢:
離用戶越近,速度越快;
減少分布式緩存查詢頻率,降低序列化和反序列化的CPU消耗;
大幅度減少網絡IO以及帶寬消耗。
進程內緩存做為一級緩存,分布式緩存做為二級緩存,首先從一級緩存中查詢,若能查詢到數(shù)據則直接返回,否則從二級緩存中查詢,若二級緩存中可以查詢到數(shù)據,則回填到一級緩存中,并返回數(shù)據。若二級緩存也查詢不到,則從數(shù)據源中查詢,將結果分別回填到一級緩存,二級緩存中。
來自《鳳凰架構》緩存篇
Spring Cache并沒有二級緩存的實現(xiàn),我們可以實現(xiàn)一個簡易的二級緩存DEMO,加深對技術的理解。
4.2 設計思路
MultiLevelCacheManager :多級緩存管理器;
MultiLevelChannel :封裝Caffeine和RedissonClient;
MultiLevelCache :實現(xiàn)org.springframework.cache.Cache接口;
MultiLevelCacheConfig :配置緩存過期時間等;
MultiLevelCacheManager是最核心的類,需要實現(xiàn)getCache 和getCacheNames 兩個接口。
創(chuàng)建多級緩存,第一級緩存是:Caffeine , 第二級緩存是:Redisson。
二級緩存,為了快速完成DEMO,我們使用Redisson對Spring Cache的擴展類RedissonCache 。它的底層是RMap ,底層存儲是Hash。
我們重點看下緩存的「查詢」和「存儲」的方法:
@Override publicValueWrapperget(Objectkey){ Objectresult=getRawResult(key); returntoValueWrapper(result); } publicObjectgetRawResult(Objectkey){ logger.info("從一級緩存查詢key:"+key); Objectresult=localCache.getIfPresent(key); if(result!=null){ returnresult; } logger.info("從二級緩存查詢key:"+key); result=RedissonCache.getNativeCache().get(key); if(result!=null){ localCache.put(key,result); } returnresult; }
「查詢 」數(shù)據的流程:
先從本地緩存中查詢數(shù)據,若能查詢到,直接返回;
本地緩存查詢不到數(shù)據,查詢分布式緩存,若可以查詢出來,回填到本地緩存,并返回;
若分布式緩存查詢不到數(shù)據,則默認會執(zhí)行被注解的方法。
下面來看下「存儲 」的代碼:
publicvoidput(Objectkey,Objectvalue){ logger.info("寫入一級緩存key:"+key); localCache.put(key,value); logger.info("寫入二級緩存key:"+key); RedissonCache.put(key,value); }
最后配置緩存管理器,原有的業(yè)務代碼不變。
執(zhí)行下getUserById方法,查詢用戶編號為1的用戶信息。
-從一級緩存查詢key:1 -從二級緩存查詢key:1 -==>Preparing:select*FROMusertwheret.id=? -==>Parameters:1(Long) -<==?Total:?1 -?寫入一級緩存?key:1 -?寫入二級緩存?key:1
第二次執(zhí)行相同的動作,從日志可用看到從優(yōu)先會從本地內存中查詢出結果。
-從一級緩存查詢key:1
等待30s , 再執(zhí)行一次,因為本地緩存會失效,所以執(zhí)行的時候會查詢二級緩存
-從一級緩存查詢key:1 -從二級緩存查詢key:1
一個簡易的二級緩存就組裝完了。
5 什么場景選擇Spring Cache
在做技術選型的時候,需要針對場景選擇不同的技術。
筆者認為Spring Cache的功能很強大,設計也非常優(yōu)雅。特別適合緩存控制沒有那么細致的場景。比如門戶首頁,偏靜態(tài)展示頁面,榜單等等。這些場景的特點是對數(shù)據實時性沒有那么嚴格的要求,只需要將數(shù)據源緩存下來,過期之后自動刷新即可。這些場景下,Spring Cache就是神器,能大幅度提升研發(fā)效率。
但在高并發(fā)大數(shù)據量的場景下,精細的緩存顆粒度的控制上,還是需要做功能擴展。
審核編輯:劉清
-
SQL
+關注
關注
1文章
753瀏覽量
44032 -
AOP
+關注
關注
0文章
40瀏覽量
11084 -
cache技術
+關注
關注
0文章
41瀏覽量
1043 -
Redis
+關注
關注
0文章
370瀏覽量
10830
原文標題:使用 Spring Cache 實現(xiàn)緩存,這種方式才叫優(yōu)雅!
文章出處:【微信號:芋道源碼,微信公眾號:芋道源碼】歡迎添加關注!文章轉載請注明出處。
發(fā)布評論請先 登錄
相關推薦
評論