1. 程式人生 > >一起來學SpringBoot(十)快取的使用

一起來學SpringBoot(十)快取的使用

Spring Framework支援透明地嚮應用程式新增快取。從本質上講,抽象將快取應用於方法,從而根據快取中可用的資訊減少執行次數。快取邏輯應用透明,不會對呼叫者造成任何干擾。只要通過@EnableCaching 註釋啟用了快取支援,Spring Boot就會自動配置快取基礎結構。下面我就介紹兩個我比較常用的快取。

JSR-107

為了統一快取的開發規範,以及我們系統的擴充套件性。java釋出了JSR-107快取規範。Java Caching定義了5個核心介面,分別是CachingProvider、CacheManager、Cache和Expiry。

  • CachingProvider 定義了建立,配置,獲取,管理和控制多個CacheManager。一個應用可以在執行期間訪問多個CachingProvider。
  • CacheManager定義了建立,配置,獲取,管理和控制多個唯一命名的Cache,這些Cache存在於CacheManager 的上下文中,一個CacheManager僅被一個CachingProvider所擁有。
  • Cache是一個類似Map的資料結構,並臨時儲存Key為索引的值。一個Cache僅被一個CacheManager 所擁有。
  • Entry是一個儲存在Cache中的key-value對
  • Expirt每一個儲存在Cache中的條目有一個定義的有效期,一旦超過這個有效期,條目就為過期狀態,一旦過期,條目不可訪問,更新,和刪除。快取有效期可以通過ExpiryPolicy設定。

在這裡插入圖片描述

但是呢實現JSR107對於我們快速開發專案,遇到沒有實現JSR-107介面的功能時,此時整合難度較大,也並不是所有框架都整合JSR-107。

Spring快取抽象

所以呢我們更多使用的是Spring的快取抽象,Spring的快取抽象的概念,基本和JSR-107是通用的。Spring從3.1開始定義了Cache和CacheManager介面來同意不同的快取技術;並且支援使用JSR-107註解來簡化我們的開發。

在這裡插入圖片描述

  • Cache介面為快取的元件規範定義,包含快取的各種操作集合。
  • Cache介面下Spring提供了各種快取的實現,比如RedisCache,EhCacheCache,
  • ConcurrentMapCache等。
  • 每次呼叫需要快取功能的方法的時候,Spring會檢查制定引數的制定目標方法,是否被呼叫過,如果有,就直接從快取中獲取方法呼叫後的結果,如果沒有就呼叫方法並快取結構後返回給使用者,下次在呼叫的時候直接從快取中獲取。
  • 使用Spring快取抽象的時候我們需要注意,確定方法需要快取以及他們的快取策略,從快取中讀取之前快取儲存的資料。

快取註解

這裡列出常用的幾個概念和註解

名稱 概念
Cache 快取介面,定義快取操作,實現有RedisCache,EhCacheCache,ConcurrentMapCache等等
CacheManager 快取管理器,管理各種快取元件
@Cacheable 主要針對方法配置,能夠根據方法的請求引數對其返回的結果盡心快取
@CacheEvict 情況快取
@CachePut 保證方法被呼叫,又希望結果被快取
@EnableCaching 開啟基於快取的註解
serialize 快取資料時value序列化策略
keyGenerator 快取資料時key的生成策略
@CacheConfig 統一配置本類的快取註解的屬性

這裡列出其中幾個註解的主要引數

