缓存介绍
一级缓存:本地缓存,也称堆内缓存, 比较有名的如Guava Cache, Caffeine, Ehcache;
二级缓存:堆外缓存, 常用Ehcache;
三级缓存:也称分布式缓存,常见如redis,memcache, 阿里Tair等;
各级缓存优缺点总结:
缓存等级
|
优点
|
缺点
|
---|---|---|
一级缓存 |
速度最快,纳秒级别访问速度; 堆内直接存取对象,无需序列化和反序列化; 无需访问远程服务器; |
多节点部署时需要考虑数据一致性问题; 一般存取数量很少的对象; 仅简单的Key-Value数据结构; 单一的淘汰算法; |
二级缓存 |
速度次之,微妙级别访问速度; 堆外存取对象,需要序列化和反序列化; 无需访问远程服务器; |
多节点部署时需要考虑数据一致性问题; 一般存取数量较少的对象; 仅简单的Key-Value数据结构; 单一的淘汰算法; |
三级缓存 |
支持大数据量存储; 支持集群部署; 一般都支持持久化功能; 部分支持丰富的数据结构,如redis, Tair; 丰富的缓存淘汰算法; |
速度最慢,毫秒级别访问速度; 存取时需要序列化和反序列化; 需要建立连接访问远程服务器(RESP协议);
|
2.缓存淘汰算法简介
命中率
命中率=返回正确结果数/请求缓存次数,命中率问题是缓存中的一个非常重要的问题,它是衡量缓存有效性的重要指标。命中率越高,表明缓存的使用率越高。
常见淘汰算法
- FIFO(first in first out)
先进先出策略,最先进入缓存的数据在缓存空间不够的情况下(超出最大元素限制)会被优先被清除掉,以腾出新的空间接受新的数据。策略算法主要比较缓存元素的创建时间。
在数据实效性要求场景下可选择该类策略,优先保障最新数据可用。
- LRU(least recently used)
最近最少使用策略,无论是否过期,根据元素最后一次被使用的时间戳,清除最远使用时间戳的元素释放空间。策略算法主要比较元素最近一次被get使用时间。
在热点数据场景下较适用,优先保证热点数据的有效性。
- LFU(less frequently used)
最近最少频率使用策略,无论是否过期,根据元素的被使用次数判断,清除使用次数较少的元素释放空间。策略算法主要比较元素的hitCount(命中次数)。
在保证高频数据有效性场景下,可选择这类策略。
除此之外,还有一些简单策略比如:
- 根据过期时间判断,清理过期时间最长的元素;
- 根据过期时间判断,清理最近要过期的元素;
- 随机清理;
- 根据关键字(或元素内容)长短清理等。
W-TinyLFU——一种现代的缓存淘汰算法 。Caffine Cache就是基于此算法而研发。Caffeine 因使用 Window TinyLfu 回收策略,提供了一个近乎最佳的命中率。
2.Caffeine使用
2.1 maven引入:
< dependency > < groupId >com.github.ben-manes.caffeine</ groupId > < artifactId >caffeine</ artifactId > < version >2.8.8</ version > </ dependency > |
2.2数据填充策略(Population)
Caffeine为我们提供了三种填充策略:手动、同步和异步:
手动加载(Manual)
Cache<String, Object> manualCache = Caffeine.newBuilder() .expireAfterWrite( 10 , TimeUnit.MINUTES) .maximumSize(10_000) .build(); String key = "name1" ; // 根据key查询一个缓存,如果没有返回NULL graph = manualCache.getIfPresent(key); // 根据Key查询一个缓存,如果没有调用createExpensiveGraph方法,并将返回值保存到缓存。 // 如果该方法返回Null则manualCache.get返回null,如果该方法抛出异常则manualCache.get抛出异常 graph = manualCache.get(key, k -> createExpensiveGraph(k)); // 将一个值放入缓存,如果以前有值就覆盖以前的值 manualCache.put(key, graph); // 删除一个缓存 manualCache.invalidate(key); ConcurrentMap<String, Object> map = manualCache.asMap(); cache.invalidate(key); |
Cache接口允许显式的去控制缓存的检索,更新和删除。
我们可以通过cache.getIfPresent(key) 方法来获取一个key的值,通过cache.put(key, value)方法显示的将数控放入缓存,但是这样子会覆盖缓原来key的数据。更加建议使用cache.get(key,k - > value) 的方式,get 方法
将一个参数为 key 的 Function (createExpensiveGraph) 作为参数传入。如果缓存中不存在该键,则调用这个 Function 函数,并将返回值作为该缓存的值插入缓存中。get 方法是以阻塞方式执行调用,即使多个线程同时
请求该值也只会调用一次Function方法。这样可以避免与其他线程的写入竞争,这也是为什么使用 get 优于 getIfPresent 的原因。
注意:如果调用该方法返回NULL(如上面的 createExpensiveGraph 方法),则cache.get返回null,如果调用该方法抛出异常,则get方法也会抛出异常。
可以使用Cache.asMap() 方法获取ConcurrentMap进而对缓存进行一些更改。
同步加载(Loading)
LoadingCache<String, Object> loadingCache = Caffeine.newBuilder() .maximumSize(10_000) .expireAfterWrite( 10 , TimeUnit.MINUTES) .build(key -> createExpensiveGraph(key)); String key = "name1" ; // 采用同步方式去获取一个缓存和上面的手动方式是一个原理。在build Cache的时候会提供一个createExpensiveGraph函数。 // 查询并在缺失的情况下使用同步的方式来构建一个缓存 Object graph = loadingCache.get(key); // 获取组key的值返回一个Map List<String> keys = new ArrayList<>(); keys.add(key); Map<String, Object> graphs = loadingCache.getAll(keys); |
异步加载(Asynchronously Loading)
AsyncLoadingCache<String, Object> asyncLoadingCache = Caffeine.newBuilder() .maximumSize(10_000) .expireAfterWrite( 10 , TimeUnit.MINUTES) .buildAsync(key -> createExpensiveGraph(key)); // .buildAsync((key, executor) -> createExpensiveGraphAsync(key, executor)); String key = "name1" ; // 查询并在缺失的情况下使用异步的方式来构建缓存 CompletableFuture<Object> graph = asyncLoadingCache.get(key); // 查询一组缓存并在缺失的情况下使用异步的方式来构建缓存 List<String> keys = new ArrayList<>(); keys.add(key); CompletableFuture<Map<String, Object>> graphs = asyncLoadingCache.getAll(keys); // 异步转同步 loadingCache = asyncLoadingCache.synchronous(); |
AsyncLoadingCache是继承自LoadingCache类的,异步加载使用Executor去调用方法并返回一个CompletableFuture。异步加载缓存使用了响应式编程模型。
如果要以同步方式调用时,应提供CacheLoader。要以异步表示时,应该提供一个AsyncCacheLoader,并返回一个CompletableFuture。
synchronous()这个方法返回了一个LoadingCacheView视图,LoadingCacheView也继承自LoadingCache。调用该方法后就相当于你将一个异步加载的缓存AsyncLoadingCache转换成了一个同步加载的
缓存LoadingCache。默认使用ForkJoinPool.commonPool()来执行异步线程,但是我们可以通过Caffeine.executor(Executor) 方法来替换线程池。
2.3 数据驱逐策略(eviction)
Caffeine提供三类驱逐策略:基于大小(size-based),基于时间(time-based)和基于引用(reference-based)
基于大小(size-based)
基于大小驱逐,有两种方式:一种是基于缓存大小,一种是基于权重。
// 根据缓存的计数进行驱逐 LoadingCache<Key, Graph> graphs = Caffeine.newBuilder() .maximumSize(10_000) .build(key -> createExpensiveGraph(key)); // 根据缓存的权重来进行驱逐(权重只是用于确定缓存大小,不会用于决定该缓存是否被驱逐) LoadingCache<Key, Graph> graphs = Caffeine.newBuilder() .maximumWeight(10_000) .weigher((Key key, Graph graph) -> graph.vertices().size()) .build(key -> createExpensiveGraph(key)); |
我们可以使用Caffeine.maximumSize(long)方法来指定缓存的最大容量。当缓存超出这个容量的时候,会使用Window TinyLfu策略来删除缓存。
我们也可以使用权重的策略来进行驱逐,可以使用Caffeine.weigher(Weigher) 函数来指定权重,使用Caffeine.maximumWeight(long) 函数来指定缓存最大权重值。
maximumWeight与maximumSize不可以同时使用。
基于时间(Time-based)
// 基于固定的到期策略进行退出 LoadingCache<Key, Graph> graphs = Caffeine.newBuilder() .expireAfterAccess( 5 , TimeUnit.MINUTES) .build(key -> createExpensiveGraph(key)); LoadingCache<Key, Graph> graphs = Caffeine.newBuilder() .expireAfterWrite( 10 , TimeUnit.MINUTES) .build(key -> createExpensiveGraph(key)); // Evict based on a varying expiration policy // 基于不同的到期策略进行退出 LoadingCache<Key, Graph> graphs = Caffeine.newBuilder() .expireAfter( new Expiry<Key, Graph>() { @Override public long expireAfterCreate(Key key, Graph graph, long currentTime) { long seconds = graph.creationDate().plusHours( 5 ) .minus(System.currentTimeMillis(), MILLIS) .toEpochSecond(); return TimeUnit.SECONDS.toNanos(seconds); } @Override public long expireAfterUpdate(Key key, Graph graph, long currentTime, long currentDuration) { return currentDuration; } @Override public long expireAfterRead(Key key, Graph graph, long currentTime, long currentDuration) { return currentDuration; } }) .build(key -> createExpensiveGraph(key)); |
Caffeine提供了三种定时驱逐策略:
- expireAfterAccess(long, TimeUnit):在最后一次访问或者写入后开始计时,在指定的时间后过期。假如一直有请求访问该key,那么这个缓存将一直不会过期。
- expireAfterWrite(long, TimeUnit): 在最后一次写入缓存后开始计时,在指定的时间后过期。
- expireAfter(Expiry): 自定义策略,过期时间由Expiry实现独自计算。
基于引用(reference-based)
Java4种引用的级别由高到低依次为:强引用 > 软引用 > 弱引用 > 虚引用
引用类型
|
被垃圾回收时间
|
用途
|
生存时间
|
---|---|---|---|
强引用 | 从来不会 | 对象的一般状态 | JVM停止运行时终止 |
软引用 | 在内存不足时 | 对象缓存 | 内存不足时终止 |
弱引用 | 在垃圾回收时 | 对象缓存 | gc运行后终止 |
虚引用 | Unknown | Unknown | Unknown |
// Evict when neither the key nor value are strongly reachable // 当key和value都没有引用时驱逐缓存 LoadingCache<Key, Graph> graphs = Caffeine.newBuilder() .weakKeys() .weakValues() .build(key -> createExpensiveGraph(key)); // Evict when the garbage collector needs to free memory // 当垃圾收集器需要释放内存时驱逐 LoadingCache<Key, Graph> graphs = Caffeine.newBuilder() .softValues() .build(key -> createExpensiveGraph(key)); |
我们可以将缓存的驱逐配置成基于垃圾回收器。为此,我们可以将key 和 value 配置为弱引用或只将值配置成软引用。
注意: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()不可以一起使用。
手动删除缓存
在任何时候,您都可能明确地使缓存无效,而不用等待缓存被驱逐。
// individual key cache.invalidate(key) // bulk keys cache.invalidateAll(keys) // all keys cache.invalidateAll() |
2.4清空缓存(Cleanup)
缓存的删除策略使用的是惰性删除和定时删除,但是我也可以自己调用cache.cleanUp()方法手动触发一次回收操作。cache.cleanUp()是一个同步方法。
2.5统计(Statistice)
Cache<Key, Graph> graphs = Caffeine.newBuilder() .maximumSize(10_000) .recordStats() .build(); |
使用Caffeine.recordStats(),您可以打开统计信息收集。Cache.stats() 方法返回提供统计信息的CacheStats,如:
- hitRate():返回命中与请求的比率
- hitCount(): 返回命中缓存的总数
- evictionCount():缓存逐出的数量
- averageLoadPenalty():加载新值所花费的平均时间