日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

筆者之前做商城項目時,做過商城首頁的商品分類功能。當時考慮分類是放在商城首頁,以后流量大,而且不經常變動,為了提升首頁訪問速度,我考慮使用緩存。對于JAVA開發而言,首先的緩存當然是redis。

 

優化前系統流程圖:

使用spring cache讓我的接口性能瞬間提升了100倍

 

我們從圖中可以看到,分類功能分為生成分類數據 和 獲取分類數據兩個流程,生成分類數據流程是有個JOB每隔5分鐘執行一次,從MySQL中獲取分類數據封裝成首頁需要展示的分類數據結構,然后保存到redis中。獲取分類數據流程是商城首頁調用分類接口,接口先從redis中獲取數據,如果沒有獲取到再從mysql中獲取。

一般情況下從redis就都能獲取數據,因為相應的key是沒有設置過期時間的,數據會一直都存在。以防萬一,我們做了一次兜底,如果獲取不到數據,就會從mysql中獲取。

 

本以為萬事大吉,后來,在系統上線之前,測試對商城首頁做了一次性能壓測,發現qps是100多,一直上不去。我們仔細分析了一下原因,發現了兩個主要的優化點:去掉多余的接口日志打印 和 分類接口引入redis cache做一次二級緩存。日志打印我在這里就不多說了,不是本文的重點,我們重點說一下redis cache。

 

優化后的系統流程圖:

使用spring cache讓我的接口性能瞬間提升了100倍

 

我們看到,其他的流程都沒有變,只是在獲取分類接口中增加了先從spring cache中獲取分類數據的功能,如果獲取不到再從redis中獲取,再獲取不到才從mysql中獲取。

 

經過這樣一次小小的調整,再重新壓測接口,性能一下子提升了N倍,滿足了業務要求。如此美妙的一次優化經驗,有必要跟大家分析一下。

 

我將從以下幾個方面給大家分享一下spring cache。

 

  1. 基本用法
  2. 項目中如何使用
  3. 工作原理

一、基本用法

SpringCache緩存功能的實現是依靠下面的這幾個注解完成的。

  • @EnableCaching:開啟緩存功能
  • @Cacheable:獲取緩存
  • @CachePut:更新緩存
  • @CacheEvict:刪除緩存
  • @Caching:組合定義多種緩存功能
  • @CacheConfig:定義公共設置,位于類之上

 

@EnableCaching注解是緩存的開關,如果要使用緩存功能,就必要打開這個開關,這個注解可以定義在Configuration類或者springboot的啟動類上面。

 

@Cacheable、@CachePut、@CacheEvict 這三個注解的用戶差不多,定義在需要緩存的具體類或方法上面。

 

@Cacheable(key="'id:'+#id")
   public User getUser(int id) {
        return userService.getUserById(id);
   }   @CachePut(key="'id:'+#user.id")
   public User insertUser(User user) {
       userService.insertUser(user);       return user;
   }   @CacheEvict(key="'id:'+#id")
   public int deleteUserById(int id) {
        userService.deleteUserById(id);        return id;
   }

需要注意的是@Caching注解跟另外三個注解不同,它可以組合另外三種注解,自定義新注解。

 