引數名 主要作用 栗子
value 快取的名稱,在spring配置檔案中定義,必須制定至少一個 @Cacheable(value=“mycache”) @Cacheable(value={“cache1”,“cache2”})
key 快取的key,可以為空,如果制定要按照SpEL表示式編寫,如果不制定,則按照方法的所有引數進行組合 @Cacheable(value=“mycache”,key="#userName")
condition 快取的條件,可以為空,使用SpEL編寫,返回true或者false,只有為true才能進行快取/清除操作,在呼叫方法之前之後都能進行判斷 @Cacheable(value=“mycache”,condition="#userName.length()>2")
allEntries (@CacheEvict) 是否清空所有快取內容,預設為fasle,如果指定為true,則方法呼叫後將立即清空所有快取 @CacheEvict(value=“mycache”,allEntries=true)
beforeInvocation (@CacheEvict) 是否在方法執行前就清空,預設為fasle,如果制定為true,則在方法還沒有執行的時候就會清空快取,預設情況下,如果方法執行丟擲異常,則不會清空快取 @CacheEvict(value=“mycache”,beforeInvocation=true)
unless (@CachePut)(@Cacheable) 用於否決快取的,不等同於condition,該表示式只在方法執行之後判斷,此時可以拿到返回值result進行判斷,條件為true不會快取,fasle才快取 @Cacheable(value=“mycache”,unless="#result==null")

SpEL

其中提到了SpEL,SpEL表示式可基於上下文並通過使用快取抽象,提供與root獨享相關聯的快取特定的內建引數

名稱 位置 描述 示例
methodName root物件 當前被呼叫的方法名 #root.methodname
method root物件 當前被呼叫的方法 #root.method.name
target root物件 當前被呼叫的目標物件例項 #root.target
targetClass root物件 當前被呼叫的目標物件的類 #root.targetClass
args root物件 當前被呼叫的方法的引數列表 #root.args[0]
caches root物件 當前方法呼叫使用的快取列表 #root.caches[0].name
Argument Name 執行上下文 當前被呼叫的方法的引數,如findArtisan(Artisan artisan),可以通過#artsian.id獲得引數 #artsian.id
result 執行上下文 方法執行後的返回值(僅當方法執行後的判斷有效,如 unless cacheEvict的beforeInvocation=false) #result

1.當我們要使用root物件的屬性作為key時我們也可以將“#root”省略,因為Spring預設使用的就是root物件的屬性。 如

@Cacheable(key = "targetClass + methodName +#p0")

2.使用方法引數時我們可以直接使用“#引數名”或者“#p引數index”。 如:

@Cacheable(value="users", key="#id")
@Cacheable(value="users", key="#p0")

SpEL提供了多種運算子

型別 運算子
關係 <,>,<=,>=,==,!=,lt,gt,le,ge,eq,ne
算術 +,- ,* ,/,%,^
邏輯 &&,||,!,and,or,not,between,instanceof
條件 ?: (ternary),?: (elvis)
正則表示式 matches
其他型別 ?.,?[…],![…],1,$[…]

開始使用

首先呢加入新增依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

然後在啟動或者配置類上加入 @EnableCaching註解來開啟快取註解。

建立一個Service來模擬對資料庫的操作

package com.maoxs.service;

import com.maoxs.pojo.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;

@Service
@Slf4j
public class UserService {

   public static final Map<Integer, User> users = new HashMap<>();

    static {
        users.put(1, new User("我是快樂魚"));
        users.put(2, new User( "我是憂鬱貓"));
        users.put(3, new User( "我是昴先生"));
    }
}

然後是操作的實體類

@Data
public class User implements Serializable {
    private Integer id;
    private String name;

    public User() {
    }

    public User(String name) {
        this.name = name;
    }

    public User(Integer id, String name) {
        this.id = id;
        this.name = name;
    }
}

@Cacheable

在呼叫方法之前,首先應該在快取中查詢方法的返回值,如果這個值能夠找到,就會返回快取的值。否則,這個方法就會被呼叫,返回值會放到快取之中。

@Cacheable(cacheNames = "user",key = "targetClass + methodName +#p0")
public User getUser(int id) {
    log.info("快取中沒有,從map中獲取");
    User user = users.get(id);
    return user;
}

此處的value是必需的,它指定了你的快取存放在哪塊名稱空間。

此處的key是使用的spEL表示式,參考上章。這裡有一個小坑,如果你把methodName換成method執行會報錯,觀察它們的返回型別,原因在於methodNameStringmethohMethod

