#Caffeine 實現緩存機制

簡介

前面剛說到Guava Cache,他的優勢是封裝了get,put操做;提供線程安全的緩存操做;提供過時策略;提供回收策略;緩存監控。當緩存的數據超過最大值時,使用LRU算法替換。這一篇咱們將要談到一個新的本地緩存框架:Caffeine Cache。它也是站在巨人的肩膀上-Guava Cache,藉着他的思想優化了算法發展而來。java

按 Caffeine Github 文檔描述,Caffeine 是基於 JAVA 8 的高性能緩存庫。而且在 spring5 (springboot 2.x) 後,spring 官方放棄了 Guava,而使用了性能更優秀的 Caffeine 做爲默認緩存組件。git

Caffine Cache

Caffeine Cache:https://github.com/ben-manes/caffeinegithub

1. 緩存填充策略

Caffeine Cache提供了三種緩存填充策略:手動、同步加載和異步加載。web

手動加載

在每次get key的時候指定一個同步的函數,若是key不存在就調用這個函數生成一個值。算法

/**
     * 手動加載
     * @param key
     * @return
     */
public Object manulOperator(String key) {
    Cache<String, Object> cache = Caffeine.newBuilder()
        .expireAfterWrite(1, TimeUnit.SECONDS)
        .expireAfterAccess(1, TimeUnit.SECONDS)
        .maximumSize(10)
        .build();
    //若是一個key不存在,那麼會進入指定的函數生成value
    Object value = cache.get(key, t -> setValue(key).apply(key));
    cache.put("hello",value);

    //判斷是否存在若是不存返回null
    Object ifPresent = cache.getIfPresent(key);
    //移除一個key
    cache.invalidate(key);
    return value;
}

public Function<String, Object> setValue(String key){
    return t -> key + "value";
}

同步加載

構造Cache時候,build方法傳入一個CacheLoader實現類。實現load方法,經過key加載value。spring

/**
     * 同步加載
     * @param key
     * @return
     */
public Object syncOperator(String key){
    LoadingCache<String, Object> cache = Caffeine.newBuilder()
        .maximumSize(100)
        .expireAfterWrite(1, TimeUnit.MINUTES)
        .build(k -> setValue(key).apply(key));
    return cache.get(key);
}

public Function<String, Object> setValue(String key){
    return t -> key + "value";
}

異步加載

AsyncLoadingCache是繼承自LoadingCache類的,異步加載使用Executor去調用方法並返回一個CompletableFuture。異步加載緩存使用了響應式編程模型。數據庫

若是要以同步方式調用時,應提供CacheLoader。要以異步表示時,應該提供一個AsyncCacheLoader,並返回一個CompletableFuture。編程

/**
 * 異步加載
 * @param key
 * @return
*/
public Object asyncOperator(String key){
    AsyncLoadingCache<String, Object> cache = Caffeine.newBuilder()
        .maximumSize(100)
        .expireAfterWrite(1, TimeUnit.MINUTES)
        .buildAsync(k -> setAsyncValue(key).get());

    return cache.get(key);
}

public CompletableFuture<Object> setAsyncValue(String key){
    return CompletableFuture.supplyAsync(() -> {
        return key + "value";
    });
}

2. 回收策略

Caffeine提供了3種回收策略:基於大小回收,基於時間回收,基於引用回收。緩存

基於大小的過時方式

基於大小的回收策略有兩種方式:一種是基於緩存大小,一種是基於權重。安全

// 根據緩存的計數進行驅逐
LoadingCache<String, Object> cache = Caffeine.newBuilder()
    .maximumSize(10000)
    .build(key -> function(key));


// 根據緩存的權重來進行驅逐(權重只是用於肯定緩存大小,不會用於決定該緩存是否被驅逐)
LoadingCache<String, Object> cache1 = Caffeine.newBuilder()
    .maximumWeight(10000)
    .weigher(key -> function1(key))
    .build(key -> function(key));
maximumWeight與maximumSize不能夠同時使用。

基於時間的過時方式