@Caching(
        cacheable = {@Cacheable(/*value = "emp",*/key = "#lastName")
        put = {@CachePut(/*value = "emp",*/key = "#result.id")}
)public Employee getEmpByLastName(String lastName){    return  employeeMApper.getEmpByLastName(lastName);
}

 

@CacheConfig一般定義在配置類上面,可以抽取緩存的公共配置,可以定義這個類全局的緩存名稱,其他的緩存方法就可以不配置緩存名稱了。

@CacheConfig(cacheNames = "emp")
@Service
public class EmployeeService 

二、項目中如何使用

  1. 引入caffeine的相關jar包我們這里使用caffeine,而非guava,因為Spring Boot 2.0中取代了guava
<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.6.0</version>
</dependency>

2. 配置CacheManager,開啟EnableCaching

 

@Configuration
@EnableCaching
public class CacheConfig {
    @Bean
    public CacheManager cacheManager(){
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        //Caffeine配置
        Caffeine<Object, Object> caffeine = Caffeine.newBuilder()
                //最后一次寫入后經過固定時間過期
                .expireAfterWrite(10, TimeUnit.SECONDS)
                //緩存的最大條數
                .maximumSize(1000);
        cacheManager.setCaffeine(caffeine);
        return cacheManager;
    }
}

3.使用Cacheable注解獲取數據

@Servicepublic class CategoryService {        //category是緩存名稱,#type是具體的key,可支持el表達式
    @Cacheable(value = "category", key = "#type")
    public CategoryModel getCategory(Integer type) {
        return getCategoryByType(type);
    }    private CategoryModel getCategoryByType(Integer type) {
        System.out.println("根據不同的type:" + type + "獲取不同的分類數據");
        CategoryModel categoryModel = new CategoryModel();        categoryModel.setId(1L);
        categoryModel.setParentId(0L);
        categoryModel.setName("電器");
        categoryModel.setLevel(3);
        return categoryModel;
    }}

4.測試

@Api(tags = "category", description = "分類相關接口")
@RestController
@RequestMapping("/category")
public class CategoryController {    @Autowired
    private CategoryService categoryService;    @GetMapping("/getCategory")
    public CategoryModel getCategory(@RequestParam("type") Integer type) {
        return categoryService.getCategory(type);
    }}

在瀏覽器中調用接口:

使用spring cache讓我的接口性能瞬間提升了100倍

 

可以看到,有數據返回。

再看看控制臺的打印。

使用spring cache讓我的接口性能瞬間提升了100倍

 

有數據打印,說明第一次請求進入了categoryService.getCategory方法的內部。

然后再重新請求一次,

使用spring cache讓我的接口性能瞬間提升了100倍

 

還是有數據,返回。但是控制臺沒有重新打印新數據,還是以前的數據,說明這一次請求走的是緩存,沒有進入categoryService.getCategory方法的內部。在5分鐘以內,再重復請求該接口,一直都是直接從緩存中獲取數據。

使用spring cache讓我的接口性能瞬間提升了100倍

 

說明緩存生效了,下面我介紹一下spring cache的工作原理

三、工作原理

 

通過上面的例子,相當朋友們對spring cache在項目中的用法有了一定的認識。那么它的工作原理是什么呢?

相信聰明的朋友們,肯定會想到,它用了AOP

 

沒錯,它就是用了AOP。那么具體是怎么用的?

 

我們先看看EnableCaching注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(CachingConfigurationSelector.class)
public @interface EnableCaching {
    // false JDK動態代理 true cglib代理   
    boolean proxyTargetClass() default false;
    //通知模式 JDK動態代理 或 AspectJ
    AdviceMode mode() default AdviceMode.PROXY;
    //排序
    int order() default Ordered.LOWEST_PRECEDENCE;
}

 

這個數據很簡單,定義了代理相關參數,引入了CachingConfigurationSelector類。再看看該類的getProxyImports方法

private String[] getProxyImports() {
    List<String> result = new ArrayList<>(3);
    result.add(AutoProxyRegistrar.class.getName());
    result.add(ProxyCachingConfiguration.class.getName());
    if (jsr107Present && jcacheImplPresent) {
      result.add(PROXY_JCACHE_CONFIGURATION_CLASS);    }    return StringUtils.toStringArray(result);
}

該方法引入了AutoProxyRegistrar和ProxyCachingConfiguration兩個類

 

AutoProxyRegistrar是讓spring cache擁有AOP的能力(至于如何擁有AOP的能力,這個是單獨的話題,感興趣的朋友可以自己閱讀一下源碼。或者關注一下我的公眾賬號,后面會有專門AOP的專題)。

 

重點看看ProxyCachingConfiguration

@Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class ProxyCachingConfiguration extends AbstractCachingConfiguration {
  @Bean(name = CacheManagementConfigUtils.CACHE_ADVISOR_BEAN_NAME)
  @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
  public BeanFactoryCacheOperationSourceAdvisor cacheAdvisor() {
      BeanFactoryCacheOperationSourceAdvisor advisor = new BeanFactoryCacheOperationSourceAdvisor();      advisor.setCacheOperationSource(cacheOperationSource());      advisor.setAdvice(cacheInterceptor());      if (this.enableCaching != null) {
        advisor.setOrder(this.enableCaching.<Integer>getNumber("order"));
      }      return advisor;
  }  @Bean
  @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
  public CacheOperationSource cacheOperationSource() {
    	return new AnnotationCacheOperationSource();
  }  @Bean
  @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
  public CacheInterceptor cacheInterceptor() {
      CacheInterceptor interceptor = new CacheInterceptor();      interceptor.setCacheOperationSources(cacheOperationSource());      if (this.cacheResolver != null) {
        interceptor.setCacheResolver(this.cacheResolver);
      }      else if (this.cacheManager != null) {
        interceptor.setCacheManager(this.cacheManager);
      }      if (this.keyGenerator != null) {
        interceptor.setKeyGenerator(this.keyGenerator);
      }      if (this.errorHandler != null) {
        interceptor.setErrorHandler(this.errorHandler);
      }      return interceptor;
  }}

 

哈哈哈,這個類里面定義了AOP的三大要素:advisor、interceptor和Pointcut,只是Pointcut是在BeanFactoryCacheOperationSourceAdvisor內部定義的。

使用spring cache讓我的接口性能瞬間提升了100倍

 

另外定義了CacheOperationSource類,該類封裝了cache方法簽名注解的解析工作,形成CacheOperation的集合。它的構造方法會實例化SpringCacheAnnotationParser,現在看看這個類的parseCacheAnnotations方法。

private Collection<CacheOperation> parseCacheAnnotations(
    DefaultCacheConfig cachingConfig, AnnotatedElement ae, boolean localOnly) {
  Collection<CacheOperation> ops = null;
  //找@cacheable注解方法
  Collection<Cacheable> cacheables = (localOnly ? AnnotatedElementUtils.getAllMergedAnnotations(ae, Cacheable.class) :
      AnnotatedElementUtils.findAllMergedAnnotations(ae, Cacheable.class));
  if (!cacheables.isEmpty()) {
    ops = lazyInit(null);
    for (Cacheable cacheable : cacheables) {
      ops.add(parseCacheableAnnotation(ae, cachingConfig, cacheable));
    }
  }
   //找@cacheEvict注解的方法
  Collection<CacheEvict> evicts = (localOnly ? AnnotatedElementUtils.getAllMergedAnnotations(ae, CacheEvict.class) :
      AnnotatedElementUtils.findAllMergedAnnotations(ae, CacheEvict.class));
  if (!evicts.isEmpty()) {
    ops = lazyInit(ops);
    for (CacheEvict evict : evicts) {
      ops.add(parseEvictAnnotation(ae, cachingConfig, evict));
    }
  }
   //找@cachePut注解的方法
  Collection<CachePut> puts = (localOnly ? AnnotatedElementUtils.getAllMergedAnnotations(ae, CachePut.class) :
      AnnotatedElementUtils.findAllMergedAnnotations(ae, CachePut.class));
  if (!puts.isEmpty()) {
    ops = lazyInit(ops);
    for (CachePut put : puts) {
      ops.add(parsePutAnnotation(ae, cachingConfig, put));
    }
  }
 //找@Caching注解的方法
  Collection<Caching> cachings = (localOnly ? AnnotatedElementUtils.getAllMergedAnnotations(ae, Caching.class) :
      AnnotatedElementUtils.findAllMergedAnnotations(ae, Caching.class));
  if (!cachings.isEmpty()) {
    ops = lazyInit(ops);
    for (Caching caching : cachings) {
      Collection<CacheOperation> cachingOps = parseCachingAnnotation(ae, cachingConfig, caching);
      if (cachingOps != null) {
        ops.addAll(cachingOps);
      }
    }
  }
  return ops;
}

 

我們看到這個類會解析@cacheable、@cacheEvict、@cachePut 和 @Caching注解的參數,封裝到CacheOperation集合中。

 

此外,spring cache 功能的關鍵就是上面的攔截器:CacheInterceptor,它最終會調到這個方法:

 

@Nullable
private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {  // Special handling of synchronized invocation
  if (contexts.isSynchronized()) {
    CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next();
    if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) {
      Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);      Cache cache = context.getCaches().iterator().next();
      try {        return wrapCacheValue(method, cache.get(key, () -> unwrapReturnValue(invokeOperation(invoker))));
      }      catch (Cache.ValueRetrievalException ex) {        // The invoker wraps any Throwable in a ThrowableWrapper instance so we
        // can just make sure that one bubbles up the stack.
        throw (CacheOperationInvoker.ThrowableWrapper) ex.getCause();
      }
    }
    else {
      // No caching required, only call the underlying method
      return invokeOperation(invoker);
    }
  }
  // 執行@CacheEvict的邏輯,這里是當beforeInvocation為true時清緩存
  processCacheEvicts(contexts.get(CacheEvictOperation.class), true,
      CacheOperationExpressionEvaluator.NO_RESULT);
  // 獲取命中的緩存對象
  Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));
  //如果沒有命中,則生成一個put的請求
  List<CachePutRequest> cachePutRequests = new LinkedList<>();
  if (cacheHit == null) {
    collectPutRequests(contexts.get(CacheableOperation.class),
        CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);
  }
  Object cacheValue;
  Object returnValue;
  if (cacheHit != null && !hasCachePut(contexts)) {
    // If there are no put requests, just use the cache hit
    cacheValue = cacheHit.get();
    returnValue = wrapCacheValue(method, cacheValue);
  }
  else {
    // 如果沒有獲得緩存對象,則調用業務方法獲得返回對象,這是關鍵代碼
    returnValue = invokeOperation(invoker);
    cacheValue = unwrapReturnValue(returnValue);
  }
  // 收集@CachePuts數據
  collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);
  // 執行cachePut或沒有命中的Cacheable請求,將返回對象放到緩存中
  for (CachePutRequest cachePutRequest : cachePutRequests) {
    cachePutRequest.apply(cacheValue);
  }
  // 執行@CacheEvict的邏輯,這里是當beforeInvocation為false時清緩存
  processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);
  return returnValue;
}

 

