简介
前面刚说到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"; } }