// 基於固定的到期策略進行退出
LoadingCache<String, Object> cache = Caffeine.newBuilder()
    .expireAfterAccess(5, TimeUnit.MINUTES)
    .build(key -> function(key));
LoadingCache<String, Object> cache1 = Caffeine.newBuilder()
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .build(key -> function(key));

// 基於不一樣的到期策略進行退出
LoadingCache<String, Object> cache2 = Caffeine.newBuilder()
    .expireAfter(new Expiry<String, Object>() {
        @Override
        public long expireAfterCreate(String key, Object value, long currentTime) {
            return TimeUnit.SECONDS.toNanos(seconds);
        }

        @Override
        public long expireAfterUpdate(@Nonnull String s, @Nonnull Object o, long l, long l1) {
            return 0;
        }

        @Override
        public long expireAfterRead(@Nonnull String s, @Nonnull Object o, long l, long l1) {
            return 0;
        }
    }).build(key -> function(key));

Caffeine提供了三種定時驅逐策略:

  • expireAfterAccess(long, TimeUnit):在最後一次訪問或者寫入後開始計時,在指定的時間後過時。假如一直有請求訪問該key,那麼這個緩存將一直不會過時。
  • expireAfterWrite(long, TimeUnit):在最後一次寫入緩存後開始計時,在指定的時間後過時。
  • expireAfter(Expiry):自定義策略,過時時間由Expiry實現獨自計算。

緩存的刪除策略使用的是惰性刪除和定時刪除。這兩個刪除策略的時間複雜度都是O(1)。

基於引用的過時方式

// 當key和value都沒有引用時驅逐緩存
LoadingCache<String, Object> cache = Caffeine.newBuilder()
    .weakKeys()
    .weakValues()
    .build(key -> function(key));

// 當垃圾收集器須要釋放內存時驅逐
LoadingCache<String, Object> cache1 = Caffeine.newBuilder()
    .softValues()
    .build(key -> function(key));

注意:AsyncLoadingCache不支持弱引用和軟引用。

Caffeine.weakKeys(): 使用弱引用存儲key。若是沒有其餘地方對該key有強引用,那麼該緩存就會被垃圾回收器回收。因爲垃圾回收器只依賴於身份(identity)相等,所以這會致使整個緩存使用身份 (==) 相等來比較 key,而不是使用 equals()。

Caffeine.weakValues() :使用弱引用存儲value。若是沒有其餘地方對該value有強引用,那麼該緩存就會被垃圾回收器回收。因爲垃圾回收器只依賴於身份(identity)相等,所以這會致使整個緩存使用身份 (==) 相等來比較 key,而不是使用 equals()。

Caffeine.softValues() :使用軟引用存儲value。當內存滿了事後,軟引用的對象以將使用最近最少使用(least-recently-used ) 的方式進行垃圾回收。因爲使用軟引用是須要等到內存滿了才進行回收,因此咱們一般建議給緩存配置一個使用內存的最大值。 softValues() 將使用身份相等(identity) (==) 而不是equals() 來比較值。

Caffeine.weakValues()和Caffeine.softValues()不能夠一塊兒使用。

3. 移除事件監聽

Cache<String, Object> cache = Caffeine.newBuilder()
    .removalListener((String key, Object value, RemovalCause cause) ->
                     System.out.printf("Key %s was removed (%s)%n", key, cause))
    .build();

4. 寫入外部存儲

CacheWriter 方法能夠將緩存中全部的數據寫入到第三方。

LoadingCache<String, Object> cache2 = Caffeine.newBuilder()
    .writer(new CacheWriter<String, Object>() {
        @Override public void write(String key, Object value) {
            // 寫入到外部存儲
        }
        @Override public void delete(String key, Object value, RemovalCause cause) {
            // 刪除外部存儲
        }
    })
    .build(key -> function(key));
若是你有多級緩存的狀況下,這個方法仍是很實用。

注意:CacheWriter不能與弱鍵或AsyncLoadingCache一塊兒使用。

5. 統計

與Guava Cache的統計同樣。

Cache<String, Object> cache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .recordStats()
    .build();
