本地缓存框架:ConcurrentHashMap,Caffeine、GuavaCache、EhCache总结
Caffeine是一个基于Java8开发的提供了近乎最佳命中率的高性能的缓存库,支持丰富的缓存过期策略,使用的是:TinyLfu淘汰算法。
caffeine的API操作功能和Guava 是基本保持一致的。并且caffeine为了兼容之前使用Guava 的用户,做了一个Guava的Adapter可供兼容。
缓存和ConcurrentMap有点相似,区别是ConcurrentMap将会持有所有加入到缓存当中的元素,直到它们被从缓存当中手动移除。但是,Caffeine的缓存Cache 通常会被配置成自动驱逐缓存中元素,以限制其内存占用。
Caffeine 关联的缓存有三种:Cache,LoadingCache 和 AsyncLoadingCache。 springboot + CaffeineCacheManager 所使用的是 LoadingCache。
Caffeine 配置说明
| 参数 | 类型 | 描述 |
| initialCapacity | int | 初始的缓存空间大小 |
| maximumSize | long | 缓存的最大条数 |
| maximumWeight | long | 缓存的最大权重 |
| expireAfterWrite或expireAfterAccess | duration | 最后一次写入或访问后经过固定时间过期 |
| refreshAfterWrite | duration | 创建缓存或者最近一次更新缓存后经过固定的时间间隔,刷新缓存 |
| weakKeys | boolean | 打开 key 的弱引用 |
| weakValues | boolean | 打开 value 的弱引用 |
| softValues | boolean | 打开 value 的软引用 |
| recordStats | 开发统计功能 |
备注:
提供了三种缓存淘汰策略,分别是基于大小、权重、时间、引用方式、手动清除。
1、可以使用Caffeine.maximumSize(long)方法来指定缓存的最大容量。
- LoadingCache
graphs = Caffeine.newBuilder() - .maximumSize(10_000)
- .build();
2、可以使用权重的策略来进行驱逐,
- LoadingCache
graphs = Caffeine.newBuilder() - .maximumWeight(10_000)
- .build();
-
-
- Caffeine
caffeine = Caffeine.newBuilder() - .maximumWeight(30)
- .weigher((String key, Person value)-> value.getAge());
3.基于时间的方式:
1、expireAfterAccess(long, TimeUnit):在最后一次访问或者写入后开始计时,在指定的时间后过期。假如一直有请求访问该key,那么这个缓存将一直不会过期
2、expireAfterWrite(long, TimeUnit): 在最后一次写入缓存后开始计时,在指定的时间后过期
3、expireAfter(Expiry): 自定义策略,过期时间由Expiry实现独自计算。
- Caffeine
- .newBuilder()
- // 二十分钟之内没有被写入,则回收
- .expireAfterWrite(20, TimeUnit.MINUTES)
- // 二十分钟之内没有被访问,则回收
- .expireAfterAccess(20, TimeUnit.MINUTES)
- .expireAfter(new Expiry
() { - @Override
- public long expireAfterCreate(String s, UserInfo userInfo, long l) {
- if(userInfo.getAge() > 60){ //首次存入缓存后,年龄大于 60 的,过期时间为 4 秒
- return 4000000000L;
- }
- return 2000000000L; // 否则为 2 秒
- }
-
- @Override
- public long expireAfterUpdate(String s, UserInfo userInfo, long l, long l1) {
- if(userInfo.getName().equals("one")){ // 更新 one 这个人之后,过期时间为 8 秒
- return 8000000000L;
- }
- return 4000000000L; // 更新其它人后,过期时间为 4 秒
- }
-
- @Override
- public long expireAfterRead(String s, UserInfo userInfo, long l, long l1) {
- return 3000000000L; // 每次被读取后,过期时间为 3 秒
- }
- }
- )
- .build();
-
- import lombok.Data;
- import lombok.ToString;
- @Data
- @ToString
- public class UserInfo {
- private Integer id;
- private String name;
- private String sex;
- private Integer age;
- }
4.基于引用的方式:
Caffeine.newBuilder().weakKeys(); 使用弱引用存储键.当key没有其他引用时,缓存项可以被垃圾回收
Caffeine.newBuilder().weakValues(); 使用弱引用存储值.当value没有其他引用时,缓存项可以被垃圾回收
Caffeine.newBuilder().softValues(); 使用软引用存储值.按照全局最近最少使用的顺序回收。