此處的User實體類一定要實現序列化public class User implements Serializable,否則會報java.io.NotSerializableException異常。

到這裡,你已經可以執行程式檢驗快取功能是否實現。

試著寫一個controller 來呼叫此方法

@RequestMapping("/user/{id}")
public User getUser(@PathVariable int id) {
	return userService.getUser(id);
}

此時注意控制檯,第一次訪問的時候日誌列印 快取中沒有,從map中獲取 第二次則什麼也沒有顯示,說明此時快取已經生效了,結果是從快取中取的。預設呢是使用SimpleCacheConfiguration,它在容器中註冊了一個ConcurrentMapCacheManager,將快取資料儲存在了ConcurrentMap中。

深入原始碼,檢視它的其它屬性

我們開啟@Cacheable註解的原始碼,可以看到該註解提供的其他屬性,如:

String[] cacheNames() default {}; //和value註解差不多,二選一
String keyGenerator() default ""; //key的生成器。key/keyGenerator二選一使用
String cacheManager() default ""; //指定快取管理器
String cacheResolver() default ""; //或者指定獲取解析器
String condition() default ""; //條件符合則快取
String unless() default ""; //條件符合則不快取
boolean sync() default false; //是否使用非同步模式

這裡key中提到了keyGenerator,預設是使用SimplekeyGenerator 來生成的,他的預設策略為

如果沒有引數:key=new SimpleKey();

如果有一個引數: key=引數的值

如果有多個引數的方法: key=new SimpleKey(params);

當然你也可以按照自己的規則去生成key,這裡我自己提供了一個自定義的使用起來呢只需要在註解中加入@Cacheable(keyGenerator = "wiselyKeyGenerator") 即可。

    /**
     * 設定統一的生成key的方式
     *
     * @return
     */
    @Bean
    public KeyGenerator wiselyKeyGenerator() {
        return new KeyGenerator() {
            @Override
            public Object generate(Object target, Method method, Object... params) {
                StringBuilder sb = new StringBuilder();
                sb.append(target.getClass().getName());
                sb.append("-");
                sb.append(method.getName());
                for (Object obj : params) {
                    sb.append(obj.toString());
                }
                return sb.toString();
            }
        };
    }

@CachePut

@CachePut註解的作用 主要針對方法配置,能夠根據方法的請求引數對其結果進行快取,和 @Cacheable 不同的是,它每次都會觸發真實方法的呼叫 。簡單來說就是使用者更新快取資料。但需要注意的是該註解的valuekey 必須與要更新的快取相同,也就是與@Cacheable 相同。


@Cacheable(cacheNames = "user", key = "#id")
public User getUser(int id) {
    log.info("快取中沒有,從map中獲取");
    User user = users.get(id);
    return user;
}
@CachePut(cacheNames = "user", key = "#user.id")
public User updateUser(User user) {
    users.put(user.getId(), user);
    return user;
}

弄個controller測試下

@RequestMapping("/user/{id}")
public User getUser(@PathVariable int id) {
	return userService.getUser(id);
}
@RequestMapping("/user/{id}/{name}")
public User updateUser(@PathVariable int id, @PathVariable String name) {
	User user = new User(id, name);
	return userService.updateUser(user);
}

首先呢按id查詢一個user 然後通過url更新這個使用者,在根據id訪問下這個使用者,這是注意日誌是不是沒有列印

快取中沒有,從map中獲取 沒有列印則快取更新成功

檢視它的其它屬性

String[] cacheNames() default {}; //與value二選一
String keyGenerator() default "";  //key的生成器。key/keyGenerator二選一使用
String cacheManager() default "";  //指定快取管理器
String cacheResolver() default ""; //或者指定獲取解析器
String condition() default ""; //條件符合則快取
String unless() default ""; //條件符合則不快取

@CacheEvict

@CachEvict 的作用 主要針對方法配置,能夠根據一定的條件對快取進行清空 。