經過使用Caffeine.recordStats(), 能夠轉化成一個統計的集合. 經過 Cache.stats() 返回一個CacheStats。CacheStats提供如下統計方法:

hitRate(): 返回緩存命中率

evictionCount(): 緩存回收數量

averageLoadPenalty(): 加載新值的平均時間

 

實例分享

SpringBoot 1.x版本中的默認本地cache是Guava Cache。在2.x(Spring Boot 2.0(spring 5) )版本中已經用Caffine Cache取代了Guava Cache。畢竟有了更優的緩存淘汰策略。

下面咱們來講在SpringBoot2.x版本中如何使用cache。

Caffeine經常使用配置說明:

initialCapacity=[integer]: 初始的緩存空間大小
maximumSize=[long]: 緩存的最大條數
maximumWeight=[long]: 緩存的最大權重
expireAfterAccess=[duration]: 最後一次寫入或訪問後通過固定時間過時
expireAfterWrite=[duration]: 最後一次寫入後通過固定時間過時
refreshAfterWrite=[duration]: 建立緩存或者最近一次更新緩存後通過固定的時間間隔,刷新緩存
weakKeys: 打開key的弱引用
weakValues:打開value的弱引用
softValues:打開value的軟引用
recordStats:開發統計功能

注意:
expireAfterWrite和expireAfterAccess同時存在時,以expireAfterWrite爲準。
maximumSize和maximumWeight不能夠同時使用
weakValues和softValues不能夠同時使用

須要說明的是,使用配置文件的方式來進行緩存項配置,通常狀況能知足使用需求,可是靈活性不是很高,若是咱們有不少緩存項的狀況下寫起來會致使配置文件很長。因此通常狀況下你也能夠選擇使用bean的方式來初始化Cache實例。

SpringBoot 有倆種使用 Caffeine 做爲緩存的方式:

  • 直接引入 Caffeine 依賴,而後使用 Caffeine 方法實現緩存。
  • 引入 Caffeine 和 Spring Cache 依賴,使用 SpringCache 註解方法實現緩存。

1. 直接引入 Caffeine 依賴

Pom

<!-- caffeine cache -->
<dependency>
	<groupId>com.github.ben-manes.caffeine</groupId>
	<artifactId>caffeine</artifactId>
	<version>2.5.5</version>
</dependency>

Conf

package com.spring.master.spring.caffeine.conf;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;

/**
 * @author Huan Lee
 * @version 1.0
 * @date 2020-09-30 15:54
 * @describtion 業精於勤,荒於嬉;行成於思,毀於隨。
 */
@Configuration
public class CaffeineCacheConfig {

    @Bean
    public Cache<String, Object> caffeineCache() {
        return Caffeine.newBuilder()
                // 設置最後一次寫入或訪問後通過固定時間過時
                .expireAfterWrite(60, TimeUnit.SECONDS)
                // 初始的緩存空間大小
                .initialCapacity(100)
                // 緩存的最大條數
                .maximumSize(1000)
                .build();
    }
}

Bean

package com.spring.master.spring.caffeine.bean;

import lombok.Data;
import lombok.ToString;

/**
 * @author Huan Lee
 * @version 1.0
 * @date 2020-09-30 15:56
 * @describtion 業精於勤,荒於嬉;行成於思,毀於隨。
 */
@Data
@ToString
public class UserInfo {

    private Integer id;
    private String name;
}

Service

package com.spring.master.spring.caffeine.service;

import com.spring.master.spring.caffeine.bean.UserInfo;

/**
 * @author Huan Lee
 * @version 1.0
 * @date 2020-09-30 15:57
 * @describtion 業精於勤,荒於嬉;行成於思,毀於隨。
 */
public interface UserInfoService {

    /**
     * 增長用戶信息
     *
     * @param userInfo 用戶信息
     */
    void addUserInfo(UserInfo userInfo);

    /**
     * 獲取用戶信息
     *
     * @param id 用戶ID
     * @return 用戶信息
     */
    UserInfo getByName(Integer id);

    /**
     * 刪除用戶信息
     *
     * @param id 用戶ID
     */
    void deleteById(Integer id);
}

