Spring Cache緩存技術的介紹

緩存用於提高系統的性能,特別適用於一些對資源需求比較高的操做。本文介紹如何基於spring boot cache技術,使用caffeine做爲具體的緩存實現,對操做的結果進行緩存。html

demo場景

本demo將建立一個web應用,提供兩個Rest接口。一個接口用於接受查詢請求,並有條件的緩存查詢結果。另外一個接口用於獲取全部緩存的數據,用於監控緩存的內部狀態。java

使用postman調用查詢接口

能夠看到此次查詢耗時3秒左右。git

使用postman調用獲取緩存的接口

能夠看到咱們的查詢結果已被緩存。這裏將一次查詢的結果緩存了兩份,具體技術細節後面介紹。github

接下來介紹具體demo的實現過程。web

demo實現

本demo已經上傳到github,讀者能夠在github上獲取源碼redis

本demo使用Maven做爲項目構建工具。按照做者的平常編程習慣,首先建立了一個root module,用於統一管理依賴。具體的功能在子module caffeine-cache中。spring

本demo的代碼結構以下:apache

demo-spring-cache/
  |- pom.xml
  L caffeine-cache/
      |- pom.xml
      L src/
          L main/
              |- java/
              |   L heyikan
              |       |- Application.yml
              |       |- QueryController.java
              |       L QueryService.java
              L resources/
                  L application.yml

建立root module

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.heyikan.demo</groupId>
    <artifactId>demo-spring-cache</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>

    <modules>
        <module>caffeine-cache</module>
    </modules>

    <properties>
        <java.version>1.8</java.version>
        <maven.compiler.source>${java.version}</maven.compiler.source>
        <maven.compiler.target>${java.version}</maven.compiler.target>

        <spring-boot.version>2.1.3.RELEASE</spring-boot.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

</project>

root module的主要做用是統一管理依賴。當項目中有多個module的時候,做者通常會構建一個root module,而後其餘的moudule都繼承自這個module,造成一個兩級module的繼承結構。編程

網上大部分的demo,通常是直接建立目標module,且繼承自spring-boot-starter-parentspring-boot-starter-parent管理了大部分經常使用的依賴,使用這些依賴咱們不用再費心考慮版本的問題。json

可是maven是單繼承結構,繼承了spring-boot-starter-parent就沒法繼承本身項目當中的parent module(root module)。在一個多module的項目當中,module之間的相互依賴就不是spring-boot-starter-parent能預先管理的了。

因此在實際項目當中,咱們通常不會直接繼承spring-boot-starter-parent。而是經過在root module中import spring-boot-dependencies,來享受spring-boot爲咱們管理依賴的便利,同時在root module管理額外的依賴。

具體的技術細節須要讀者參考Maven的知識。做者只是闡述下這麼作的緣由,實際上跟demo自己的功能沒有多大關係。

建立目標module

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>demo-spring-cache</artifactId>
        <groupId>com.heyikan.demo</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>caffeine-cache</artifactId>

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

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

這個module主要引入了三個依賴:

  • spring-boot-starter-web
    打包了web項目的常規依賴
  • spring-boot-starter-cache
    打包了依賴功能的常規依賴
  • caffeine
    具體的依賴實現

spring cache提供了一層抽象和使用接口,底層能夠切換不一樣的cache實現,caffeine就是其中之一,且性能表現較優。

spring cache還能夠與redis集成,提供分佈式緩存的能力。

建立Application

package heyikan;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

熟悉spring-boot項目的讀者應該對此比較熟悉,spring-boot項目須要建立一個Application來啓動整個應用。

@EnableCaching註解用於啓用緩存,沒有這個註解,咱們後面的緩存功能將不會生效。

建立Controller

package heyikan;

import com.github.benmanes.caffeine.cache.Cache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.CacheManager;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Function;
import java.util.stream.Collectors;

@RestController
public class QueryController {
    @Autowired
    private QueryService queryService;

    @GetMapping("/query")
    public ResponseEntity<?> query(String keyWord) {
        String result = queryService.query(keyWord);
        return ResponseEntity.ok(result);
    }

    @Autowired
    @SuppressWarnings("all")
    private CacheManager cacheManager;

    @GetMapping("/caches")
    public ResponseEntity<?> getCache() {
        Map<String, ConcurrentMap> cacheMap = cacheManager.getCacheNames().stream()
                .collect(Collectors.toMap(Function.identity(), name -> {
                    Cache cache = (Cache) cacheManager.getCache(name).getNativeCache();
                    return cache.asMap();
                }));
        return ResponseEntity.ok(cacheMap);
    }
}

QueryController提供了兩個Rest接口,query用於模擬耗時的查詢請求,getCache用於獲取當前的緩存內容。

QueryController中引入了QueryService依賴,它是提供查詢和緩存功能的核心組件。

QueryController中引入了CacheManager依賴,它持有全部的緩存,並提供了遍歷的API。

建立緩存組件

package heyikan;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
@CacheConfig(cacheNames = {"query-result", "demo"})
public class QueryService {
    private static Logger LOG = LoggerFactory.getLogger(QueryService.class);

