简介
缓存是程序员们绕不开的话题,像是常用的本地缓存Guava,分布式缓存Redis等,是提供高性能服务的基础。今天敬姐带大家一起认识一个更高效的本地缓存——Caffeine。
Caffeine Cache使用了基于内存的存储策略,并且支持高并发、低延迟,同时还提供了缓存过期、定时刷新、缓存大小控制等功能。Caffeine是一个Java高性能的本地缓存库。据其官方说明,因使用 Window TinyLfu 回收策略,其缓存命中率已经接近最优值。此处应有掌声👏🏻
它是Guava Cache的升级版本, 但是比Guava Cache更快,更稳定。Caffeine Cache最适合做数据量不大,但是读写频繁的应用场景。结合Redis等可以实现应用中的多级缓存策略。
!
还是老套路,先写代码示例,对Caffeine的帅有个肤浅的认识,后面再去研究他的内在机制
Caffeine缓存类型
新建项目,添加caffeine的maven引用
com.github.ben-manes.caffeine caffeine 3.1.7
Caffeine四种缓存类型:Cache, LoadingCache, AsyncCache, AsyncLoadingCache
1. Cache
在获取缓存值时,如果想要在缓存值不存在时,原子地将值写入缓存,则可以调用get(key, k -> value)方法,该方法将避免写入竞争。
在多线程情况下,当使用get(key, k -> value)时,如果有另一个线程同时调用本方法进行竞争,则后一线程会被阻塞,直到前一线程更新缓存完成;而若另一线程调用getIfPresent()方法,则会立即返回null,不会被阻塞。
public class CacheDemo { static Cache cache = Caffeine.newBuilder() .maximumSize(1000) .expireAfterWrite(10, TimeUnit.MINUTES) .build(); public static void main(String[] args) { //get System.out.println(cache.get(1, x -> new Article(x)));//Article{id=1, title='title 1'} //getIfPresent System.out.println(cache.getIfPresent(2));//null //put 设置缓存 cache.put(2, new Article(2)); System.out.println(cache.getIfPresent(2));//Article{id=2, title='title 2'} //invalidate 移除缓存 cache.invalidate(2); System.out.println(cache.getIfPresent(2));//null }}
2.LoadingCache
LoadingCache是一种自动加载的缓存。使用时需要指定CacheLoader,并实现其中的load()方法供缓存缺失时自动加载。
get()方法用来读取缓存,和上面的Cache方式不同之处在于,当缓存不存在或者已经过期时,会自动调用CacheLoader.load()方法加载最新值。加载过程是一种同步操作,将返回值插入缓存并且返回。
/** * LoadingCache示例,自动加载的缓存 * * @author chenjing */public class LoadingCacheDemo { private static LoadingCache cache = Caffeine.newBuilder() .build(new CacheLoader() { @Override public @Nullable Article load(Integer id) { return new Article(id); } }); public static void main(String[] args) { System.out.println(cache.get(1));//Article{id=1, title='title 1'} //getIfPresent System.out.println(cache.getIfPresent(2));//null System.out.println(cache.getAll(List.of(10,20)));//{10=Article{id=10, title='title 10'}, 20=Article{id=20, title='title 20'}} }}
3.AsyncCache
和Cache类似,但是异步执行操作,并返回保存实际值的CompletableFuture,适用于需要进行并发执行提高吞吐量的场景。
/** * Caffeine 异步缓存 * * @author chenjing */public class AsyncCacheDemo { static AsyncCache cache = Caffeine.newBuilder() .maximumSize(1000) .expireAfterWrite(10, TimeUnit.MINUTES) .buildAsync(); public static void main(String[] args) throws ExecutionException, InterruptedException { //get 返回的是CompletableFuture CompletableFuture future = cache.get(1, x -> new Article(x)); System.out.println(future.get());//Article{id=1, title='title 1'} }}
4.AsyncLoadingCache
异步加载缓存
/** * Caffeine AsyncLoadingCache 异步自动加载缓存 * @author chenjing */public class AsyncLoadingCacheDemo { private static AsyncLoadingCache asyncLoadingCache = Caffeine.newBuilder() .maximumSize(1000) .expireAfterWrite(10, TimeUnit.MINUTES) .buildAsync( (key, executor) -> CompletableFuture.supplyAsync(() -> new Article(key), executor) ); public static void main(String[] args) { CompletableFuture userCompletableFuture = asyncLoadingCache.get(66); System.out.println(userCompletableFuture.join());//Article{id=66, title='title 66'} }}
缓存过期
- 基于缓存大小
通过 maximumSize() 指定缓存大小。
/** * 测试Caffeine基于空间的驱逐策略 * * @author chenjing */public class MaxSizeExpire { public static void main(String[] args) { System.out.println("测试基于容量过期的缓存"); Integer size = 10; Cache cache = Caffeine.newBuilder() .maximumSize(size) .recordStats() .build(); cache.put(1, new Article(1)); System.out.println("放入一条数据,获取看看"); System.out.println(cache.getIfPresent(1));//Article{id=1, title='title 1'} System.out.println("放入" + size + "条数据"); for (int i = 2; i <= size + 5; i++) { cache.put(i, new Article(i)); } System.out.println("再打印第一条数据看看"); System.out.println(cache.getIfPresent(1));//Article{id=1, title='title 1'} //打印一下缓存状态 System.out.println(cache.stats());//CacheStats{hitCount=2, missCount=0, loadSuccessCount=0, loadFailureCount=0, totalLoadTime=0, evictionCount=5, evictionWeight=5} }}
- 基于缓存写入时间
一般最近一段时间缓存的数据才是有效的,缓存很久之前的业务数据是没有意义的。常见的一种场景就是,缓存写入之后N分钟之后自动失效。
/** * Caffeine expireAfterWrite * @author chenjing */public class ExpireAfterWriteDemo { public static void main(String[] args) throws InterruptedException { System.out.println("测试基于时间过期的缓存"); Integer seconds = 5; Integer size = 5; Cache cache = Caffeine.newBuilder() //基于时间过期 .expireAfterWrite(seconds, TimeUnit.SECONDS) //监控缓存移除 .removalListener((Integer key, Article value, RemovalCause cause) -> System.out.printf("移除key %s,原因是 %s %n", key, cause)) .build(); System.out.println("放入" + size + "条数据"); for (int i = 1; i <= size; i++) { cache.put(i, new Article(i)); System.out.println(cache.getIfPresent(i)); } System.out.println("sleep 10 seconds"); Thread.sleep(10000); for (int i = 1; i <= size; i++) { cache.put(i, new Article(i)); System.out.println(cache.getIfPresent(i)); } }}
执行结果:
测试基于时间过期的缓存放入5条数据Article{id=1, title='title 1'}Article{id=2, title='title 2'}Article{id=3, title='title 3'}Article{id=4, title='title 4'}Article{id=5, title='title 5'}sleep 10 seconds移除key 1,原因是 EXPIREDArticle{id=1, title='title 1'}Article{id=2, title='title 2'}移除key 2,原因是 EXPIRED移除key 3,原因是 EXPIREDArticle{id=3, title='title 3'}移除key 4,原因是 EXPIRED移除key 5,原因是 EXPIREDArticle{id=4, title='title 4'}Article{id=5, title='title 5'}
缓存刷新
refreshAfterWrite()和expireAfterWrite()不同,在刷新的时候如果查询缓存元素,其旧值将仍被返回,直到该元素的刷新完毕后结束后才会返回刷新后的新值。这里的刷新操作默认也是由线程池ForkJoinPool.commonPool()异步执行的,我们也可以通过Caffeine.executor()重写来指定自定义的线程池。
public class RefreshAfterWriteDemo { public static void main(String[] args) throws InterruptedException { LoadingCache cache = Caffeine.newBuilder() .refreshAfterWrite(2, TimeUnit.SECONDS) .build(new CacheLoader() { @Override public @Nullable Article load(Integer integer) { return new Article(integer); } @Override public @Nullable Article reload(Integer key, Article oldValue) { return new Article(key + 100); } }); cache.put(1, new Article(1)); for (int i = 0; i < 3; i++) { System.out.println(cache.get(1)); Thread.sleep(3000); } }}
缓存监控
创建Caffeine时设置removalListener,可以监听缓存地清除或更新监听。
Cache cache = Caffeine.newBuilder() //基于时间过期 .expireAfterWrite(seconds, TimeUnit.SECONDS) //监控缓存移除 .removalListener((Integer key, Article value, RemovalCause cause) -> System.out.printf("移除key %s,原因是 %s %n", key, cause)) .build();
当缓存中的数据发送更新,或者被清除时,就会触发监听器,在监听器里可以自定义一些处理手段,比如打印出哪个数据被清除,原因是什么。这个触发和监听的过程是异步的,就是有可能数据都被删除一小会儿了,监听器才监听到。
本人公众号[ 敬YES ]同步更新,欢迎大家关注~
作者:陈敬(公众号:敬YES)
出处:http://www.cnblogs.com/janes/
博客文章仅供交流学习,请勿用于商业用途。如需转载,请务必注明出处。