#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";
    }

}
相关文章
相关标签/搜索