Impl

package com.spring.master.spring.caffeine.impl;

import com.github.benmanes.caffeine.cache.Cache;
import com.spring.master.spring.caffeine.bean.UserInfo;
import com.spring.master.spring.caffeine.service.UserInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.HashMap;

/**
 * @author Huan Lee
 * @version 1.0
 * @date 2020-09-30 15:57
 * @describtion 業精於勤,荒於嬉;行成於思,毀於隨。
 */
@Service
@Slf4j
public class UserInfoServiceImpl implements UserInfoService {

    /**
     * 模擬數據庫存儲數據
     */
    private HashMap<Integer, UserInfo> userInfoMap = new HashMap<>();

    @Autowired
    Cache<String, Object> caffeineCache;

    @Override
    public void addUserInfo(UserInfo userInfo) {
        userInfoMap.put(userInfo.getId(), userInfo);
        // 加入緩存
        caffeineCache.put(String.valueOf(userInfo.getId()),userInfo);
    }

    @Override
    public UserInfo getByName(Integer id) {
        // 先從緩存讀取
        caffeineCache.getIfPresent(id);
        UserInfo userInfo = (UserInfo) caffeineCache.asMap().get(String.valueOf(id));
        if (userInfo != null){
            System.out.println("Hello, 我來自Caffeine Cache");
            return userInfo;
        }
        // 若是緩存中不存在,則從庫中查找
        userInfo = userInfoMap.get(id);
        System.out.println("Hello, 我來自DataBase");

        // 若是用戶信息不爲空,則加入緩存
        if (userInfo != null){
            caffeineCache.put(String.valueOf(userInfo.getId()),userInfo);
        }
        return userInfo;
    }

    @Override
    public void deleteById(Integer id) {
        userInfoMap.remove(id);
        // 從緩存中刪除
        caffeineCache.asMap().remove(String.valueOf(id));
    }
}

Controller

package com.spring.master.spring.caffeine.controller;

import com.spring.master.spring.caffeine.bean.UserInfo;
import com.spring.master.spring.caffeine.service.UserInfoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;

/**
 * @author Huan Lee
 * @version 1.0
 * @date 2020-09-30 15:59
 * @describtion 業精於勤,荒於嬉;行成於思,毀於隨。
 */
@RequestMapping(value = "/caffeine")
@RestController
public class CaffeineCacheController {

    @Autowired
    private UserInfoService userInfoService;

    @GetMapping("/userInfo/{id}")
    public Object getUserInfo(@PathVariable Integer id) {
        UserInfo userInfo = userInfoService.getByName(id);
        if (userInfo == null) {
            return "沒有該用戶";
        }
        return userInfo;
    }

    @RequestMapping(value = "/userInfo")
    public Object createUserInfo() {
        UserInfo userInfo = new UserInfo();
        userInfo.setId(1);
        userInfo.setName("HLee");
        userInfoService.addUserInfo(userInfo);
        return "SUCCESS";
    }

    @GetMapping(value = "/delete/{id}")
    public Object deleteUserInfo(@PathVariable Integer id) {
        userInfoService.deleteById(id);
        return "SUCCESS";
    }
}


啓動服務:
localhost:2000/spring-master/caffeine/userInfo
localhost:2000/spring-master/caffeine/userInfo/1
localhost:2000/spring-master/caffeine/delete/1

2. 引入 Caffeine 和 Spring Cache 依賴

Pom

<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>

Conf

@Configuration
public class CacheConfig {

    /**
     * 配置緩存管理器
     *
     * @return 緩存管理器
     */
    @Bean("caffeineCacheManager")
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        cacheManager.setCaffeine(Caffeine.newBuilder()
                // 設置最後一次寫入或訪問後通過固定時間過時
                .expireAfterAccess(60, TimeUnit.SECONDS)
                // 初始的緩存空間大小
                .initialCapacity(100)
                // 緩存的最大條數
                .maximumSize(1000));
        return cacheManager;
    }

}

Bean

package com.spring.master.spring.caffeine.bean;

