乡下人产国偷v产偷v自拍,国产午夜片在线观看,婷婷成人亚洲综合国产麻豆,久久综合给合久久狠狠狠9

  • <output id="e9wm2"></output>
    <s id="e9wm2"><nobr id="e9wm2"><ins id="e9wm2"></ins></nobr></s>

    • 分享

      使用 Spring Cache 實(shí)現(xiàn)緩存,這種方式才叫優(yōu)雅!

       昵稱10087950 2022-06-16 發(fā)布于江蘇

      最近負(fù)責(zé)教育類產(chǎn)品的架構(gòu)工作,兩位研發(fā)同學(xué)建議:“團(tuán)隊(duì)封裝的Redis客戶端可否適配Spring Cache,這樣加緩存就會(huì)方便多了” 。

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

      圖片

      1 硬編碼

      在學(xué)習(xí)Spring Cache之前,筆者經(jīng)常會(huì)硬編碼的方式使用緩存。

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

        @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);
         }
        }

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

      1. 代碼不夠優(yōu)雅。業(yè)務(wù)邏輯有四個(gè)典型動(dòng)作:存儲(chǔ),讀取,修改刪除。每次操作都需要定義緩存Key ,調(diào)用緩存命令的API,產(chǎn)生較多的重復(fù)代碼;

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

        侵入性主要體現(xiàn)如下兩點(diǎn):

        • 開發(fā)聯(lián)調(diào)階段,需要去掉緩存,只能注釋或者臨時(shí)刪除緩存操作代碼,也容易出錯(cuò);
        • 某些場景下,需要更換緩存組件,每個(gè)緩存組件有自己的API,更換成本頗高。

      2 緩存抽象

      首先需要明確一點(diǎn):Spring Cache不是一個(gè)具體的緩存實(shí)現(xiàn)方案,而是一個(gè)對緩存使用的抽象(Cache Abstraction)。

      圖片

      2.1 Spring AOP

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

      通常情況下,定義一個(gè)對象,調(diào)用它的方法的時(shí)候,方法是直接被調(diào)用的。

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

      將代碼做一些調(diào)整,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();
      圖片

      調(diào)用pojo的foo方法的時(shí)候,實(shí)際上是動(dòng)態(tài)生成的代理類調(diào)用foo方法。

      代理類在方法調(diào)用前可以獲取方法的參數(shù),當(dāng)調(diào)用方法結(jié)束后,可以獲取調(diào)用該方法的返回值,通過這種方式就可以實(shí)現(xiàn)緩存的邏輯。

      2.2  緩存聲明

      緩存聲明,也就是標(biāo)識(shí)需要緩存的方法以及緩存策略

      Spring Cache 提供了五個(gè)注解。

      • @Cacheable:根據(jù)方法的請求參數(shù)對其結(jié)果進(jìn)行緩存,下次同樣的參數(shù)來執(zhí)行該方法時(shí)可以直接從緩存中獲取結(jié)果,而不需要再次執(zhí)行該方法;
      • @CachePut:根據(jù)方法的請求參數(shù)對其結(jié)果進(jìn)行緩存,它每次都會(huì)觸發(fā)真實(shí)方法的調(diào)用;
      • @CacheEvict:根據(jù)一定的條件刪除緩存;
      • @Caching:組合多個(gè)緩存注解;
      • @CacheConfig:類級別共享緩存相關(guān)的公共配置。

      我們重點(diǎn)講解:@Cacheable,@CachePut,@CacheEvict三個(gè)核心注解。

      2.2.1 @Cacheable注解

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

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

      上面的代碼片段里,getUserById方法和緩存user_cache 關(guān)聯(lián)起來,若方法返回的User對象不為空,則緩存起來。第二次相同參數(shù)userId調(diào)用該方法的時(shí)候,直接從緩存中獲取數(shù)據(jù),并返回。

      ▍ 緩存key的生成

      我們都知道,緩存的本質(zhì)是key-value存儲(chǔ)模式,每一次方法的調(diào)用都需要生成相應(yīng)的Key, 才能操作緩存。

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

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

      它的算法也很容易理解:

      • 如果沒有參數(shù),則直接返回SimpleKey.EMPTY;
      • 如果只有一個(gè)參數(shù),則直接返回該參數(shù);
      • 若有多個(gè)參數(shù),則返回包含多個(gè)參數(shù)的SimpleKey對象。

      當(dāng)然Spring Cache也考慮到需要自定義Key生成方式,需要我們實(shí)現(xiàn)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) 

      ▍ 緩存條件

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

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

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

      上面的代碼片段里,當(dāng)參數(shù)的長度小于32,方法執(zhí)行的結(jié)果才會(huì)緩存。

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

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

      上面的代碼片段里,當(dāng)返回的結(jié)果為null則不緩存。

      2.2.2 @CachePut注解

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

      返回值是否會(huì)放入緩存,依賴于condition和unless,默認(rèn)情況下結(jié)果會(huì)存儲(chǔ)到緩存。

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

      當(dāng)調(diào)用updateUser方法時(shí),每次方法都會(huì)被執(zhí)行,但是因?yàn)閡nless屬性每次都是true,所以并沒有將結(jié)果緩存。當(dāng)去掉unless屬性,則結(jié)果會(huì)被緩存。

      2.2.3 @CacheEvict注解

      @CacheEvict 注解的方法在調(diào)用時(shí)會(huì)從緩存中移除已存儲(chǔ)的數(shù)據(jù)。

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

      當(dāng)調(diào)用deleteUserById方法完成后,緩存key等于參數(shù)id的緩存會(huì)被刪除,而且方法的返回的類型是Void ,這和@Cacheable明顯不同。

      2.3 緩存配置

      Spring Cache是一個(gè)對緩存使用的抽象,它提供了多種存儲(chǔ)集成。

      圖片

      要使用它們,需要簡單地聲明一個(gè)適當(dāng)?shù)?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(0, 150, 136);">CacheManager - 一個(gè)控制和管理Cache的實(shí)體。

      我們以Spring Cache默認(rèn)的緩存實(shí)現(xiàn)Simple例子,簡單探索下CacheManager的機(jī)制。

      CacheManager非常簡單:

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

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

      圖片

      通過SpringBoot的自動(dòng)裝配機(jī)制,創(chuàng)建CacheManager的實(shí)現(xiàn)類ConcurrentMapCacheManager。

      圖片

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

      圖片

      ConcurrentCacheMap實(shí)現(xiàn)了org.springframework.cache.Cache接口。

      圖片

      從Spring Cache的Simple的實(shí)現(xiàn),緩存配置需要實(shí)現(xiàn)兩個(gè)接口:

      • org.springframework.cache.CacheManager

      • org.springframework.cache.Cache

      3 入門例子

      首先我們先創(chuàng)建一個(gè)工程spring-cache-demo。

      圖片

      caffeine和Redisson分別是本地內(nèi)存和分布式緩存Redis框架中的佼佼者,我們分別演示如何集成它們。

      3.1 集成caffeine

      3.1.1 maven依賴

      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-cache</artifactId>
      </dependency>
      <dependency>
        <groupId>com.github.ben-manes.caffeine</groupId>
        <artifactId>caffeine</artifactId>
        <version>2.7.0</version>
      </dependency>

      3.1.2 Caffeine緩存配置

      我們先創(chuàng)建一個(gè)緩存配置類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)建了一個(gè)Caffeine對象,該對象標(biāo)識(shí)本地緩存的最大數(shù)量是10000條,每個(gè)緩存數(shù)據(jù)在寫入60分鐘后失效。

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

      3.1.3  業(yè)務(wù)代碼

      根據(jù)緩存聲明這一節(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);
      }

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

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

      第一次調(diào)用會(huì)查詢數(shù)據(jù)庫,打印相關(guān)日志:

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

      第二次調(diào)用查詢方法的時(shí)候,數(shù)據(jù)庫SQL日志就沒有出現(xiàn)了, 也就說明緩存生效了。

      3.2 集成Redisson

      3.2.1 maven依賴

      <dependency>
         <groupId>org.Redisson</groupId>
         <artifactId>Redisson</artifactId>
         <version>3.12.0</version>
      </dependency>

      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<String, CacheConfig> config = new HashMap<String, CacheConfig>();
       // 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è)務(wù)代碼并不需要改動(dòng)。

      Controller層調(diào)用 getUserById方法,用戶ID為1的時(shí)候,可以從Redis Desktop Manager里看到:用戶信息已被緩存,user_cache緩存存儲(chǔ)是Hash數(shù)據(jù)結(jié)構(gòu)。

      圖片

      因?yàn)镽edisson默認(rèn)的編解碼是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);
      }

      再次調(diào)用 getUserById方法 ,控制臺(tái)就變成:

      圖片

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

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

      列表緩存在業(yè)務(wù)中經(jīng)常會(huì)遇到。通常有兩種實(shí)現(xiàn)形式:

      1. 整體列表緩存;
      2. 按照每個(gè)條目緩存,通過redis,memcached的聚合查詢方法批量獲取列表,若緩存沒有命中,則從數(shù)據(jù)庫重新加載,并放入緩存里。

      那么Spring cache整合Redisson如何緩存列表數(shù)據(jù)呢?

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

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

      圖片

      執(zhí)行完成之后,控制臺(tái)里可以看到:列表整體直接被緩存起來,用戶列表緩存和用戶條目緩存并沒有共享,他們是平行的關(guān)系。

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

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

      圖片圖片

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

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

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

      官方也未采納,因?yàn)?strong style="color: rgb(34, 34, 34);letter-spacing: 0.5444px;">緩存抽象并不想引入太多的復(fù)雜性。

      寫到這里,相信大家對緩存抽象有了更進(jìn)一步的理解。當(dāng)我們想實(shí)現(xiàn)更復(fù)雜的緩存功能時(shí),需要對Spring Cache做一定程度的擴(kuò)展。

      4 自定義二級緩存

      4.1 應(yīng)用場景

      筆者曾經(jīng)在原來的項(xiàng)目,高并發(fā)場景下多次使用多級緩存。多級緩存是一個(gè)非常有趣的功能點(diǎn),值得我們?nèi)U(kuò)展。

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

      1. 離用戶越近,速度越快;
      2. 減少分布式緩存查詢頻率,降低序列化和反序列化的CPU消耗;
      3. 大幅度減少網(wǎng)絡(luò)IO以及帶寬消耗。

      進(jìn)程內(nèi)緩存做為一級緩存,分布式緩存做為二級緩存,首先從一級緩存中查詢,若能查詢到數(shù)據(jù)則直接返回,否則從二級緩存中查詢,若二級緩存中可以查詢到數(shù)據(jù),則回填到一級緩存中,并返回?cái)?shù)據(jù)。若二級緩存也查詢不到,則從數(shù)據(jù)源中查詢,將結(jié)果分別回填到一級緩存,二級緩存中。

      圖片
      來自《鳳凰架構(gòu)》緩存篇

      Spring Cache并沒有二級緩存的實(shí)現(xiàn),我們可以實(shí)現(xiàn)一個(gè)簡易的二級緩存DEMO,加深對技術(shù)的理解。

      4.2 設(shè)計(jì)思路

      1. MultiLevelCacheManager:多級緩存管理器;
      2. MultiLevelChannel:封裝Caffeine和RedissonClient;
      3. MultiLevelCache:實(shí)現(xiàn)org.springframework.cache.Cache接口;
      4. MultiLevelCacheConfig:配置緩存過期時(shí)間等;

      MultiLevelCacheManager是最核心的類,需要實(shí)現(xiàn)getCachegetCacheNames兩個(gè)接口。

      圖片

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

      圖片

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

      圖片

      我們重點(diǎn)看下緩存的「查詢」和「存儲(chǔ)」的方法:

      @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;
      }

      查詢」數(shù)據(jù)的流程:

      1. 先從本地緩存中查詢數(shù)據(jù),若能查詢到,直接返回;
      2. 本地緩存查詢不到數(shù)據(jù),查詢分布式緩存,若可以查詢出來,回填到本地緩存,并返回;
      3. 若分布式緩存查詢不到數(shù)據(jù),則默認(rèn)會(huì)執(zhí)行被注解的方法。

      下面來看下「存儲(chǔ)」的代碼:

      public void put(Object key, Object value) {
          logger.info("寫入一級緩存 key:" + key);
          localCache.put(key, value);
          logger.info("寫入二級緩存 key:" + key);
          RedissonCache.put(key, value);
      }

      最后配置緩存管理器,原有的業(yè)務(wù)代碼不變。

      圖片

      執(zhí)行下getUserById方法,查詢用戶編號(hào)為1的用戶信息。

      - 從一級緩存查詢key:1
      - 從二級緩存查詢key:1
      - ==> Preparing: select * FROM user t where t.id = ? 
      - ==> Parameters: 1(Long)
      - <== Total: 1
      - 寫入一級緩存 key:1
      - 寫入二級緩存 key:1

      第二次執(zhí)行相同的動(dòng)作,從日志可用看到從優(yōu)先會(huì)從本地內(nèi)存中查詢出結(jié)果。

      - 從一級緩存查詢key:1

      等待30s , 再執(zhí)行一次,因?yàn)楸镜鼐彺鏁?huì)失效,所以執(zhí)行的時(shí)候會(huì)查詢二級緩存

      - 從一級緩存查詢key:1
      - 從二級緩存查詢key:1

      一個(gè)簡易的二級緩存就組裝完了。

      5 什么場景選擇Spring Cache

      在做技術(shù)選型的時(shí)候,需要針對場景選擇不同的技術(shù)。

      筆者認(rèn)為Spring Cache的功能很強(qiáng)大,設(shè)計(jì)也非常優(yōu)雅。特別適合緩存控制沒有那么細(xì)致的場景。比如門戶首頁,偏靜態(tài)展示頁面,榜單等等。這些場景的特點(diǎn)是對數(shù)據(jù)實(shí)時(shí)性沒有那么嚴(yán)格的要求,只需要將數(shù)據(jù)源緩存下來,過期之后自動(dòng)刷新即可。這些場景下,Spring Cache就是神器,能大幅度提升研發(fā)效率。

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

      1. 多級緩存;
      2. 列表緩存;
      3. 緩存變更監(jiān)聽器;

      筆者也在思考這幾點(diǎn)的過程,研讀了 j2cache , jetcache相關(guān)源碼,受益匪淺。它們的設(shè)計(jì)思想很多可以用于擴(kuò)展Spring Cache。 

        本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點(diǎn)擊一鍵舉報(bào)。
        轉(zhuǎn)藏 分享 獻(xiàn)花(0

        0條評論

        發(fā)表

        請遵守用戶 評論公約

        類似文章 更多