也行有些朋友看到這里會有一個疑問:

 

既然spring cache的增刪改查都有了,為啥還要 @Caching 注解呢?

 

其實是這樣的:spring考慮如果除了增刪改查之外,如果用戶需要自定義自己的注解,或者有些比較復雜的功能需要增刪改查的情況,這時就可以用@Caching 注解來實現。

 

還要一個問題:

上面的例子中使用的緩存key是#type,但是如果有些緩存key比較復雜,是實體中的幾個字段組成的,這種情況要如何定義呢?

 

一起看看下面的例子:

@Data
public class QueryCategoryModel {
    /**
     * 系統編號
     */
    private Long id;
    /**
     * 父分類編號
     */
    private Long parentId;
    /**
     * 分類名稱
     */
    private String name;
    /**
     * 分類層級
     */
    private Integer level;
    /**
     * 類型
     */
    private Integer type;
}@Cacheable(value = "category", key = "#type")
public CategoryModel getCategory2(QueryCategoryModel queryCategoryModel) {
    return getCategoryByType(queryCategoryModel.getType());
}

 

這個例子中需要用到QueryCategoryModel實體對象的所有字段,作為一個key,這種情況要如何定義呢?

1.自定義一個類實現KeyGenerator接口

public class CategoryGenerator  implements KeyGenerator {
    @Override
    public Object generate(Object target, Method method, Object... params) {
        return target.getClass().getSimpleName() + "_"
                + method.getName() + "_"
                + StringUtils.arrayToDelimitedString(params, "_");
    }}

 

2.在CacheConfig類中定義CategoryGenerator的bean實例

@Bean
public CategoryGenerator categoryGenerator() {
    return new CategoryGenerator();
}

 

3.修改之前定義的key

@Cacheable(value = "category", key = "categoryGenerator")
public CategoryModel getCategory2(QueryCategoryModel queryCategoryModel) {
    return getCategoryByType(queryCategoryModel.getType());
}

 

好了,spring cache先介紹到這里,如果這篇文檔對您有所幫助或者有所啟發的話,麻煩關注一下:蘇三說技術,或者幫忙點贊或轉發,堅持原創不易,您的支持是我堅持最大的動力。后面我會分享更多更實用的干貨,謝謝大家的支持。

分享到:
標簽:spring cache
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定