import lombok.Data;
import lombok.ToString;

/**
 * @author Huan Lee
 * @version 1.0
 * @date 2020-09-30 15:56
 * @describtion 業精於勤,荒於嬉;行成於思,毀於隨。
 */
@Data
@ToString
public class UserInfo {

    private Integer id;
    private String name;
}

Service

package com.spring.master.spring.caffeine.service;

import com.spring.master.spring.caffeine.bean.UserInfo;

/**
 * @author Huan Lee
 * @version 1.0
 * @date 2020-09-30 15:57
 * @describtion 業精於勤,荒於嬉;行成於思,毀於隨。
 */
public interface UserInfoService {

    /**
     * 增長用戶信息
     *
     * @param userInfo 用戶信息
     */
    void addUserInfo(UserInfo userInfo);

    /**
     * 獲取用戶信息
     *
     * @param id 用戶ID
     * @return 用戶信息
     */
    UserInfo getByName(Integer id);

    /**
     * 刪除用戶信息
     *
     * @param id 用戶ID
     */
    void deleteById(Integer id);
}

Impl

import lombok.extern.slf4j.Slf4j;
import mydlq.club.example.entity.UserInfo;
import mydlq.club.example.service.UserInfoService;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.HashMap;

@Slf4j
@Service
@CacheConfig(cacheNames = "caffeineCacheManager")
public class UserInfoServiceImpl implements UserInfoService {

    /**
     * 模擬數據庫存儲數據
     */
    private HashMap<Integer, UserInfo> userInfoMap = new HashMap<>();

    @Override
    @CachePut(key = "#userInfo.id")
    public void addUserInfo(UserInfo userInfo) {
        userInfoMap.put(userInfo.getId(), userInfo);
    }

    @Override
    @Cacheable(key = "#id")
    public UserInfo getByName(Integer id) {
        return userInfoMap.get(id);
    }

    @Override
    @CachePut(key = "#userInfo.id")
    public UserInfo updateUserInfo(UserInfo userInfo) {
        if (!userInfoMap.containsKey(userInfo.getId())) {
            return null;
        }
        // 取舊的值
        UserInfo oldUserInfo = userInfoMap.get(userInfo.getId());
        // 替換內容
        if (!StringUtils.isEmpty(oldUserInfo.getAge())) {
            oldUserInfo.setAge(userInfo.getAge());
        }
        if (!StringUtils.isEmpty(oldUserInfo.getName())) {
            oldUserInfo.setName(userInfo.getName());
        }
        if (!StringUtils.isEmpty(oldUserInfo.getSex())) {
            oldUserInfo.setSex(userInfo.getSex());
        }
        // 將新的對象存儲,更新舊對象信息
        userInfoMap.put(oldUserInfo.getId(), oldUserInfo);
        // 返回新對象信息
        return oldUserInfo;
    }

    @Override
    @CacheEvict(key = "#id")
    public void deleteById(Integer id) {
        userInfoMap.remove(id);
    }

}

Controller

@RestController
@RequestMapping
public class UserInfoController {

    @Autowired
    private UserInfoService userInfoService;

    @GetMapping("/userInfo/{id}")
    public Object getUserInfo(@PathVariable Integer id) {
        UserInfo userInfo = userInfoService.getByName(id);
        if (userInfo == null) {
            return "沒有該用戶";
        }
        return userInfo;
    }

    @PostMapping("/userInfo")
    public Object createUserInfo(@RequestBody UserInfo userInfo) {
        userInfoService.addUserInfo(userInfo);
        return "SUCCESS";
    }

    @PostMapping("/updateInfo")
    public Object updateUserInfo(@RequestBody UserInfo userInfo) {
        UserInfo newUserInfo = userInfoService.updateUserInfo(userInfo);
        if (newUserInfo == null){
            return "不存在該用戶";
        }
        return newUserInfo;
    }

    @GetMapping("/delete/{id}")
    public Object deleteUserInfo(@PathVariable Integer id) {
        userInfoService.deleteById(id);
        return "SUCCESS";
    }

}
相關文章
相關標籤/搜索