品味 spring cache設計之美

      網友投稿 773 2022-05-30

      最近負責教育類產品的架構工作,兩位研發(fā)同學建議:“團隊封裝的Redis客戶端可否適配Spring Cache,這樣加緩存就會方便多了” 。

      于是邊查閱文檔邊實戰(zhàn),收獲頗豐,寫這篇文章,想和大家分享筆者學習的過程,一起品味Spring Cache設計之美。

      1 硬編碼

      在學習Spring Cache之前,筆者經常會硬編碼的方式使用緩存。

      舉個例子,為了提升用戶信息的查詢效率,我們對用戶信息使用了緩存,示例代碼如下:

      @Autowire private UserMapper userMapper; @Autowire private StringCommand stringCommand; //查詢用戶 public User getUserById(Long userId) { String cacheKey = "userId_" + userId; User user=stringCommand.get(cacheKey); if(user != null) { return user; } user = userMapper.getUserById(userId); if(user != null) { stringCommand.set(cacheKey,user); return user; } //修改用戶 public void updateUser(User user){ userMapper.updateUser(user); String cacheKey = "userId_" + userId.getId(); stringCommand.set(cacheKey , user); } //刪除用戶 public void deleteUserById(Long userId){ userMapper.deleteUserById(userId); String cacheKey = "userId_" + userId.getId(); stringCommand.del(cacheKey); } }

      相信很多同學都寫過類似風格的代碼,這種風格符合面向過程的編程思維,非常容易理解。但它也有一些缺點:

      代碼不夠優(yōu)雅。業(yè)務邏輯有四個典型動作:存儲,讀取,修改,刪除。每次操作都需要定義緩存Key ,調用緩存命令的API,產生較多的重復代碼;

      緩存操作和業(yè)務邏輯之間的代碼耦合度高,對業(yè)務邏輯有較強的侵入性。

      侵入性主要體現如下兩點:

      開發(fā)聯調階段,需要去掉緩存,只能注釋或者臨時刪除緩存操作代碼,也容易出錯;

      某些場景下,需要更換緩存組件,每個緩存組件有自己的API,更換成本頗高。

      2 緩存抽象

      首先需要明確一點:Spring Cache不是一個具體的緩存實現方案,而是一個對

      緩存使用的抽象

      (Cache Abstraction)。

      2.1 Spring AOP

      Spring AOP是基于代理模式(proxy-based)。

      通常情況下,定義一個對象,調用它的方法的時候,方法是直接被調用的。

      Pojo pojo = new SimplePojo(); pojo.foo();

      將代碼做一些調整,pojo對象的引用修改成代理類。

      ProxyFactory factory = new ProxyFactory(new SimplePojo()); factory.addInterface(Pojo.class); factory.addAdvice(new RetryAdvice()); Pojo pojo = (Pojo) factory.getProxy(); //this is a method call on the proxy! pojo.foo();

      調用pojo的foo方法的時候,實際上是動態(tài)生成的代理類調用foo方法。

      代理類在方法調用前可以獲取方法的參數,當調用方法結束后,可以獲取調用該方法的返回值,通過這種方式就可以實現緩存的邏輯。

      2.2 緩存聲明

      緩存聲明,也就是標識需要緩存的方法以及緩存策略。

      Spring Cache 提供了五個注解。

      @Cacheable:根據方法的請求參數對其結果進行緩存,下次同樣的參數來執(zhí)行該方法時可以直接從緩存中獲取結果,而不需要再次執(zhí)行該方法;

      @CachePut:根據方法的請求參數對其結果進行緩存,它每次都會觸發(fā)真實方法的調用;

      @CacheEvict:根據一定的條件刪除緩存;

      @Caching:組合多個緩存注解;

      @CacheConfig:類級別共享緩存相關的公共配置。

      我們重點講解:@Cacheable,@CachePut,@CacheEvict三個核心注解。

      2.2.1 @Cacheable注解

      @Cacheble注解表示這個方法有了緩存的功能。

      @Cacheable(value="user_cache",key="#userId", unless="#result == null") public User getUserById(Long userId) { User user = userMapper.getUserById(userId); return user; }

      上面的代碼片段里,getUserById方法和緩存user_cache 關聯起來,若方法返回的User對象不為空,則緩存起來。第二次相同參數userId調用該方法的時候,直接從緩存中獲取數據,并返回。

      ▍ 緩存key的生成

      我們都知道,緩存的本質是key-value存儲模式,每一次方法的調用都需要生成相應的Key, 才能操作緩存。

      通常情況下,@Cacheable有一個屬性key可以直接定義緩存key,開發(fā)者可以使用SpEL語言定義key值。

      若沒有指定屬性key,緩存抽象提供了 KeyGenerator來生成key ,默認的生成器代碼見下圖:

      它的算法也很容易理解:

      如果沒有參數,則直接返回SimpleKey.EMPTY;

      如果只有一個參數,則直接返回該參數;

      若有多個參數,則返回包含多個參數的SimpleKey對象。

      當然Spring Cache也考慮到需要自定義Key生成方式,需要我們實現org.springframework.cache.interceptor.KeyGenerator 接口。

      Object generate(Object target, Method method, Object... params);

      然后指定@Cacheable的keyGenerator屬性。

      @Cacheable(value="user_cache", keyGenerator="myKeyGenerator", unless="#result == null") public User getUserById(Long userId)

      ▍ 緩存條件

      有的時候,方法執(zhí)行的結果是否需要緩存,依賴于方法的參數或者方法執(zhí)行后的返回值。

      注解里可以通過condition屬性,通過Spel表達式返回的結果是true 還是false 判斷是否需要緩存。

      @Cacheable(cacheNames="book", condition="#name.length() < 32") public Book findBook(String name)

      上面的代碼片段里,當參數的長度小于32,方法執(zhí)行的結果才會緩存。

      除了condition,unless屬性也可以決定結果是否緩存,不過是在執(zhí)行方法后。

      @Cacheable(value="user_cache",key="#userId", unless="#result == null") public User getUserById(Long userId) {

      上面的代碼片段里,當返回的結果為null則不緩存。

      2.2.2 @CachePut注解

      @CachePut注解作用于緩存需要被更新的場景,和 @Cacheable 非常相似,但被注解的方法每次都會被執(zhí)行。

      返回值是否會放入緩存,依賴于condition和unless,默認情況下結果會存儲到緩存。

      @CachePut(value = "user_cache", key="#user.id", unless = "#result != null") public User updateUser(User user) { userMapper.updateUser(user); return user; }

      當調用updateUser方法時,每次方法都會被執(zhí)行,但是因為unless屬性每次都是true,所以并沒有將結果緩存。當去掉unless屬性,則結果會被緩存。

      2.2.3 @CacheEvict注解

      @CacheEvict 注解的方法在調用時會從緩存中移除已存儲的數據。

      @CacheEvict(value = "user_cache", key = "#id") public void deleteUserById(Long id) { userMapper.deleteUserById(id); }

      當調用deleteUserById方法完成后,緩存key等于參數id的緩存會被刪除,而且方法的返回的類型是Void ,這和@Cacheable明顯不同。

      2.3 緩存配置

      Spring Cache是一個對

      緩存使用的抽象

      ,它提供了多種存儲集成。

      要使用它們,需要簡單地聲明一個適當的CacheManager - 一個控制和管理Cache的實體。

      我們以Spring Cache默認的緩存實現Simple例子,簡單探索下CacheManager的機制。

      CacheManager非常簡單:

      public interface CacheManager { @Nullable Cache getCache(String name); Collection getCacheNames(); }

      在CacheConfigurations配置類中,可以看到不同集成類型有不同的緩存配置類。

      通過SpringBoot的自動裝配機制,創(chuàng)建CacheManager的實現類ConcurrentMapCacheManager。

      而ConcurrentMapCacheManager的getCache方法,會創(chuàng)建ConcurrentCacheMap。

      ConcurrentCacheMap實現了org.springframework.cache.Cache接口。

      品味 spring cache設計之美

      從Spring Cache的Simple的實現,緩存配置需要實現兩個接口:

      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 public class MyCacheConfig { @Bean public Caffeine caffeineConfig() { return Caffeine.newBuilder() .maximumSize(10000). expireAfterWrite(60, TimeUnit.MINUTES); } @Bean public CacheManager cacheManager(Caffeine caffeine) { CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager(); caffeineCacheManager.setCaffeine(caffeine); return caffeineCacheManager; } }

      首先創(chuàng)建了一個Caffeine對象,該對象標識本地緩存的最大數量是10000條,每個緩存數據在寫入60分鐘后失效。

      另外,MyCacheConfig類上我們添加了注解:@EnableCaching。

      3.1.3 業(yè)務代碼

      根據緩存聲明這一節(jié),我們很容易寫出如下代碼。

      @Cacheable(value = "user_cache", unless = "#result == null") public User getUserById(Long id) { return userMapper.getUserById(id); } @CachePut(value = "user_cache", key = "#user.id", unless = "#result == null") public User updateUser(User user) { userMapper.updateUser(user); return user; } @CacheEvict(value = "user_cache", key = "#id") public void deleteUserById(Long id) { userMapper.deleteUserById(id); }

      這段代碼與硬編碼里的代碼片段明顯精簡很多。

      當我們在Controller層調用 getUserById方法時,調試的時候,配置mybatis日志級別為DEBUG,方便監(jiān)控方法是否會緩存。

      第一次調用會查詢數據庫,打印相關日志:

      Preparing: select * FROM user t where t.id = ? Parameters: 1(Long) Total: 1

      第二次調用查詢方法的時候,數據庫SQL日志就沒有出現了, 也就說明緩存生效了。

      3.2 集成Redisson

      3.2.1 maven依賴

      org.Redisson Redisson 3.12.0

      3.2.2 Redisson緩存配置

      @Bean(destroyMethod = "shutdown") public RedissonClient Redisson() { Config config = new Config(); config.useSingleServer() .setAddress("redis://127.0.0.1:6201").setPassword("ts112GpO_ay"); return Redisson.create(config); } @Bean CacheManager cacheManager(RedissonClient RedissonClient) { Map config = new HashMap(); // create "user_cache" spring cache with ttl = 24 minutes and maxIdleTime = 12 minutes config.put("user_cache", new CacheConfig( 24 * 60 * 1000, 12 * 60 * 1000)); return new RedissonSpringCacheManager(RedissonClient, config); }

      可以看到,從Caffeine切換到Redisson,只需要修改緩存配置類,定義CacheManager 對象即可。而業(yè)務代碼并不需要改動。

      Controller層調用 getUserById方法,用戶ID為1的時候,可以從Redis Desktop Manager里看到: 用戶信息已被緩存,user_cache緩存存儲是Hash數據結構。

      因為Redisson默認的編解碼是FstCodec, 可以看到key的名稱是: \xF6\x01。

      在緩存配置代碼里,可以修改編解碼器。

      public RedissonClient Redisson() { Config config = new Config(); config.useSingleServer() .setAddress("redis://127.0.0.1:6201").setPassword("ts112GpO_ay"); config.setCodec(new JsonJacksonCodec()); return Redisson.create(config); }

      再次調用 getUserById方法 ,控制臺就變成:

      可以觀察到:緩存key已經變成了:[“java.lang.Long”,1],改變序列化后key和value已發(fā)生了變化。

      3.3 從列表緩存再次理解緩存抽象

      列表緩存在業(yè)務中經常會遇到。通常有兩種實現形式:

      整體列表緩存;

      按照每個條目緩存,通過redis,memcached的聚合查詢方法批量獲取列表,若緩存沒有命中,則從數據庫重新加載,并放入緩存里。

      那么Spring cache整合Redisson如何緩存列表數據呢?

      @Cacheable(value = "user_cache") public List getUserList(List idList) { return userMapper.getUserByIds(idList); }

      執(zhí)行getUserList方法,參數id列表為:[1,3] 。

      執(zhí)行完成之后,控制臺里可以看到:

      列表整體直接被緩存起來

      ,用戶列表緩存和用戶條目緩存并沒有共享,他們是平行的關系。

      這種情況下,緩存的顆粒度控制也沒有那么細致。

      類似這樣的思考,很多開發(fā)者也向Spring Framework研發(fā)團隊提過。

      官方的回答也很明確:對于緩存抽象來講,它并不關心方法返回的數據類型,假如是集合,那么也就意味著需要把集合數據在緩存中保存起來。

      還有一位開發(fā)者,定義了一個@CollectionCacheable注解,并做出了原型,擴展了Spring Cache的列表緩存功能。

      @Cacheable("myCache") public String findById(String id) { //access DB backend return item } @CollectionCacheable("myCache") public Map findByIds(Collection ids) { //access DB backend,return map of id to item }

      官方也未采納,因為緩存抽象并不想引入太多的復雜性。

      寫到這里,相信大家對緩存抽象有了更進一步的理解。當我們想實現更復雜的緩存功能時,需要對Spring Cache做一定程度的擴展。

      4 自定義二級緩存

      4.1 應用場景

      筆者曾經在原來的項目,高并發(fā)場景下多次使用多級緩存。多級緩存是一個非常有趣的功能點,值得我們去擴展。

      多級緩存有如下優(yōu)勢:

      離用戶越近,速度越快;

      減少分布式緩存查詢頻率,降低序列化和反序列化的CPU消耗;

      大幅度減少網絡IO以及帶寬消耗。

      進程內緩存做為一級緩存,分布式緩存做為二級緩存,首先從一級緩存中查詢,若能查詢到數據則直接返回,否則從二級緩存中查詢,若二級緩存中可以查詢到數據,則回填到一級緩存中,并返回數據。若二級緩存也查詢不到,則從數據源中查詢,將結果分別回填到一級緩存,二級緩存中。

      Spring Cache并沒有二級緩存的實現,我們可以實現一個簡易的二級緩存DEMO,加深對技術的理解。

      4.2 設計思路

      MultiLevelCacheManager:多級緩存管理器;

      MultiLevelChannel:封裝Caffeine和RedissonClient;

      MultiLevelCache:實現org.springframework.cache.Cache接口;

      MultiLevelCacheConfig:配置緩存過期時間等;

      MultiLevelCacheManager是最核心的類,需要實現getCache和getCacheNames兩個接口。

      創(chuàng)建多級緩存,第一級緩存是:Caffeine , 第二級緩存是:Redisson。

      二級緩存,為了快速完成DEMO,我們使用Redisson對Spring Cache的擴展類RedissonCache 。它的底層是RMap,底層存儲是Hash。

      我們重點看下緩存的「查詢」和「存儲」的方法:

      @Override public ValueWrapper get(Object key) { Object result = getRawResult(key); return toValueWrapper(result); } public Object getRawResult(Object key) { logger.info("從一級緩存查詢key:" + key); Object result = localCache.getIfPresent(key); if (result != null) { return result; } logger.info("從二級緩存查詢key:" + key); result = RedissonCache.getNativeCache().get(key); if (result != null) { localCache.put(key, result); } return result; }

      「查詢」數據的流程:

      先從本地緩存中查詢數據,若能查詢到,直接返回;

      本地緩存查詢不到數據,查詢分布式緩存,若可以查詢出來,回填到本地緩存,并返回;

      若分布式緩存查詢不到數據,則默認會執(zhí)行被注解的方法。

      下面來看下「存儲」的代碼:

      public void put(Object key, Object value) { 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 * FROM user t where t.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)展示頁面,榜單等等。這些場景的特點是對數據實時性沒有那么嚴格的要求,只需要將數據源緩存下來,過期之后自動刷新即可。 這些場景下,Spring Cache就是神器,能大幅度提升研發(fā)效率。

      但在高并發(fā)大數據量的場景下,精細的緩存顆粒度的控制上,還是需要做功能擴展。

      多級緩存;

      列表緩存;

      緩存變更-;

      筆者也在思考這幾點的過程,研讀了 j2cache , jetcache相關源碼,受益匪淺。后續(xù)的文章會重點分享下筆者的心得。

      如果我的文章對你有所幫助,還請幫忙、在看、轉發(fā)一下,你的支持會激勵我輸出更高質量的文章,非常感謝!

      Spring Spring Boot 緩存

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發(fā)現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:真正理解Git的重寫歷史是啥
      下一篇:前端響應式布局與Bootstrap柵格系統(tǒng)快速了解與應用
      相關文章
      亚洲国产香蕉碰碰人人| 亚洲色成人四虎在线观看| 久久精品国产精品亚洲毛片| 亚洲aⅴ无码专区在线观看春色| 日韩亚洲国产高清免费视频| 亚洲成a人片毛片在线| 亚洲无限乱码一二三四区| 亚洲综合视频在线观看| 亚洲精品白色在线发布| 亚洲精品中文字幕乱码| 亚洲国产日韩女人aaaaaa毛片在线 | 91在线亚洲精品专区| 久久久影院亚洲精品| 青青草原亚洲视频| 亚洲无码在线播放| 亚洲熟妇无码另类久久久| 国产成人精品日本亚洲专区61| 亚洲精品偷拍视频免费观看| 国产成人综合亚洲一区| 午夜亚洲国产成人不卡在线| 亚洲AV无码专区在线电影成人| 色欲aⅴ亚洲情无码AV| 国产精品亚洲一区二区三区在线观看| 亚洲天堂一区在线| 亚洲日本国产综合高清| 亚洲youwu永久无码精品| 亚洲熟伦熟女专区hd高清| 亚洲av无码成人影院一区| www亚洲一级视频com| 国产亚洲色视频在线| 亚洲va久久久噜噜噜久久狠狠| 91天堂素人精品系列全集亚洲 | 久久精品夜色国产亚洲av| 亚洲VA中文字幕无码一二三区| 久久久久亚洲AV无码专区体验| 亚洲另类春色校园小说| 亚洲人成未满十八禁网站| 国产精品亚洲一区二区三区在线观看| 不卡一卡二卡三亚洲| 亚洲成人在线电影| 亚洲av无码电影网|