常见的内存缓存框架

  • Post author:
  • Post category:其他


前言

本次将介绍常用的内存缓存框架,主要围绕以下两点

  • 常见开源的内存缓存框架介绍及使用
  • 常见开源的内存缓存框架对比

常用的内存缓存框架


  • Guava Cache

  • Ehcache

  • Caffeine





Guava Cache

Google Guava Cache是一种非常优秀的本地缓存解决方案,提供了基于容量、时间、引用的缓存回收方式

内部实现采用LRU算法,

基于引用回收

很好的利用了java虚拟机的垃圾回收机制

Guava Cache和ConcurrentHashMap很相似,但也不完全一样.最基本的区别是ConcurrentHashMap会一直保存所有添加的元素,

直至显示地移除.Guava Cache为了限制内存占用,通常都

设定为自动回收元素


使用场景

  • 愿意消耗一些内存空间来提升速度

  • 预料到某些键会被多次查询

  • 缓存中存放的数据总量不会超出内存容量


注意事项

Guava Cache是运行在JVM的本地缓存,并不能把数据存放到外部服务器上

如果有这样的要求,可以使用Memcached或Redis这样的分布式缓存





使用





加载方式

Guava Cache主要有两种加载方式,下面将通过代码演示.详细说明两种加载方式的使用

  • CacheLoader
  • Callable


CacheLoader


loadingCache是附带CacheLoader构建而成的缓存实现.创建自己的CacheLoader通常只需要简单实现V load(K key)方法

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.cache.RemovalListener;

import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

/**
 * @author 昊天锤
 * @date 2020/8/10 0010 15:06
 * @description GuavaCache缓存使用缓存加载器示例
 */
public class GuavaCacheByLoaderDemo {

    /**
     * 模拟数据库数据
     */
    private static List<Map<String, String>> dataBase = new ArrayList<>();

    static {
        for (int i = 1; i <= 10; i++) {
            Map<String, String> map = new HashMap<>();
            map.put(String.valueOf(i), "张三" + i);
            dataBase.add(map);
        }
    }

    public static void main(String[] args) throws ExecutionException {
        LoadingCache<String, String> cache = CacheBuilder.newBuilder()
                // 设置缓存容量
                .maximumSize(5)
                // 设置超时时间
                .expireAfterWrite(10, TimeUnit.MINUTES)
                // 提供移除监听器
                .removalListener(removalListener())
                // 提供缓存加载器
                .build(cacheLoader());


        // 使用缓存插入数据
        // 由于缓存的容量只设置了5个,存入6个就会由guava基于容量回收掉1个
        for (int i = 1; i <= 6; i++) {
            cache.put(String.valueOf(i), "张三" + i);
        }

        // 刻意获取一个不存在缓存中的key,让它去调用缓存加载器加载数据到缓存中
        System.out.println(cache.get("10"));
    }

    /**
     * 缓存加载器:缓存中找不到.调用这个方法,加载到缓存中
     */
    private static CacheLoader<String, String> cacheLoader() {
        return new CacheLoader<String, String>() {
            @Override
            public String load(String id) {
                System.out.println("调用缓存加载器加载缓存:key = " + id);
                for (Map<String, String> map : dataBase) {
                    String name = map.get(id);
                    if (Objects.nonNull(name)) {
                        return name;
                    }
                }
                return null;
            }
        };
    }

    /**
     * 移除监听器: 当key被移除时调用该方法
     */
    private static RemovalListener<String, String> removalListener() {
        return removalNotification -> {
            String key = removalNotification.getKey();
            String value = removalNotification.getValue();
            System.out.println("监听到移除操作:key = " + key + ",value = " + value);
        };
    }
}

注意两个点:

如果超过了容量大小便会移除key,并调用removalListener的removalNotification方法

如果get一个不存在缓存的key便会调用cacheLoader的load方法进行运算、缓存、返回


Callable


所有类型的guava cache,不管有没有自动加载功能,都支持get(K ,Callable)方法.这个方法返回缓存中相应的值