- Caffeine
- .newBuilder()
- // 使用弱引用存储键.当key没有其他引用时,缓存项可以被垃圾回收
- .weakKeys()
- // 使用弱引用存储值.当value没有其他引用时,缓存项可以被垃圾回收
- .weakValues()
- // 使用软引用存储值.按照全局最近最少使用的顺序回收
- .softValues()
- .build();
注意:Caffeine.weakValues()和Caffeine.softValues()不可以一起使用。
5.手动清除:
- Cache
cache = Caffeine.newBuilder().maximumSize(5).build(); - // 单个清除
- cache.invalidate("key");
- // 批量清除
- cache.invalidateAll(Arrays.asList("key", "key1"));
- // 清空
- cache.invalidateAll();
caffeine主要有3种加载方式:
手动加载Cache:
- import com.github.benmanes.caffeine.cache.Cache;
- import com.github.benmanes.caffeine.cache.Caffeine;
-
- import java.util.concurrent.ConcurrentMap;
- import java.util.concurrent.TimeUnit;
- /**
- * Caffeine 手动加载
- */
- public class CaffeineDemo {
-
- public static void main(String[] args) throws InterruptedException {
- Cache
cache = Caffeine.newBuilder() - // 基于时间失效,写入之后开始计时失效
- .expireAfterWrite(2000, TimeUnit.MILLISECONDS)
- // 缓存容量
- .maximumSize(5)
- .build();
-
- // 使用java8 Lambda表达式声明一个方法,get不到缓存中的值调用这个方法运算、缓存、返回
- String value = cache.get("key", key -> key + "_" + System.currentTimeMillis());
- System.out.println(value);
-
- //让缓存到期
- Thread.sleep(2001);
- // 存在就取,不存在就返回空
- System.out.println(cache.getIfPresent("key"));
- // 重新存值
- cache.put("key", "value");
- String key = cache.get("key", keyOne -> keyOne + "_" + System.currentTimeMillis());
- System.out.println(key);
- // 获取所有值打印出来
- ConcurrentMap
concurrentMap = cache.asMap(); - System.out.println(concurrentMap);
- // 删除key
- cache.invalidate("key");
- // 获取所有值打印出来
- System.out.println(cache.asMap());
- }
- }
同步加载LoadingCache:
- import com.github.benmanes.caffeine.cache.CacheLoader;
- import com.github.benmanes.caffeine.cache.Caffeine;
- import com.github.benmanes.caffeine.cache.LoadingCache;
- import org.checkerframework.checker.nullness.qual.NonNull;
- import org.checkerframework.checker.nullness.qual.Nullable;
-
- import java.util.Arrays;
- import java.util.Map;
- import java.util.concurrent.TimeUnit;
-
- /**
- * @description Caffeine 同步加载
- */
- public class CaffenineLoadingCacheDemo {
-
- public static void main(String[] args) throws InterruptedException {
- LoadingCache
cache = Caffeine.newBuilder() - // 基于时间失效,写入之后开始计时失效
- .expireAfterWrite(2000, TimeUnit.MILLISECONDS)
- // 缓存容量
- .maximumSize(5)
- // 可以使用java8函数式接口的方式,这里其实是重写CacheLoader类的load方法
- .build(new MyLoadingCache());
-
- // 获取一个不存在的kay,让它去调用CacheLoader的load方法
- System.out.println(cache.get("key"));
- // 等待2秒让key失效
- TimeUnit.SECONDS.sleep(2);
- System.out.println(cache.getIfPresent("key"));
- // 批量获取key,让他批量去加载
- Map
all = cache.getAll(Arrays.asList("key1", "key2", "key3")); - System.out.println(all);
- }
-
- static class MyLoadingCache implements CacheLoader {
-
- @Nullable
- @Override
- public Object load(@NonNull Object key) throws Exception {
- return key + "_" + System.currentTimeMillis();
- }
- }
-
- }
异步加载AsyncLoadingCache:
- import com.github.benmanes.caffeine.cache.AsyncLoadingCache;
- import com.github.benmanes.caffeine.cache.CacheLoader;
- import com.github.benmanes.caffeine.cache.Caffeine;
- import org.checkerframework.checker.nullness.qual.NonNull;
- import org.checkerframework.checker.nullness.qual.Nullable;
-
- import java.util.Objects;
- import java.util.concurrent.CompletableFuture;
- import java.util.concurrent.ExecutionException;
- import java.util.concurrent.TimeUnit;
-
- /**
- * @description Caffeine 异步加载
- */
- public class CaffeineAsyncLoadingCacheDemo {
-
- public static void main(String[] args) throws InterruptedException, ExecutionException {
- AsyncLoadingCache
- // 基于时间失效,写入之后开始计时失效
- .expireAfterWrite(2000, TimeUnit.MILLISECONDS)
- // 缓存容量
- .maximumSize(5)
- // 可以使用java8函数式接口的方式,这里其实是重写CacheLoader的load方法
- .buildAsync(new MyCacheLoader());
-
-
- // 获取一个不存在的kay,让它异步去调用CacheLoader的load方法。这时候他会返回一个CompletableFuture
- CompletableFuture
future = cache.get("key"); - //验证
- future.thenAccept(s -> System.out.println("当前的时间为:" + System.currentTimeMillis() + " -> 异步加载的值为:" + s));
-
- // 睡眠2秒让它的key失效
- TimeUnit.SECONDS.sleep(2);
-
- // 注意:当使用getIfPresent时,也是返回的CompletableFuture
- // 因为getIfPresent从缓存中找不到是不会去运算key既不会调用(CacheLoader.load)方法
- // 所以得到的CompletableFuture可能会为null,如果想从CompletableFuture中取值的话.先判断CompletableFuture是否会为null
- CompletableFuture
completableFuture = cache.getIfPresent("key"); - if (Objects.nonNull(completableFuture)) {
- System.out.println(completableFuture.get());
- }
- }
- static class MyCacheLoader implements CacheLoader{
-
- @Nullable
- @Override
- public Object load(@NonNull Object key) throws Exception {
- return key + "_" + System.currentTimeMillis();
- }
- }
-
- }
更新策略:在设定多长时间后会自动刷新缓存。
- LoadingCache
build = Caffeine.newBuilder(). - //1s后刷新缓存
- refreshAfterWrite(1, TimeUnit.SECONDS).build(new CacheLoader
() { - @Override
- public String load(String key) {
- // TODO 此处可以查询数据库 更新该key对应的value数据
- return "1111";
- }
- });
默认是使用Caffeine自带的,也可以自己进行实现。在StatsCounter接口中,定义了需要打点的方法:
默认:
- public class CaffeineStatsCounterDemo {
- public static void main(String[] args) throws InterruptedException {
- Cache
cache = Caffeine.newBuilder() - // 基于时间失效,写入之后开始计时失效
- .expireAfterWrite(2000, TimeUnit.MILLISECONDS)
- // 缓存容量
- .maximumSize(5)
- .recordStats()
- .build();
- cache.put("key", "value");
- CacheStats cacheStats = cache.stats();
- System.out.println(cacheStats.toString());
- }
- }
自定义:
- import com.github.benmanes.caffeine.cache.RemovalCause;
- import com.github.benmanes.caffeine.cache.stats.CacheStats;
- import com.github.benmanes.caffeine.cache.stats.StatsCounter;
- import lombok.Data;
-
- import java.util.concurrent.atomic.LongAdder;
-
- /**
- * 自定义的缓存状态收集器
- */
- @Data
- public class MyStatsCounter implements StatsCounter {
-
- private final LongAdder hitCount = new LongAdder();
- private final LongAdder missCount = new LongAdder();
- private final LongAdder loadSuccessCount = new LongAdder();
- private final LongAdder loadFailureCount = new LongAdder();
- private final LongAdder totalLoadTime = new LongAdder();
- private final LongAdder evictionCount = new LongAdder();
- private final LongAdder evictionWeight = new LongAdder();
-
- public MyStatsCounter() {
- }
-
- @Override
- public void recordHits(int i) {
- hitCount.add(i);
- System.out.println("命中次数:" + i);
- }
-
- @Override
- public void recordMisses(int i) {
- missCount.add(i);
- System.out.println("未命中次数:" + i);
- }
-
- @Override
- public void recordLoadSuccess(long l) {
- loadSuccessCount.increment();
- totalLoadTime.add(l);
- System.out.println("加载成功次数:" + l);
- }
-
- @Override
- public void recordLoadFailure(long l) {
- loadFailureCount.increment();
- totalLoadTime.add(l);
- System.out.println("加载失败次数:" + l);
- }
-
- @Override
- public void recordEviction() {
- evictionCount.increment();
- System.out.println("因为缓存权重限制,执行了一次缓存清除工作");
- }
-
- @Override
- public void recordEviction(int weight, RemovalCause cause) {
- evictionCount.increment();
- evictionWeight.add(weight);
- System.out.println("因为缓存权重限制,执行了一次缓存清除工作,清除的数据的权重为:" + weight);
- }
-
- @Override
- public void recordEviction(int weight) {
- evictionCount.increment();
- evictionWeight.add(weight);
- System.out.println("因为缓存权重限制,执行了一次缓存清除工作,清除的数据的权重为:" + weight);
- }
-
- @Override
- public CacheStats snapshot() {
- return CacheStats.of(
- negativeToMaxValue(hitCount.sum()),
- negativeToMaxValue(missCount.sum()),
- negativeToMaxValue(loadSuccessCount.sum()),
- negativeToMaxValue(loadFailureCount.sum()),
- negativeToMaxValue(totalLoadTime.sum()),
- negativeToMaxValue(evictionCount.sum()),
- negativeToMaxValue(evictionWeight.sum()));
- }
- private static long negativeToMaxValue(long value) {
- return (value >= 0) ? value : Long.MAX_VALUE;
- }
- }
-
-
-
-
- MyStatsCounter myStatsCounter = new MyStatsCounter();
- Cache
cache = Caffeine.newBuilder() - // 基于时间失效,写入之后开始计时失效
- .expireAfterWrite(2000, TimeUnit.MILLISECONDS)
- // 缓存容量
- .maximumSize(5)
- .recordStats(()->myStatsCounter)
- .build();
- cache.put("one", "one");
- cache.put("two", "two");
- cache.put("three","three");
- cache.getIfPresent("ww");
- CacheStats stats = myStatsCounter.snapshot();
- Thread.sleep(1000);
- System.out.println(stats.toString());
- Caffeine
caffeine = Caffeine.newBuilder() - .maximumWeight(30)
- .removalListener((String key, UserInfo value, RemovalCause cause)->{
- System.out.println("被清除人的年龄:" + value.getAge() + "; 清除的原因是:" + cause);
- }).weigher((String key, UserInfo value)-> value.getAge());
- Cache
cache = caffeine.build(); - cache.put("one", new UserInfo(12, "1"));
- cache.put("two", new UserInfo(18, "2"));
- cache.put("one", new UserInfo(14, "3"));
- cache.invalidate("one");
- cache.put("three", new UserInfo(31, "three"));
- Thread.sleep(2000);
在Caffeine中被淘汰的原因有很多种:
Java 进程内存是有限的,不可能无限地往里面放缓存对象。这就需要有合适的淘汰算法淘汰无用的对象,为新进的对象留有空间。常见的缓存淘汰算法有 FIFO、LRU、LFU。
LRU(Least Recently Used):最近最久未使用。它是优先淘汰掉最久未访问到的数据。缺点是不能很好地应对偶然的突发流量。比如一个数据在一分钟内的前59秒访问很多次,而在最后1秒没有访问,但是有一批冷门数据在最后一秒进入缓存,那么热点数据就会被冲刷掉。