    @Cacheable(unless = "#result.length() > 20")
    public String query(String keyWord) {
        LOG.info("do query by keyWord: {}", keyWord);
        String queryResult = doQuery(keyWord);
        return queryResult;
    }

    private String doQuery(String keyWord) {
        try {
            Thread.sleep(3000L);
            String result = "result of " + keyWord;
            return result;
        } catch (InterruptedException e) {
            throw new IllegalStateException(e);
        }
    }
}

咱們使用@CacheConfig配置緩存,如代碼所示,數據將會同時緩存到"query-result"和"demo"中。

query方法是查詢的入口,@Cacheable註解用於表示query方法的返回結果將被放到緩存中,默認以方法的參數做爲key。

@Cacheable註解的unless屬性補充了緩存的條件,按照代碼所示,當query的返回結果其長度大於20的時候,就不會進行緩存。

doQuery方法表明實際的查詢操做,模擬耗時的查詢過程。

建立配置

application.yml文件內容以下:

spring:
  cache:
    caffeine:
      spec: maximumSize=500, expireAfterAccess=30s
logging:
  pattern:
    console: "%-5level - %msg%n"
  level:
    - error
    - heyikan=ALL

spring.cache.caffeine.spec配置了兩個緩存指標:

  • maximumSize
    配置緩存的最大容量,當快要達到容量上限的時候,緩存管理器會根據必定的策略將部分緩存項移除。
  • expireAfterAccess
    配置緩存項的過時機制,如代碼所示當緩存項被訪問後30秒將會過時,從而被移除。

技術要點

緩存的結構

在上文獲取緩存的接口中,咱們獲得的結果是:

{
    "query-result": {
        "spring": "result of spring"
    },
    "demo": {
        "spring": "result of spring"
    }
}

緩存的結構大概像Map<cacheName, Map<key, value>>,其中每一對key-value又稱爲一個緩存項。

上文中,咱們緩存組件的query方法的返回結果,就是以參數爲key,以結果爲value,構建緩存項進行緩存的。

另外,咱們配置的超時時間,也是以緩存項爲粒度進行控制的。

包含緩存項的Map咱們稱爲緩存實例,每個實例有一個實例名(cacheName)。

cache結構相關的類圖以下:

spring-cache-class.jpg

上圖簡單繪製了Spring中定義的Cache接口和caffeine中定義的Cache接口。

Spring的Cache定義了極其通用的方法,包括獲取實例名、根據緩存項的key獲取、更新和移除緩存項。

Spring並無限定緩存所使用的具體存儲結構,無論使用哪種存儲結構,在Spring的Cache中都以nativeCache進行表示,注意它是Object類型的。

caffeine的Cache接口,就是caffeine對nativeCache的又一層抽象,它提供了asMap方法能夠對緩存項進行遍歷。

使用緩存

在上文中,咱們已經簡單演示瞭如何使用緩存。除了獲取緩存以外,咱們幾乎沒有任何額外的代碼,只是在合適的地方,添加了註解,就添加了緩存的功能。

因此在平常開發中,若是咱們意識到某個操做可能會有很大開銷,不妨把它移到一個獨立的組件,實現以後根據具體狀況考慮是否爲它添加緩存。

注意:若是緩存的方法是組件內部調用的,可能沒有緩存的效果。

好比,上文中的QueryService的query方法,是由QueryController調用的,緩存生效了。若是該方法由QueryService自身的其餘方法調用,緩存無效。

在上文的demo中,咱們已經使用了一些基本的功能,還有一些經常使用的功能以下:

指定key構建規則

在上文中,咱們使用默認的規則來構建緩存項的key,即以參數keyWord做爲key。

在必要的狀況下,咱們能夠指定key構建的規則,使用spring el表達式:

@Cacheable(cacheNames="books", key="#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

@Cacheable(cacheNames="books", key="#isbn.rawNumber")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

@Cacheable(cacheNames="books", key="T(someType).hash(#isbn)")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

第一個實例,咱們使用三個參數中的其中一個來構建key。
第二個實例,咱們使用參數內部的field來構建key。
第三個實例,咱們使用靜態方法來生成key。

更多內容能夠參考Custom Key Generation Declaration

有選擇的cache

上文demo中咱們使用unless屬性對方法返回的結果進行判斷,當返回結果知足必定條件時才進行緩存。

另外,咱們還可使用condition屬性對方法的參數進行判斷:

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

上述代碼表示,只有當參數的長度小於32時,咱們纔會緩存。

更多內容能夠參考Conditional Caching

擴展閱讀

  • Spring官方demo
    這裏提供了使用默認緩存的demo,內容更加簡單,適合對spring-boot不熟悉的讀者。
  • Spring官方文檔
    這裏有對如何使用cache的詳細介紹,好比如何主動更新緩存、移除緩存,都是本demo中沒有的內容。
  • Spring Boot Caffeine Caching Example Configuration 這裏介紹瞭如何使用Caffeine緩存,本文的內容至關一部分參考了這篇文章。
相關文章
相關標籤/搜索