這裡需要注意兩個屬性

屬性 解釋 示例
allEntries 是否清空所有快取內容,預設為 false,如果指定為 true,則方法呼叫後將立即清空所有快取 @CachEvict(value=”testcache”,allEntries=true)
beforeInvocation 是否在方法執行前就清空,預設為 false,如果指定為 true,則在方法還沒有執行的時候就清空快取,預設情況下,如果方法執行丟擲異常,則不會清空快取 @CachEvict(value=”testcache”,beforeInvocation=true)

給個栗子


    @Cacheable(cacheNames = "user", key = "#id")
    public User getUser(int id) {
        log.info("快取中沒有,從map中獲取");
        User user = users.get(id);
        return user;
    }

    //清除一條快取,key為要清空的資料
    @CacheEvict(value = "user", key = "#id")
    public void delect(int id) {
        users.remove(id);
    }

    //方法呼叫後清空所有快取
    @CacheEvict(value = "accountCache", allEntries = true)
    public void delectAll() {
        users.clear();
    }

    //方法呼叫前清空所有快取
    @CacheEvict(value = "accountCache", beforeInvocation = true)
    public void delectAllBefore() {
        users.clear();
    }

其他屬性

String[] cacheNames() default {}; //與value二選一
String keyGenerator() default "";  //key的生成器。key/keyGenerator二選一使用
String cacheManager() default "";  //指定快取管理器
String cacheResolver() default ""; //或者指定獲取解析器
String condition() default ""; //條件符合則清空

@CacheConfig

當我們需要快取的地方越來越多,你可以使用@CacheConfig(cacheNames = {"myCache"})註解來統一指定value的值,這時可省略value,如果你在你的方法依舊寫上了value,那麼依然以方法的value值為準。

@Service
@Slf4j
@CacheConfig(cacheNames = {"user"})
public class UserService {
//    @Cacheable(cacheNames = "user", key = "#id")
    @Cacheable(key = "#id")
    public User getUser(int id) {
        log.info("快取中沒有,從map中獲取");
        User user = users.get(id);
        return user;
    }
}    

檢視它的其它屬性

String keyGenerator() default "";  //key的生成器。key/keyGenerator二選一使用
String cacheManager() default "";  //指定快取管理器
String cacheResolver() default ""; //或者指定獲取解析器

@Caching

有時候我們可能組合多個Cache註解使用,此時就需要@Caching組合多個註解標籤了。

@Caching(cacheable = {
            @Cacheable(value = "emp",key = "#p0"),
            ...
    },
    put = {
            @CachePut(value = "emp",key = "#p0"),
            ...
    },evict = {
            @CacheEvict(value = "emp",key = "#p0"),
            ....
    })
    public User save(User user) {
        ....
    }

整合EHCACHE3.x

Ehcache是一種廣泛使用的開源Java分散式快取。主要面向通用快取,Java EE和輕量級容器。它具有記憶體和磁碟儲存,快取載入器,快取擴充套件,快取異常處理程式,一個gzip快取servlet過濾器,支援REST和SOAP api等特點。ehcache3.x與2.x的差距還是非常大的,主要區別在於3.x後使用了java的快取規範JSR107!!!

依賴

引入jar包

<!-- JSR107 API -->
<dependency>
    <groupId>javax.cache</groupId>
    <artifactId>cache-api</artifactId>
</dependency>
<dependency>
    <groupId>org.ehcache</groupId>
    <artifactId>ehcache</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

yml配置

需要說明的是預設路徑為config: classpath:/ehcache.xml 入過在這個目錄下這個配置可以不用寫,但ehcache.xml必須有。

spring:
  cache:
    type: jcache
    jcache:
      config: classpath:/cache/ehcache.xml

配置檔案

在resources的cache目錄下新建ehcache.xml