或者用给定的Callable运算并把结果加入缓存中.在整个加载方法完成前,缓存项相关的可观察状态都不会更改

这个方法简便的地实现了-如果有缓存返回.否则运算、缓存、再返回

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.springframework.util.StringUtils;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;

/**
 * @author 昊天锤
 * @date 2020/8/10 0010 15:41
 * @description GuavaCache缓存使用Callable示例
 */
public class GuavaCacheByCallableDemo {

    /**
     * 缓存
     */
    private static Cache<String, String> cache = CacheBuilder.newBuilder().maximumSize(5).build();


    /**
     * 模拟数据库数据
     */
    private static List<Map<String, String>> dataBase = new ArrayList<>();

    static {
        for (int i = 1; i <= 10; i++) {
            Map<String, String> map = new HashMap<>();
            map.put(String.valueOf(i), "张三" + i);
            dataBase.add(map);
        }
    }

    public static void main(String[] args) throws ExecutionException {
        // 此时缓存中并没有任何数据,所有现在加载啥都需要运算、缓存、再返回
        String key = "1";
        System.out.println("加载一个不存在的key = " + getByCallable(key));
        
        // 该key之前已经加载过并缓存起来了,所以本次获取并不需要运算.可以直接从缓存中读取
        System.out.println("加载一个已存在的key = " + getByCallable(key));
    }


    /**
     * 如果有缓存返回.否则运算、缓存、再返回
     *
     * @param key 需要查找额key
     * @return key对应的值
     */
    private static String getByCallable(String key) throws ExecutionException {
        return cache.get(key, () -> {
            System.out.println("运算加载 key = " + key);
            for (Map<String, String> map : dataBase) {
                String name = map.get(key);
                if (!StringUtils.isEmpty(name)) {
                    return name;
                }
            }
            return null;
        });
    }
}

注意一个点: 运算加载的过程是:运算、缓存、再返回.所以第一次获取时进行运算 第二次获取时直接取缓存中的数据





缓存回收

Guava Cache支持三种缓存回收策略


容量回收

maximumSize(long ) 当缓存中的元素数量超过指定值时就会进行回收

 CacheBuilder
     .newBuilder()
     // 指定容量大小
     .maximumSize(5)
     .build();


定时回收

expireAfterWrite(long,TimeUnit) 缓存项在给定时间内没有被写入则回收

expireAfterAccess(long,TimeUnit) 缓存项在给定时间内没有被访问则回收

 CacheBuilder
     .newBuilder()
     // 十分钟之内没有被写入,则回收
     .expireAfterWrite(10, TimeUnit.MINUTES)
     // 十分钟之内没有被访问,则回收
     .expireAfterAccess(10, TimeUnit.MINUTES)
     .build();


引用回收 (Reference-based Eviction)


CacheBuilder.newBuilder().weakKeys(); 使用弱引用存储键.当key没有其他引用时,缓存项可以被垃圾回收

CacheBuilder.newBuilder().weakValues(); 使用弱引用存储值.当value没有其他引用时,缓存项可以被垃圾回收

CacheBuilder.newBuilder().softValues(); 使用软引用存储值.按照全局最近最少使用的顺序回收

CacheBuilder
    .newBuilder()
    // 使用弱引用存储键.当key没有其他引用时,缓存项可以被垃圾回收
    .weakKeys()
    // 使用弱引用存储值.当value没有其他引用时,缓存项可以被垃圾回收
    .weakValues()
    // 使用软引用存储值.按照全局最近最少使用的顺序回收
    .softValues()
    .build();





显示清除

任何时候都可以显示地清除缓存项,而不是等到它被回收

Cache<String, Object> cache = CacheBuilder.newBuilder().maximumSize(5).build();
// 单个清除
cache.invalidate("key");
// 批量清除
cache.invalidateAll(Arrays.asList("key", "key1"));
// 清空
cache.invalidateAll();

由于GuavaCache它是存在就读取不存在就加载