FIFO(First In First Out):先进先出。它是优先淘汰掉最先缓存的数据、是最简单的淘汰算法。缺点是如果先缓存的数据使用频率比较高的话,那么该数据就不停地进进出出,因此它的缓存命中率比较低。

LFU(Least Frequently Used):最近最少频率使用。它是优先淘汰掉最不经常使用的数据,需要维护一个表示使用频率的字段。主要有两个缺点:一、大多数据访问频率比较高,内存堪忧;二、无法合理更新新上的热点数据,比如某个数据历史较多,新旧数据一起需要操作,那内存堪忧。

Caffeine采用了一种结合LRU、LFU优点的算法:W-TinyLFU,其特点:高命中率、低内存占用。是一种为了解决传统LFU算法空间存储比较大的问题LFU算法,它可以在较大访问量的场景下近似的替代LFU的数据统计部分,它的原理有些类似BloomFilter。
BloomFilter原理:在BloomFilter中,使用一个大的bit数组用于存储所有key,每一个key通过多次不同的hash计算来映射数组的不同bit位,如果key存在将对应的bit位设置为1,这样就可以通过少量的存储空间进行大量的数据过滤。
在TinyLFU中,把多个bit位看做一个整体,用于统计一个key的使用频率,TinyFLU中的key也是通过多次不同的hash计算来映射多个不同的bit组。在读取时,取映射的所有值中的最小的值作为key的使用频率。
TinyLFU根据最大数据量设置生成一个long数组,然后将频率值保存在其中的四个long的4个bit位中(4个bit位不会大于15),取频率值时则取四个中的最小一个。
Caffeine认为频率大于15已经很高了,是属于热数据,所以它只需要4个bit位来保存,long有8个字节64位,这样可以保存16个频率。取hash值的后左移两位,然后加上hash四次,这样可以利用到16个中的13个,利用率挺高的,或许有更好的算法能将16个都利用到。
过程说明:

-
org.springframework.boot -
spring-boot-starter-cache
- @EnableCaching
- @Configuration
- public class CaffeineCacheConfig {
- /**
- * 在 springboot 中使用 CaffeineCacheManager 管理器管理 Caffeine 类型的缓存,Caffeine 类似 Cache 缓存的工厂,
- * 可以生产很多个 Cache 实例,Caffeine 可以设置各种缓存属性,这些 Cache 实例都共享 Caffeine 的缓存属性。
- * @return
- */
- @Bean(name = "caffeineCacheManager")
- public CacheManager oneHourCacheManager(){
- Caffeine caffeine = Caffeine.newBuilder()
- .initialCapacity(10) //初始大小
- .maximumSize(11) //最大大小
- //写入/更新之后1小时过期
- .expireAfterWrite(1, TimeUnit.HOURS);
-
- CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();
- caffeineCacheManager.setAllowNullValues(true);
- caffeineCacheManager.setCaffeine(caffeine);
- return caffeineCacheManager;
- }
- }
- @RestController
- public class CaffeineController {
-
- @Autowired
- private CaffeineService caffeineService;
-
- @GetMapping("/cache-Data/{key}")
- public String cacheDataL(@PathVariable String key) {
-
- return caffeineService.cacheData(key);
- }
-
- @GetMapping("/cache-put-Data/{key}")
- public String cachePutDataL(@PathVariable String key) {
-
- return caffeineService.cachePutData(key);
- }
-
-
- @GetMapping("/cache-delete-Data/{key}")
- public String cacheDelteDataL(@PathVariable String key) {
-
- return caffeineService.deleteCaffeineServiceTest(key);
- }
-
- @GetMapping("/getCaffeineServiceTest")
- public String getCaffeineServiceTest(String name,Integer age) {
-
- return caffeineService.getCaffeineServiceTest(name,age);
- }
-
- }
- import lombok.extern.slf4j.Slf4j;
- 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;
-
- /**
- * @Cacheable 触发缓存入口(这里一般放在创建和获取的方法上)
- * @CacheEvict 触发缓存的eviction(用于删除的方法上)
- * @CachePut 更新缓存且不影响方法执行(用于修改的方法上,该注解下的方法始终会被执行)
- * @Caching 将多个缓存组合在一个方法上(该注解可以允许一个方法同时设置多个注解)
- * @CacheConfig 在类级别设置一些缓存相关的共同配置(与其它缓存配合使用)
- */
- @Service
- @CacheConfig(cacheManager = "caffeineCacheManager")
- @Slf4j
- public class CaffeineService {
-
- @Cacheable(value = "data", key = "#key")
- public String cacheData(String key) {
- log.info("cacheData()方法执行");
- return getCache(key);
- }
-
- @CachePut(value = "data", key = "#key")
- public String cachePutData(String key) {
- log.info("cachePutData()方法执行");
- return "cachePutData--" + key;
- }
-
- private String getCache(String key) {
- try {
- log.info("getCache()方法执行");
- Thread.sleep(3000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- return key;
- }
-
- /**
- * CacheEvict删除key,会调用cache的evict
- * */
- @CacheEvict(cacheNames = "outLimit",key = "#name")
- public String deleteCaffeineServiceTest(String name){
- String value = name + " nihao";
- log.info("deleteCaffeineServiceTest value = {}",value);
- return value;
- }
-
- /**
- * condition条件判断是否要走缓存,无法使用方法中出现的值(返回结果等),条件为true放入缓存
- * unless是方法执行后生效,决定是否放入缓存,返回true的放缓存
- * */
- @Cacheable(cacheNames = "outLimit",key = "#name",condition = "#value != null ")
- public String getCaffeineServiceTest(String name,Integer age){
- String value = name + " nihao "+ age;
- log.info("getCaffeineServiceTest value = {}",value);
- return value;
- }
-
- }


-
com.github.ben-manes.caffeine -
caffeine -
2.9.3
- @Configuration
- public class CaffeineCacheConfig {
-
- @Bean
- public Cache
caffeineCache() { - return Caffeine.newBuilder()
- // 设置最后一次写入或访问后经过固定时间过期
- .expireAfterWrite(5000, TimeUnit.SECONDS)
- // 初始的缓存空间大小
- .initialCapacity(200)
- // 缓存的最大条数
- .maximumSize(2000)
- .build();
- }
- }
- import lombok.Data;
- import lombok.ToString;
- @Data
- @ToString
- public class UserInfo {
- private Integer id;
- private String name;
- private String sex;
- private Integer age;
-
- public UserInfo(Integer id, String name) {
- this.id = id;
- this.name = name;
- }
- }
-
- import com.caffeine.lean.entity.UserInfo;
- import com.caffeine.lean.utils.CaffeineUtils;
- import lombok.extern.slf4j.Slf4j;
- import org.apache.commons.lang3.StringUtils;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Service;
-
- import java.util.Map;
- import java.util.concurrent.ConcurrentHashMap;
-
- /**
- * 用户模块接口
- */
- @Service
- @Slf4j
- public class UserInfoService {
-
- //模拟数据库存储数据
- private Map
userInfoMap = new ConcurrentHashMap<>(); -
- @Autowired
- private CaffeineUtils caffeineUtils;
-
- public void addUserInfo(UserInfo userInfo) {
- log.info("create");
- userInfoMap.put(userInfo.getId(), userInfo);
- // 加入缓存
- caffeineUtils.putAndUpdateCache(String.valueOf(userInfo.getId()), userInfo);
- }
-
- public UserInfo getByName(Integer userId) {
- // 先从缓存读取
- UserInfo userInfo = caffeineUtils.getObjCacheByKey(String.valueOf(userId), UserInfo.class);
- if (userInfo != null) {
- return userInfo;
- }
- // 如果缓存中不存在,则从库中查找
- log.info("get");
- userInfo = userInfoMap.get(userId);
- // 如果用户信息不为空,则加入缓存
- if (userInfo != null) {
- caffeineUtils.putAndUpdateCache(String.valueOf(userInfo.getId()), userInfo);
- }
- return userInfo;
- }
-
- public UserInfo updateUserInfo(UserInfo userInfo) {
- log.info("update");
- if (!userInfoMap.containsKey(userInfo.getId())) {
- return null;
- }
- // 取旧的值
- UserInfo oldUserInfo = userInfoMap.get(userInfo.getId());
- // 替换内容
- if (StringUtils.isNotBlank(oldUserInfo.getName())) {
- oldUserInfo.setName(userInfo.getName());
- }
- if (StringUtils.isNotBlank(oldUserInfo.getSex())) {
- oldUserInfo.setSex(userInfo.getSex());
- }
- oldUserInfo.setAge(userInfo.getAge());
- // 将新的对象存储,更新旧对象信息
- userInfoMap.put(oldUserInfo.getId(), oldUserInfo);
- // 替换缓存中的值
- caffeineUtils.putAndUpdateCache(String.valueOf(oldUserInfo.getId()), oldUserInfo);
- return oldUserInfo;
- }
-
- public void deleteById(Integer id) {
- log.info("delete");
- userInfoMap.remove(id);
- // 从缓存中删除
- caffeineUtils.removeCacheByKey(String.valueOf(id));
- }
-
- }
- **
- * Caffeine缓存工具类
- * Cache 可以有的操作
- * V getIfPresent(K key) :如果缓存中 key 存在,则获取 value,否则返回 null。
- * void put( K key, V value):存入一对数据
。 - * Map
getAllPresent(Iterable> var1) :参数是一个迭代器,表示可以批量查询缓存。 - * void putAll( Map extends K, ? extends V> var1); 批量存入缓存。
- * void invalidate(K var1):删除某个 key 对应的数据。
- * void invalidateAll(Iterable> var1):批量删除数据。
- * void invalidateAll():清空缓存。
- * long estimatedSize():返回缓存中数据的个数。
- * CacheStats stats():返回缓存当前的状态指标集。
- * ConcurrentMap
asMap():将缓存中所有的数据构成一个 map。 - * void cleanUp():会对缓存进行整体的清理,比如有一些数据过期了,但是并不会立马被清除,所以执行一次 cleanUp 方法,会对缓存进行一次检查,清除那些应该清除的数据。
- * V get( K var1, Function super K, ? extends V> var2):第一个参数是想要获取的 key,第二个参数是函数
- **/
- @Component
- public class CaffeineUtils {
-
- @Autowired
- Cache
caffeineCache; -
- /**
- * 添加或更新缓存
- *
- * @param key
- * @param value
- */
- public void putAndUpdateCache(String key, Object value) {
- caffeineCache.put(key, value);
- }
-
-
- /**
- * 获取对象缓存
- *
- * @param key
- * @return
- */
- public
T getObjCacheByKey(String key, Class t) { - caffeineCache.getIfPresent(key);
- return (T) caffeineCache.asMap().get(key);
- }
-
- /**
- * 根据key删除缓存
- *
- * @param key
- */
- public void removeCacheByKey(String key) {
- // 从缓存中删除
- caffeineCache.asMap().remove(key);
- }
- }
- import com.caffeine.lean.entity.UserInfo;
- import com.caffeine.lean.service.UserInfoService;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.web.bind.annotation.*;
-
- /**
- * 用户模块入口
- */
- @RestController
- @RequestMapping
- public class UserInfoController {
-
- @Autowired
- private UserInfoService userInfoService;
-
- @GetMapping("/getUserInfo/{id}")
- public Object getUserInfo(@PathVariable Integer id) {
- UserInfo userInfo = userInfoService.getByName(id);
- if (userInfo == null) {
- return "没有该用户";
- }
- return userInfo;
- }
-
- @PostMapping("/createUserInfo")
- public Object createUserInfo(@RequestBody UserInfo userInfo) {
- userInfoService.addUserInfo(userInfo);
- return "SUCCESS";
- }
-
- @PutMapping("/updateUserInfo")
- public Object updateUserInfo(@RequestBody UserInfo userInfo) {
- UserInfo newUserInfo = userInfoService.updateUserInfo(userInfo);
- if (newUserInfo == null) {
- return "不存在该用户";
- }
- return newUserInfo;
- }
-
- @DeleteMapping("/deleteUserInfo/{id}")
- public Object deleteUserInfo(@PathVariable Integer id) {
- userInfoService.deleteById(id);
- return "SUCCESS";
- }
-
- }


Google Guava Cache是本地缓存,提供了基于容量、时间、引用的缓存回收方式,内部实现采用LRU算法,基于引用回收很好的利用了java虚拟机的垃圾回收机制.
设计灵感来源ConcurrentHashMap,使用多个segments方式的细粒度锁,在保证线程安全的同时,支持高并发场景需求,同时支持多种类型的缓存清理策略,包括基于容量的清理、基于时间的清理、基于引用的清理等。
Guava Cache和ConcurrentHashMap很相似,但也不完全一样.最基本的区别是ConcurrentHashMap会一直保存所有添加的元素,直至显示地移除.Guava Cache为了限制内存占用,通常都设定为自动回收元素。
注意:springboot 2.x以上就踢出了guava cache.
自动将节点加载至缓存结构中,当缓存的数据超过最大值时,使用LRU算法替换;它具备根据节点上一次被访问或写入时间计算缓存过期机制,缓存的key被封装在WeakReference引用中,缓存的value被封装在WeakReference或SoftReference引用中;还可以统计缓存使用过程中的命中率、异常率和命中率等统计数据。
Guava Cache实质是一个在本地缓存KV数据的LocalCache。而LocalCache的数据结构和ConcurrentHashMap一样,都是采用分segment来细化管理HashMap中的节点Entry。不同的是LocalCahce中的ReferenceEntry节点更为复杂。
1.CacheLoader
- //LoadingCache类型的缓存,可以使用get(K)或get(K,Callable)方法,并且如果使用的是get(K,Callable)方法,
- // 当K值不存在时,使用的是Callable计算值,不走load方法计算,然后将值放入缓存。
- LoadingCache
cache = CacheBuilder - .newBuilder().maximumSize(100)
- .expireAfterWrite(100, TimeUnit.SECONDS) // 根据写入时间过期
- .build(new CacheLoader
() { - @Override
- public String load(String key) {
- return getSchema(key);
- }
- });
-
- private static String getSchema(String key) {
- System.out.println("load...");
- return key + "schema";
- }
2.callable
- //Cache类型的缓存只能使用Callable的方式get(K,Callable)方法
- Cache
cache2 = CacheBuilder.newBuilder() - .maximumSize(1000)
- .expireAfterWrite(100, TimeUnit.SECONDS) // 根据写入时间过期
- .build();
- try {
- String value = cache2.get("key4", new Callable
() { - @Override
- public String call() throws Exception {
- System.out.println("i am callable...");
- return "i am callable...";
- }
- });
- System.out.println(value);
- } catch (ExecutionException e1) {
- e1.printStackTrace();
- }
- private static String getSchema(String key) {
- System.out.println("load...");
- return key + "schema";
- }
提供了三种缓存淘汰策略,分别是基于大小、权重、时间、引用方式、手动清除。
基于大小的方式:
1、可以使用CacheBuilder.maximumSize(long)方法来指定缓存的最大容量。
- CacheBuilder.newBuilder()
- // 缓存的最大条数
- .maximumSize(1000)
- .build();
2、可以使用权重的策略来进行驱逐,
- CacheBuilder.newBuilder()
- .maximumWeight(10_000)
- .build();
-
-
- CacheBuilder.newBuilder()
- .maximumWeight(30)
- .weigher((String key, UserInfo value)-> value.getAge());
3.基于时间的方式:
1、expireAfterAccess(long, TimeUnit):在最后一次访问或者写入后开始计时,在指定的时间后过期。假如一直有请求访问该key,那么这个缓存将一直不会过期
2、expireAfterWrite(long, TimeUnit): 在最后一次写入缓存后开始计时,在指定的时间后过期。
注意:expireAfterWrite时guava重新加载数据时使用的是load方法,不会调用loadAll。
- CacheBuilder
- .newBuilder()
- // 二十分钟之内没有被写入,则回收
- .expireAfterWrite(20, TimeUnit.MINUTES)
- // 二十分钟之内没有被访问,则回收
- .expireAfterAccess(20, TimeUnit.MINUTES)
- .build();
注:Guava Cache不会专门维护一个线程来回收这些过期的缓存项,只有在读/写访问时,才去判断该缓存项是否过期,如果过期,则会回收。而且注意,回收后会同步调用load方法来加载新值到cache中。
4.基于引用的方式:
CacheBuilder.newBuilder().weakKeys(); 使用弱引用存储键.当key没有其他引用时,缓存项可以被垃圾回收
CacheBuilder.newBuilder().weakValues(); 使用弱引用存储值.当value没有其他引用时,缓存项可以被垃圾回收
CacheBuilder.newBuilder().softValues(); 使用软引用存储值.按照全局最近最少使用的顺序回收。

- CacheBuilder
- .newBuilder()
- // 使用弱引用存储键.当key没有其他引用时,缓存项可以被垃圾回收
- .weakKeys()
- // 使用弱引用存储值.当value没有其他引用时,缓存项可以被垃圾回收
- .weakValues()
- // 使用软引用存储值.按照全局最近最少使用的顺序回收
- .softValues().build();
注意:CacheBuilder.weakValues()和CacheBuilder.softValues()不可以一起使用。
5.手动清除:
- Cache
cache = CacheBuilder.newBuilder().maximumSize(5).build(); - // 单个清除
- cache.invalidate("key");
- // 批量清除
- cache.invalidateAll(Arrays.asList("key", "key1"));
- // 清空
- cache.invalidateAll();
在guava cache中移除key可以设置相应得监听操作,以便key被移除时做一些额外操作。缓存项被移除时,RemovalListener会获取移除通知[RemovalNotification],其中包含移除原因[RemovalCause]、键和值。监听有同步监听和异步监听两种 :
同步监听:
- CacheLoader
cacheLoader = CacheLoader.from(String::toUpperCase); - LoadingCache
loadingCache = CacheBuilder.newBuilder().maximumSize(4) - .removalListener((notification) -> {
- if (notification.wasEvicted()) {
- RemovalCause cause = notification.getCause();
- System.out.println("remove cacase is:" + cause.toString());
- System.out.println("key:" + notification.getKey() + "value:" + notification.getValue());
- }
- }).build(cacheLoader);
-
- loadingCache.getUnchecked("a");
- loadingCache.getUnchecked("b");
- loadingCache.getUnchecked("c");
- loadingCache.getUnchecked("d");//容量是4 吵了自动清除,监听程序清除是单线程。
- loadingCache.getUnchecked("e");
异步监听:
- CacheLoader
cacheLoader = CacheLoader.from(String::toUpperCase); - RemovalListener
listener =(notification) -> { - if (notification.wasEvicted()) {
- RemovalCause cause = notification.getCause();
- System.out.println("remove cacase is:" + cause.toString());
- System.out.println("key:" + notification.getKey() + "value:" + notification.getValue());
- }
- };
- //同步监听
- LoadingCache
loadingCache = CacheBuilder.newBuilder().maximumSize(4) - //异步监控
- .removalListener(RemovalListeners.asynchronous(listener, Executors.newSingleThreadExecutor()))
- .build(cacheLoader);
-
- loadingCache.getUnchecked("a");
- loadingCache.getUnchecked("b");
- loadingCache.getUnchecked("c");
- loadingCache.getUnchecked("d");//容量是4 吵了自动清除,监听程序清除是单线程。
- loadingCache.getUnchecked("e");
CacheBuilder.recordStats()用来开启Guava Cache的统计功能。统计打开后Cache.stats()方法返回如下统计信息:
- Cache
cache = CacheBuilder.newBuilder() - // 基于时间失效,写入之后开始计时失效
- .expireAfterWrite(2000, TimeUnit.MILLISECONDS)
- // 缓存容量
- .maximumSize(5)
- .recordStats()
- .build();
- cache.put("key", "value");
- CacheStats cacheStats = cache.stats();
- System.out.println(cacheStats.toString());
GuavaCache通过设置 concurrencyLevel 使得缓存支持并发的写入和读取
- LoadingCache
build = CacheBuilder.newBuilder() - // 最大3个 同时支持CPU核数线程写缓存
- .maximumSize(3).concurrencyLevel(Runtime.getRuntime().availableProcessors()).build(new CacheLoader
() { - @Override
- public UserInfo load(Long id) throws Exception {
- // 读取 db 数据
- return null;
- }
- });
更新策略:在设定多长时间后会自动刷新缓存。
- LoadingCache
build = CacheBuilder.newBuilder(). - //1s内阻塞会返回旧数据
- refreshAfterWrite(1, TimeUnit.SECONDS).build(new CacheLoader
() { - @Override
- public String load(String key) {
- // TODO 此处可以查询数据库 更新该key对应的value数据
- return null;
- }
- });
案例:
使用方式一、
-
com.google.guava -
guava -
27.0.1-jre
- import com.google.common.cache.Cache;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Component;
- @Component
- public class GuavaCacheUtils {
-
- @Autowired
- Cache
guavaCache; -
- /**
- * 添加或更新缓存
- *
- * @param key
- * @param value
- */
- public void putAndUpdateCache(String key, Object value) {
- guavaCache.put(key, value);
- }
-
-
- /**
- * 获取对象缓存
- *
- * @param key
- * @return
- */
- public
T getObjCacheByKey(String key, Class t) { - //通过key获取缓存中的value,若不存在直接返回null
- guavaCache.getIfPresent(key);
- return (T) guavaCache.asMap().get(key);
- }
-
- /**
- * 根据key删除缓存
- *
- * @param key
- */
- public void removeCacheByKey(String key) {
- // 从缓存中删除
- guavaCache.asMap().remove(key);
- }
-
- }
- import com.guava.cache.lean.entity.UserInfo;
- import com.guava.cache.lean.utils.GuavaCacheUtils;
- import lombok.extern.slf4j.Slf4j;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Service;
- import org.springframework.util.StringUtils;
-
- import java.util.Map;
- import java.util.concurrent.ConcurrentHashMap;
-
- /**
- * 用户模块接口
- */
- @Service
- @Slf4j
- public class UserInfoService {
-
- //模拟数据库存储数据
- private Map
userInfoMap = new ConcurrentHashMap<>(); -
- @Autowired
- private GuavaCacheUtils guavaCacheUtils;
-
- public void addUserInfo(UserInfo userInfo) {
- log.info("create");
- userInfoMap.put(userInfo.getId(), userInfo);
- // 加入缓存
- guavaCacheUtils.putAndUpdateCache(String.valueOf(userInfo.getId()), userInfo);
- }
-
- public UserInfo getByName(Integer userId) {
- // 先从缓存读取
- UserInfo userInfo = guavaCacheUtils.getObjCacheByKey(String.valueOf(userId), UserInfo.class);
- if (userInfo != null) {
- return userInfo;
- }
- // 如果缓存中不存在,则从库中查找
- log.info("get");
- userInfo = userInfoMap.get(userId);
- // 如果用户信息不为空,则加入缓存
- if (userInfo != null) {
- guavaCacheUtils.putAndUpdateCache(String.valueOf(userInfo.getId()), userInfo);
- }
- return userInfo;
- }
-
- public UserInfo updateUserInfo(UserInfo userInfo) {
- log.info("update");
- if (!userInfoMap.containsKey(userInfo.getId())) {
- return null;
- }
- // 取旧的值
- UserInfo oldUserInfo = userInfoMap.get(userInfo.getId());
- // 替换内容
- if (!StringUtils.isEmpty(oldUserInfo.getName())) {
- oldUserInfo.setName(userInfo.getName());
- }
- if (!StringUtils.isEmpty(oldUserInfo.getSex())) {
- oldUserInfo.setSex(userInfo.getSex());
- }
- oldUserInfo.setAge(userInfo.getAge());
- // 将新的对象存储,更新旧对象信息
- userInfoMap.put(oldUserInfo.getId(), oldUserInfo);
- // 替换缓存中的值
- guavaCacheUtils.putAndUpdateCache(String.valueOf(oldUserInfo.getId()), oldUserInfo);
- return oldUserInfo;
- }
-
- public void deleteById(Integer id) {
- log.info("delete");
- userInfoMap.remove(id);
- // 从缓存中删除
- guavaCacheUtils.removeCacheByKey(String.valueOf(id));
- }
-
- }
- import com.guava.cache.lean.entity.UserInfo;
- import com.guava.cache.lean.service.UserInfoService;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.web.bind.annotation.*;
-
- /**
- * 用户模块入口
- */
- @RestController
- @RequestMapping
- public class UserInfoController {
-
- @Autowired
- private UserInfoService userInfoService;
-
- @GetMapping("/getUserInfo/{id}")
- public Object getUserInfo(@PathVariable Integer id) {
- UserInfo userInfo = userInfoService.getByName(id);
- if (userInfo == null) {
- return "没有该用户";
- }
- return userInfo;
- }
-
- @PostMapping("/createUserInfo")
- public Object createUserInfo(@RequestBody UserInfo userInfo) {
- userInfoService.addUserInfo(userInfo);
- return "SUCCESS";
- }
-
- @PutMapping("/updateUserInfo")
- public Object updateUserInfo(@RequestBody UserInfo userInfo) {
- UserInfo newUserInfo = userInfoService.updateUserInfo(userInfo);
- if (newUserInfo == null) {
- return "不存在该用户";
- }
- return newUserInfo;
- }
-
- @DeleteMapping("/deleteUserInfo/{id}")
- public Object deleteUserInfo(@PathVariable Integer id) {
- userInfoService.deleteById(id);
- return "SUCCESS";
- }
-
- }
- import com.google.common.cache.*;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
- import java.util.concurrent.TimeUnit;
-
- @EnableCaching
- @Configuration
- public class GuavaCacheConfig {
- @Bean
- public Cache
guavaCache() { - return CacheBuilder.newBuilder()
- // 设置最后一次写入或访问后经过固定时间过期
- .expireAfterWrite(6000, TimeUnit.SECONDS)
- // 初始的缓存空间大小
- .initialCapacity(100)
- // 缓存的最大条数
- .maximumSize(1000)
- .build();
- }
-
- }
使用方式二、
-
-
org.springframework.boot -
spring-boot-starter-cache -
-
-
-
-
com.google.guava -
guava -
27.0.1-jre -
- import com.google.common.cache.*;
- import org.springframework.cache.CacheManager;
- import org.springframework.cache.annotation.EnableCaching;
- import org.springframework.cache.guava.GuavaCacheManager;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
- import java.util.concurrent.TimeUnit;
-
- @EnableCaching
- @Configuration
- public class GuavaCacheConfig {
- @Bean(name = "guavaCacheManager")
- public CacheManager oneHourCacheManager(){
- CacheBuilder cacheBuilder = CacheBuilder.newBuilder()
- .initialCapacity(10) //初始大小
- .maximumSize(11) //最大大小
- //写入/更新之后1小时过期
- .expireAfterWrite(1, TimeUnit.HOURS);
- //默认 使用CacheLoader
- GuavaCacheManager cacheManager = new GuavaCacheManager();
- cacheManager.setCacheBuilder(cacheBuilder);
- return cacheManager;
- }
- }
- import com.guava.cache.lean.entity.UserInfo;
- import com.guava.cache.lean.service.GuavaCacheService;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.cache.CacheManager;
- import org.springframework.web.bind.annotation.GetMapping;
- import org.springframework.web.bind.annotation.PathVariable;
- import org.springframework.web.bind.annotation.RestController;
-
- @RestController
- public class GuavaCacheController {
- @Autowired
- private GuavaCacheService guavaCacheService;
-
- @Autowired
- private CacheManager cacheManager;
-
- @GetMapping("/users/{name}")
- public UserInfo getUser(@PathVariable String name) {
- System.out.println("==================");
- UserInfo user = guavaCacheService.getUserByName(name);
- System.out.println(cacheManager.toString());
- return user;
- }
- }
- import com.guava.cache.lean.entity.UserInfo;
- import lombok.extern.slf4j.Slf4j;
- import org.springframework.cache.annotation.CacheConfig;
- import org.springframework.cache.annotation.Cacheable;
- import org.springframework.stereotype.Service;
-
- import java.util.List;
- import java.util.Objects;
-
- /**
- * @Cacheable 触发缓存入口(这里一般放在创建和获取的方法上)
- * @CacheEvict 触发缓存的eviction(用于删除的方法上)
- * @CachePut 更新缓存且不影响方法执行(用于修改的方法上,该注解下的方法始终会被执行)
- * @Caching 将多个缓存组合在一个方法上(该注解可以允许一个方法同时设置多个注解)
- * @CacheConfig 在类级别设置一些缓存相关的共同配置(与其它缓存配合使用)
- */
- @Service
- @CacheConfig(cacheManager = "guavaCacheManager")
- @Slf4j
- public class GuavaCacheService {
- /**
- * 获取用户信息(此处是模拟的数据)
- */
- /**
- * 缓存 key 是 username 的数据到缓存 users 中,
- * 如果没有指定 key,则方法参数作为 key 保存到缓存中
- */
- @Cacheable(value = "Userdata", key = "#username")
- public UserInfo getUserByName(String username) {
- UserInfo user = getUserFromList(username);
- return user;
- }
- /**
- * 从模拟的数据集合中筛选 username 的数据
- */
- private UserInfo getUserFromList(String username) {
- List
userDaoList = UserDataFactory.getUserDaoList(); - for (UserInfo user : userDaoList) {
- if (Objects.equals(user.getName(), username)) {
- return user;
- }
- }
- return null;
- }
-
- }
- import com.guava.cache.lean.entity.UserInfo;
- import java.util.ArrayList;
- import java.util.List;
- public class UserDataFactory {
- private UserDataFactory() {
- }
- private static List
userDtoList; - static {
- // 初始化集合
- userDtoList = new ArrayList<>();
-
- UserInfo user = null;
- for (int i = 0; i < 5; i++) {
- user = new UserInfo();
- user.setName("star" + i);
- user.setAge(23);
- userDtoList.add(user);
- }
- }
- public static List
getUserDaoList() { - return userDtoList;
- }
- }
- @Data
- @ToString
- @AllArgsConstructor
- @NoArgsConstructor
- public class UserInfo {
- private Integer id;
- private String name;
- private String sex;
- private Integer age;
- }


Ehcache:纯java进程内存缓存框架,具有快速、精干等特点。
核心组件:
特点:

Ehcache的缓存数据过期策略
Ehcache采用的是懒淘汰机制,每次往缓存放入数据的时候,都会存一个时间,在读取的时候要和设置的时间做TTL比较来判断是否过期。
使用介绍:
ehcache.xml
- "1.0" encoding="UTF-8"?>
-
-
"java.io.tmpdir"/> -
-
-
-
-
- maxElementsInMemory="10000"
- eternal="false"
- overflowToDisk="true"
- timeToIdleSeconds="10"
- timeToLiveSeconds="20"
- diskPersistent="false"
- diskExpiryThreadIntervalSeconds="120"/>
-
-
"simpleCache" - maxElementsInMemory="1000"
- eternal="false"
- overflowToDisk="true"
- timeToIdleSeconds="10"
- timeToLiveSeconds="20"/>
-
可以看看这个文章,挺详细,并且写了关于其在分布式方面的应用