<?xml version="1.0" encoding="UTF-8"?>
<config
    xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
    xmlns='http://www.ehcache.org/v3'
    xmlns:jsr107='http://www.ehcache.org/v3/jsr107'
    xsi:schemaLocation="
        http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.0.xsd
        http://www.ehcache.org/v3/jsr107 http://www.ehcache.org/schema/ehcache-107-ext-3.0.xsd">

    <cache-template name="heap-cache">
        <resources>
            <heap unit="entries">2000</heap>
            <offheap unit="MB">100</offheap>
        </resources>
    </cache-template>

    <cache alias="myuser" uses-template="heap-cache">
        <expiry>
            <ttl unit="seconds">40</ttl>
        </expiry>
    </cache>

</config>

然後呢使用的時候@CacheConfig(cacheNames = {"myuser"}) 中的cacheNames 的名字,xml中的alias必須也有,不然會報找不到快取名。

整合EHCACHE2.x

整合原理跟ehcache3.x一樣,需要稍微改動下

依賴

 <dependency>
     <groupId>net.sf.ehcache</groupId>
     <artifactId>ehcache</artifactId>
 </dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

yml配置

spring:
  cache:
    type: ehcache
    ehcache:
      config: classpath:/cache/ehcache.xml

配置檔案

<ehcache>
    <!--
        磁碟儲存:將快取中暫時不使用的物件,轉移到硬碟,類似於Windows系統的虛擬記憶體
        path:指定在硬碟上儲存物件的路徑
        path可以配置的目錄有:
            user.home(使用者的家目錄)
            user.dir(使用者當前的工作目錄)
            java.io.tmpdir(預設的臨時目錄)
            ehcache.disk.store.dir(ehcache的配置目錄)
            絕對路徑(如:d:\\ehcache)
        檢視路徑方法:String tmpDir = System.getProperty("java.io.tmpdir");
     -->
    <diskStore path="java.io.tmpdir" />
    <!--
        defaultCache:預設的快取配置資訊,如果不加特殊說明,則所有物件按照此配置項處理
        maxElementsInMemory:設定了快取的上限,最多儲存多少個記錄物件
        eternal:代表物件是否永不過期 (指定true則下面兩項配置需為0無限期)
        timeToIdleSeconds:最大的發呆時間 /秒
        timeToLiveSeconds:最大的存活時間 /秒
        overflowToDisk:是否允許物件被寫入到磁碟
        說明:下列配置自快取建立起600秒(10分鐘)有效 。
        在有效的600秒(10分鐘)內,如果連續120秒(2分鐘)未訪問快取,則快取失效。
        就算有訪問,也只會存活600秒。
     -->
    <defaultCache maxElementsInMemory="10000" eternal="false"
                  timeToIdleSeconds="600" timeToLiveSeconds="600" overflowToDisk="true" />
    <cache name="myCache" maxElementsInMemory="10000" eternal="false"
                  timeToIdleSeconds="120" timeToLiveSeconds="600" overflowToDisk="true" />
</ehcache>

同樣呢也是這樣使用@CacheConfig(cacheNames = {"myCache"}) 中的cacheNames 的名字,xml中的alias必須也有,不然會報找不到快取名。

整合Redis

  • 效能極高 – Redis能讀的速度是110000次/s,寫的速度是81000次/s 。
  • 豐富的資料型別 – Redis支援二進位制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 資料型別操作。
  • 原子 – Redis的所有操作都是原子性的,意思就是要麼成功執行要麼失敗完全不執行。單個操作是原子性的。多個操作也支援事務,即原子性,通過MULTI和EXEC指令包起來。
  • 豐富的特性 – Redis還支援 publish/subscribe, 通知, key 過期等等特性

依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

當你匯入這一個依賴時,SpringBoot的CacheManager就會使用RedisCache。

存入redis呢預設的快取序列化策略為jdk序列化如果想更改怎麼辦呢,這裡呢我們注入了一個RedisTemplate 設定了裡面的序列化,然後呢把他注入到redisCacheManger裡就可以了。

    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<Object,