所以在修改key的时候可以显示清除缓存,当这个key在下一次被读取的时候就会去加载数据源数据

这样就最大程度的保证了数据源数据和缓存数据是一致的





统计

GuavaCache还提供了缓存统计功能可以调用recordStats来开启统计功能

// 用来开启统计功能
Cache<String, Object> cache = CacheBuilder.newBuilder().maximumSize(5).recordStats().build();

然后可以调用cache.stats()方法会返回一个CacheStats对象,该对象提供如下统计信息

CacheStats stats = cache.stats();
// 缓存命中率
stats.hitRate();
// 加载新值的平均时间,单位为纳秒
stats.averageLoadPenalty();
// 缓存项被回收的总数,不包括显示清除
stats.evictionCount();





Ehcache

Ehcache是一个纯java进程内存缓存框架,具有快速、精干等特点,是Hibernate中默认的CacheProvider


主要特性

  • 快速、简单、支持多种缓存策略
  • 支持内存和磁盘缓存数据,因为无需担心容量问题
  • 缓存数据会在虚拟机重启的过程中写入磁盘
  • 可以通过RMI、可插入API等方式进行分布式缓存(比较弱)
  • 具有缓存和缓存管理器的侦听接口
  • 支持多缓存管理实例,以及一个实例的多个缓存区域
  • 提供Hibernate的缓存实现


Ehcache架构图

从这个图中可以看到CacheManager和CacheManager Listener SPI的扩展

同时包含了Cache核心、存储、磁盘存储还支持多种缓存淘汰策略:LRU、LFU、FIFO

适用场景

  • 单个应用或者对缓存访问要求很高的应用
  • 简单的共享可以,但是不合适涉及缓存恢复、大数据缓存
  • 如果系统比较大型,存在缓存共享,分布式部署,缓存内容大这几点便不合适使用该缓存
  • 在实际工作中,更多是将Ehcache作为与Redis配合的二级缓存





使用





ehcache.xml

准备ehcache.xml文件,存放在resources目录下,如图所示

ehcache.xml内容如下

<?xml version="1.0" encoding="UTF-8"?>
<ehcache>
    <!-- 指定一个文件目录,当EhCache把数据写到硬盘上时,将把数据写到这个文件目录下 -->
    <diskStore path="java.io.tmpdir"/>

    <!--
    cache元素的属性:
        name:缓存名称
        maxElementsInMemory:内存中最大缓存对象数
        maxElementsOnDisk:硬盘中最大缓存对象数,若是0表示无穷大
        eternal:true表示对象永不过期,此时会忽略timeToIdleSeconds和timeToLiveSeconds属性,默认为false
        overflowToDisk:true表示当内存缓存的对象数目达到了maxElementsInMemory界限后,会把溢出的对象写到硬盘缓存中。注意:如果缓存的对象要写入到硬盘中的话,则该对象必须实现了Serializable接口才行。
        diskSpoolBufferSizeMB:磁盘缓存区大小,默认为30MB。每个Cache都应该有自己的一个缓存区。
        diskPersistent:是否缓存虚拟机重启期数据
        diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认为120秒
        timeToIdleSeconds: 设定允许对象处于空闲状态的最长时间,以秒为单位。当对象自从最近一次被访问后,如果处于空闲状态的时间超过了timeToIdleSeconds属性值,这个对象就会过期,EHCache将把它从缓存中清空。只有当eternal属性为false,该属性才有效。如果该属性值为0,则表示对象可以无限期地处于空闲状态
        timeToLiveSeconds:设定对象允许存在于缓存中的最长时间,以秒为单位。当对象自从被存放到缓存中后,如果处于缓存中的时间超过了 timeToLiveSeconds属性值,这个对象就会过期,EHCache将把它从缓存中清除。只有当eternal属性为false,该属性才有效。如果该属性值为0,则表示对象可以无限期地存在于缓存中。timeToLiveSeconds必须大于timeToIdleSeconds属性,才有意义
        memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。可选策略有:LRU(最近最少使用,默认策略)、FIFO(先进先出)、LFU(最少访问次数)。
    -->

    <!-- 设定缓存的默认数据过期策略 -->
    <defaultCache
            maxElementsInMemory="10000"
            eternal="false"
            overflowToDisk="true"
            timeToIdleSeconds="10"
            timeToLiveSeconds="20"
            diskPersistent="false"
            diskExpiryThreadIntervalSeconds="120"/>

    <cache name="simpleCache"
           maxElementsInMemory="1000"
           eternal="false"
           overflowToDisk="true"
           timeToIdleSeconds="10"
           timeToLiveSeconds="20"/>

</ehcache>

文件注释有对设置缓存参数的详细说明:最大缓存对象数、磁盘缓存区大小等等





Ehcache使用示例

import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;

/**
 * @author 昊天锤
 * @date 2020/8/10 0010 17:00
 * @description Ehcache使用示例
 */
public class EhcacheDemo {

    public static void main(String[] args) {
        // 初始化CacheManager对象
        CacheManager cacheManager = new CacheManager();
        // 加载自定义cache对象
        Cache cache = cacheManager.getCache("simpleCache");
        // 把集合放入缓存 存放键值对集合 类似map
        cache.put(new Element("user", "张三"));
        // 取出集合根据key获取值
        System.out.println("key=user,value=" + cache.get("user").getObjectValue());
        // 更新缓存的值
        cache.put(new Element("user", "李四"));
        System.out.println("key=user,value=" + cache.get("user").getObjectValue());
        // 获取缓存中的元素个数
        System.out.println("集合个数:" + cache.getSize());
        // 移除cache某个值
        cache.remove("user");
        System.out.println("集合个数:" + cache.getSize());
        // 关闭当前cacheManager对象
        cacheManager.shutdown();
    }
}

存入、取出、删除等方法使用都很简单,对于缓存参数配置主要在ehcache.xml里面.这样ehcache的简单使用便就完成了





Ehcache缓存集群

对!刚刚还提到Ehcache支持分布式缓存集群,其实也是在xml中加入配置,如下

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">

    <!--EHCache分布式缓存集群环境配置-->
    <!--rmi手动配置-->
    <cacheManagerPeerProviderFactory class= "net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory"
                                     properties="peerDiscovery=manual,rmiUrls=//localhost:40000/user"/>

    <cacheManagerPeerListenerFactory
            class="net.sf.ehcache.distribution.RMICacheManagerPeerListenerFactory"
            properties="hostName=localhost,port=40001, socketTimeoutMillis=120000"/>
</ehcache>





caffeine

caffeine是Google基于java8对Guava Cache的重写升级版本,支持丰富的缓存过期策略,尤其是

TinyLfu淘汰算法

,提供了一个几乎最佳的命中率

从性能上(读、写、读/写)也足以秒杀其他一堆进程内缓存框架.Spring5更是直接放弃了使用多年的Guava而采用caffeine

下面是caffeine官方读写性能测试报告

caffeine的API操作功能和Guava 是基本保持一致的

并且caffeine为了兼容之前使用Guava 的用户,做了一个Guava的Adapter给大家使用.这一点也是十分的贴心

caffeine是一个

非常不错的缓存框架

,无论在

性能方面还是API方面都要比Guava Cache要优秀一些

如果在新的项目中要使用local cache的话,可以

优先考虑使用caffeine

对于老项目,如果使用了Guava Cache,想要升级为caffeine的话,可以使用caffeine提供的Guava Cache适配器方便的进行切换





使用

caffeine使用和Guava Cache非常相似





加载方式

caffeine主要有3种加载方式,下面将通过代码演示.详细说明三种加载方式的使用

  • 手动加载
  • 同步加载
  • 异步加载


手动加载Cache


手动加载的方式也还是在get方法的基础上,加上一个function运算函数.还是那句话,缓存中找不到既运算、缓存、再返回

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;

import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

/**
 * @author 昊天锤
 * @date 2020/8/10 0010 20:17
 * @description Caffeine 手动加载
 */
public class CaffeineDemo {

    public static void main(String[] args) throws InterruptedException {
        Cache<String, String> cache = Caffeine.newBuilder()
                // 基于时间失效,写入之后开始计时失效
                .expireAfterWrite(2000, TimeUnit.MILLISECONDS)
                // 缓存容量
                .maximumSize(5)
                .build();

        // 使用java8 Lambda表达式声明一个方法,get不到缓存中的值调用这个方法运算、缓存、返回
        Function function = key -> key + "_" + System.currentTimeMillis();
        String value = cache.get("key", function);
        System.out.println(value);

        //让缓存到期
        Thread.sleep(2001);
        // 存在就取,不存在就返回空
        System.out.println(cache.getIfPresent("key"));

        // 重新存值
        cache.put("key", "hello world");
        String key = cache.get("key", function);
        System.out.println(key);


        // 获取所有值打印出来
        ConcurrentMap<String, String> concurrentMap = cache.asMap();
        System.out.println(concurrentMap);

        // 删除key
        cache.invalidate("key");
        // 获取所有值打印出来
        System.out.println(cache.asMap());
    }
}


同步加载LoadingCache


同步加载的方式是需要在build的时候指定运算缓存的CacheLoader并实现其load方法

import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;

import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * @author 昊天锤
 * @date 2020/8/10 0010 20:17
 * @description Caffeine 同步加载
 */
public class CaffeineDemo {

    public static void main(String[] args) throws InterruptedException {
        LoadingCache<String, String> cache = Caffeine.newBuilder()
                // 基于时间失效,写入之后开始计时失效
                .expireAfterWrite(2000, TimeUnit.MILLISECONDS)
                // 缓存容量
                .maximumSize(5)
                // 可以使用java8函数式接口的方式,这里其实是重写CacheLoader类的load方法
                .build(key -> key + "_" + System.currentTimeMillis());

        // 获取一个不存在的kay,让它去调用CacheLoader的load方法
        System.out.println(cache.get("key"));

        // 等待2秒让key失效
        TimeUnit.SECONDS.sleep(2);
        System.out.println(cache.getIfPresent("key"));


        // 批量获取key,让他批量去加载
        Map<String, String> all = cache.getAll(Arrays.asList("key1", "key2", "key3"));
        System.out.println(all);
    }
}


异步加载AsyncLoadingCache


异步加载的需要使用的build方式是buildAsync,同时需要指定运算缓存的CacheLoader并实现其load方法

import com.github.benmanes.caffeine.cache.AsyncLoadingCache;
import com.github.benmanes.caffeine.cache.Caffeine;

import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

/**
 * @author 昊天锤
 * @date 2020/8/10 0010 20:17
 * @description Caffeine 异步加载
 */
public class CaffeineDemo {

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        AsyncLoadingCache<Object, String> cache = Caffeine.newBuilder()
                // 基于时间失效,写入之后开始计时失效
                .expireAfterWrite(2000, TimeUnit.MILLISECONDS)
                // 缓存容量
                .maximumSize(5)
                // 可以使用java8函数式接口的方式,这里其实是重写CacheLoader的load方法
                .buildAsync(key -> key + "_" + System.currentTimeMillis());


        // 获取一个不存在的kay,让它异步去调用CacheLoader的load方法。这时候他会返回一个CompletableFuture
        // 既:我已经帮你异步去运算key的值了,你什么时候要再什么时候调用CompletableFuture的get方法就好了
        CompletableFuture<String> future = cache.get("key");
        // 为了证明是异步调用,可以将时间戳打印出来和value中的时间戳比较.
        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<String> completableFuture = cache.getIfPresent("key");
        if (Objects.nonNull(completableFuture)) {
            System.out.println(completableFuture.get());
        }
    }
}

可以从结果看到,调用运算key的时候是比当前时间要早.但是又没有阻塞当前线程,所以可以判定确实是异步加载

异步调用的好处就在于在高并发场景下会有显著的性能提升,有对这一块比较感兴趣的可以学习一下JUC包里面内容





驱逐策略


caffeine主要有三种缓存回收的方式,下面将通过代码演示说明三种加载方式的使用


容量大小驱逐

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.RemovalListener;

/**
 * @author 昊天锤
 * @date 2020/8/10 0010 20:17
 * @description Caffeine 基于缓存容量大小驱逐缓存
 */
public class CaffeineDemo {

    public static void main(String[] args) {
        // 基于缓存容量大小
        Cache<String, String> cache = Caffeine
                .newBuilder()
                .maximumSize(5)
                .removalListener((RemovalListener) (k, v, removalCause) -> {
                    System.out.println("移除: key = " + k + ",cause = " + removalCause);
                })
                .build();

        for (int i = 1; i <= 6; i++) {
            cache.put(String.valueOf(i), "张三" + i);
        }
        cache.cleanUp();
    }
}

由于我们设置的缓存大小是5个,可是缓存了6个,所以必然有一个要被回收


权重驱逐

Caffeine.weigher(Weigher) 函数来指定权重,Caffeine.maximumWeight(long) 函数来指定缓存最大权重值

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.RemovalListener;

/**
 * @author 昊天锤
 * @date 2020/8/10 0010 20:17
 * @description Caffeine 基于权重驱逐
 */
public class CaffeineDemo {

    public static void main(String[] args) {
        // 基于权重驱逐
        Cache<String, String> cache = Caffeine
                .newBuilder()
                .maximumWeight(15)
                .weigher((k, v) -> Integer.valueOf((String) v))
                .removalListener((RemovalListener) (k, v, removalCause) -> {
                    System.out.println("移除: key = " + k + ",cause = " + removalCause);
                })
                .build();

        for (int i = 10; i <= 15; i++) {
            cache.put(String.valueOf(i), String.valueOf(i));
        }
        cache.cleanUp();
    }
}

权重只是用于确定缓存大小,不会用于决定该缓存是否被驱逐

注意:权重驱逐和容量大小驱逐两种方式不能同时使用


时间驱逐

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.RemovalListener;

import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * @author 昊天锤
 * @date 2020/8/10 0010 20:17
 * @description Caffeine 基于时间驱逐
 */
public class CaffeineDemo {

    public static void main(String[] args) throws InterruptedException {
        // 基于时间驱逐
        Cache cache = Caffeine
                .newBuilder()
                .maximumSize(10)
                // 写入之后开始计时失效
                .expireAfterWrite(2000, TimeUnit.MILLISECONDS)
                // 访问之后开始计时失效
                .expireAfterAccess(2000, TimeUnit.MILLISECONDS)
                // 自定义线程池执行RemovalListener
                .executor(Executors.newSingleThreadExecutor())
                .removalListener((RemovalListener) (k, v, removalCause) -> {
                    System.out.println("移除: key = " + k + ",cause = " + removalCause);
                })
                .build();

        cache.put("key", "hello world");
        System.out.println(cache.getIfPresent("key"));
        TimeUnit.SECONDS.sleep(2);
        System.out.println(cache.getIfPresent("key"));
    }
}





总结

到这就已经介绍完常用的内存缓存框架了,最后在这里做一个比较

比较项 ConcurrentMap Ehcache GuavaCache caffeine
读写性能 很好,分段锁 很好
淘汰算法 LRU、LFU、FIFO LRU W-TinyLFU
功能丰富度 功能简单 功能丰富 功能丰富,支持刷新和虚引用 功能和GuavaCache很相似
工具大小 jdk自带,很小 一般 较小 一般
是否支持持久化
是否支持集群

综合来说如果不考虑分布式集群缓存还有持久化的功能,就纯粹的内存缓存.

caffeine无疑是最好的选择

为了更好地掌握caffeine这个优秀的缓存框架,下一篇文章将详细讲解caffeine的实现原理(源码分析)



版权声明:本文为wjandy0211原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。