Caffeine本地缓存示例详解

2023-08-06 0 1,057

目录

一. 概述

Caffeine是一种高性能的缓存库,是基于Java 8的最佳(最优)缓存框架。

基于Google的Guava Cache,Caffeine提供一个性能卓越的本地缓存(local cache) 实现, 也是SpringBoot内置的本地缓存实现。(Caffeine性能是Guava Cache的6倍)

Caffeine提供了灵活的结构来创建缓存,并且有以下特性:

  • 自动加载条目到缓存中,可选异步方式
  • 可以基于大小剔除
  • 可以设置过期时间,时间可以从上次访问或上次写入开始计算
  • 异步刷新
  • keys自动包装在弱引用中
  • values自动包装在弱引用或软引用中
  • 条目剔除通知
  • 缓存访问统计

二. 数据加载

Caffeine提供以下四种类型的加载策略:

1. Manual手动

public static void demo(){
		Cache<String,String> cache = Caffeine.newBuilder()
			.expireAfterWrite(20, TimeUnit.SECONDS)
			.maximumSize(5000)
			.build();
		// 1.Insert or update an entry
		cache.put(\"hello\",\"world\");
		// 2. Lookup an entry, or null if not found
		String val1 = cache.getIfPresent(\"hello\");
		// 3. Lookup and compute an entry if absent, or null if not computable
		cache.get(\"msg\", k -> createExpensiveGraph(k));
		// 4. Remove an entry
		cache.invalidate(\"hello\");
	}
	private static String createExpensiveGraph(String key){
		System.out.println(\"begin to query db...\"+Thread.currentThread().getName());
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
		}
		System.out.println(\"success to query db...\");
		return UUID.randomUUID().toString();
	}

Cache接口可以显式地控制检索、更新和删除Entry

2. Loading自动

private static void demo() {
		LoadingCache<String, String> cache = Caffeine.newBuilder()
			.expireAfterWrite(5, TimeUnit.SECONDS)
			.maximumSize(500)
			.build(new CacheLoader<String, String>() {
				@Override
				public String load(String key) throws Exception {
					return createExpensiveGraph(key);
				}
				@Override
				public Map<String, String>  loadAll(Iterable<? extends String> keys) {
					System.out.println(\"build keys\");
					Map<String,String> map = new HashMap<>();
					for(String k : keys){
						map.put(k,k+\"-val\");
					}
					return map;
				}
			});
		String val1 = cache.get(\"hello\");
		Map<String,String> values = cache.getAll(Lists.newArrayList(\"key1\", \"key2\"));
	}
	private static String createExpensiveGraph(String key){
		System.out.println(\"begin to query db...\"+Thread.currentThread().getName());
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
		}
		System.out.println(\"success to query db...\");
		return UUID.randomUUID().toString();
	}

LoadingCache通过关联一个CacheLoader来构建Cache, 当缓存未命中会调用CacheLoader的load方法生成V

还可以通过LoadingCache的getAll方法批量查询, 当CacheLoader未实现loadAll方法时, 会批量调用load方法聚合会返回.

当CacheLoader实现loadAll方法时, 则直接调用loadAll返回.

public interface CacheLoader<K, V>{
    V load(@NonNull K var1) throws Exception;
    Map<K, V> loadAll(@NonNull Iterable<? extends K> keys);
}  

3. Asynchronous Manual异步手动

private static void demo() throws ExecutionException, InterruptedException {
		AsyncCache<String,String> cache = Caffeine.newBuilder()
			.maximumSize(500)
			.expireAfterWrite(10, TimeUnit.SECONDS)
			.buildAsync();
		// Lookup and asynchronously compute an entry if absent
		CompletableFuture<String> future = cache.get(\"hello\", k -> createExpensiveGraph(k));
		System.out.println(future.get());
	}
	private static String createExpensiveGraph(String key){
		System.out.println(\"begin to query db...\"+Thread.currentThread().getName());
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
		}
		System.out.println(\"success to query db...\");
		return UUID.randomUUID().toString();
	}

AsyncCache是另一种Cache,它基于Executor计算Entry,并返回一个CompletableFuture

和Cache的区别是, AsyncCache计算Entry的线程是ForkJoinPool线程池. 手动Cache缓存是调用线程进行计算

4. Asynchronously Loading异步自动

public static void demo() throws ExecutionException, InterruptedException {
		AsyncLoadingCache<String,String> cache = Caffeine.newBuilder()
			.expireAfterWrite(10, TimeUnit.SECONDS)
			.maximumSize(500)
			.buildAsync(k -> createExpensiveGraph(k));
		CompletableFuture<String> future = cache.get(\"hello\");
		System.out.println(future.get());
	}
	private static String createExpensiveGraph(String key){
		System.out.println(\"begin to query db...\"+Thread.currentThread().getName());
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
		}
		System.out.println(\"success to query db...\");
		return UUID.randomUUID().toString();
	}

AsyncLoadingCache 是关联了 AsyncCacheLoader 的 AsyncCache

三. 数据驱逐

Caffeine提供以下几种剔除方式:基于大小、基于权重、基于时间、基于引用

1. 基于容量

又包含两种, 基于size和基于weight权重

基于size

LoadingCache<String,String> cache = Caffeine.newBuilder()
			.maximumSize(500)
			.recordStats()
			.build( k -> UUID.randomUUID().toString());
		for (int i = 0; i < 600; i++) {
			cache.get(String.valueOf(i));
			if(i> 500){
				CacheStats stats = cache.stats();
				System.out.println(\"evictionCount:\"+stats.evictionCount());
				System.out.println(\"stats:\"+stats.toString());
			}
		}

如果缓存的条目数量不应该超过某个值,那么可以使用Caffeine.maximumSize(long)。如果超过这个值,则会剔除很久没有被访问过或者不经常使用的那个条目。

上述测试并不是i=500时, 而是稍微延迟于i的增加, 说明驱逐是另外一个线程异步进行的

基于权重

LoadingCache<Integer,String> cache = Caffeine.newBuilder()
			.maximumWeight(300)
			.recordStats()
			.weigher((Weigher<Integer, String>) (key, value) -> {
				if(key % 2 == 0){
					return 2;
				}
				return 1;
			})
			.build( k -> UUID.randomUUID().toString());
		for (int i = 0; i < 300; i++) {
			cache.get(i);
			if(i> 200){
				System.out.println(cache.stats().toString());
			}
		}

如果,不同的条目有不同的权重值的话(不同的实例占用空间大小不一样),那么你可以用Caffeine.weigher(Weigher)来指定一个权重函数,并且使用Caffeine.maximumWeight(long)来设定最大的权重值。

上述测试并不是i=200时, 而是稍微延迟于i的增加, 说明驱逐是另外一个线程异步进行的

简单的来说,要么限制缓存条目的数量,要么限制缓存条目的权重值,二者取其一。

2. 基于时间

基于时间又分为四种: expireAfterAccess、expireAfterWrite、refreshAfterWrite、expireAfter

expireAfterAccess

超时未访问则失效: 访问包括读和写

private static LoadingCache<String,String> cache = Caffeine.newBuilder()
		.expireAfterAccess(1, TimeUnit.SECONDS)
		.build(key -> UUID.randomUUID().toString());

特征:

  • 访问包括读和写入
  • 数据失效后不会主动重新加载, 必须依赖下一次访问. (言外之意: 失效和回源是两个动作)
  • key超时失效或不存在,若多个线程并发访问, 只有1个线程回源数据,其他线程阻塞等待数据返回
  • 对同一数据一直访问, 且间隔小于失效时间, 则不会去load数据, 一直读到的是脏数据

expireAfterWrite

写后超时失效

private static LoadingCache<String,String> cache = Caffeine.newBuilder()
		.expireAfterWrite(1, TimeUnit.SECONDS)
		.build(key -> UUID.randomUUID().toString());

特征:

数据失效后不会主动重新加载, 必须依赖下一次访问. (言外之意: 失效和回源是两个动作)

key超时失效或不存在,若多个线程并发访问, 只有1个线程回源数据,其他线程阻塞等待数据返回

expire后来访问一定能保证拿到最新的数据

refreshAfterWrite

private static LoadingCache<String,String> cache = Caffeine.newBuilder()
		.refreshAfterWrite(1, TimeUnit.SECONDS)
		.build(key -> UUID.randomUUID().toString());

和expireAfterWrite类似基于写后超时驱逐, 区别是重新load的操作不一样.

特征:

  • 数据失效后不会主动重新加载, 必须依赖下一次访问. (言外之意: 失效和回源是两个动作)
  • 当cache命中未命中时, 若多个线程并发访问时, 只有1个线程回源数据,其他线程阻塞等待数据返回
  • 当cache命中失效数据时, 若多个线程并发访问时, 第一个访问的线程提交一个load数据的任务到公共线程池,然后和所有其他访问线程一样直接返回旧值

实际通过LoadingCache.refresh(K)进行异步刷新, 如果想覆盖默认的刷新行为, 可以实现CacheLoader.reload(K, V)方法

expireAfter

比较少用

public static void demo(){
		MyTicker ticker = new MyTicker();
		LoadingCache<String,String> cache = Caffeine.newBuilder()
			.maximumSize(500)
			.ticker(ticker)
			//此时的效果为expireAfterWrite(5,TimeUnit.SECONDS)
			.expireAfter(new Expiry<String, String>() {
				//1.如果写入key时是第一次创建,则调用该方法返回key剩余的超时时间, 单位纳秒ns
				//currentTime为当前put时Ticket的时间,单位ns
				@Override
				public long expireAfterCreate(String key,String value, long currentTime) {
					System.out.println(\"write first currentTime:\"+currentTime/1_000_000_000L);
					return 5_000_000_000L;//5s
				}
				//2.如果写入key时已经存在即更新key时,则调用该方法返回key剩余的超时时间, 单位纳秒ns
				//currentTime为当前put时Ticket的时间,单位ns,durationTime为旧值(上次设置)剩余的存活时间,单位是ns
				@Override
				public long expireAfterUpdate(String key,String value, long currentTime,long durationTime) {
					System.out.println(\"update currentTime:\"+currentTime/1_000_000_000L+\",leftTime:\"+durationTime/1_000_000_000L);
					return 5_000_000_000L;//5s
				}
				//3.如果key被访问时,则调用该方法返回key剩余的超时时间, 单位纳秒ns
				//currentTime为read时Ticket的时间,单位ns,durationTime为旧值(上次设置)剩余的存活时间,单位是ns
				@Override
				public long expireAfterRead(String key,String value, long currentTime,long durationTime) {
					System.out.println(\"read currentTime:\"+currentTime/1_000_000_000L+\",leftTime:\"+durationTime/1_000_000_000L);
					return durationTime;
				}
			})
			.build(k ->  UUID.randomUUID().toString());
		cache.get(\"key1\");//触发expireAfterCreate
		ticker.advance(1, TimeUnit.SECONDS);//模拟时间消逝
		cache.get(\"key1\");//触发expireAfterRead,剩余生存时间4s
		ticker.advance(2, TimeUnit.SECONDS);//模拟时间消逝
		cache.put(\"key1\",\"value1\");//触发expireAfterUpdate,重置生存时间为5s
		ticker.advance(3, TimeUnit.SECONDS);//模拟时间消逝
		cache.get(\"key1\");//触发expireAfterCreate,剩余生存时间为2s
	}
public class MyTicker implements Ticker {
	private final AtomicLong nanos = new AtomicLong();
	//模拟时间消逝
	public void advance(long time, TimeUnit unit) {
		this.nanos.getAndAdd(unit.toNanos(time));
	}
	@Override
	public long read() {
		return this.nanos.get();
	}
}

上述实现了Expiry接口, 分别重写了expireAfterCreate、expireAfterUpdate、expireAfterRead方法, 当第一次写入时、更新时、读访问时会分别调用这三个方法有机会重新设置剩余的失效时间, 上述案例模拟了expireAfterWrite(5,TimeUnit.SECONDS)的效果.

注意点:

  • 以上基于时间驱逐, 数据超时失效和回源是两个动作, 必须依赖下一次访问. 为了避免服务启动时大量缓存穿透, 可以通过提前项目启动时手动预热
  • 一般expireAfterWrite和refreshAfterWrite结合使用, expire的时间t1大于refresh的时间t2, 在t2~t1内数据更新允许脏数据, t1之后必须要重新同步加载新数据

3. 基于弱/软引用

/**
	 * 允许GC时回收keys或values
	 */
	public static void demo(){
		LoadingCache<String,String> cache = Caffeine.newBuilder()
			.maximumSize(500)
			.expireAfterWrite(10, TimeUnit.SECONDS)
			.weakKeys()
			.weakValues()
			.build(k -> UUID.randomUUID().toString());
	}

Caffeine.weakKeys() 使用弱引用存储key。如果没有强引用这个key,则GC时允许回收该条目

Caffeine.weakValues() 使用弱引用存储value。如果没有强引用这个value,则GC时允许回收该条目

Caffeine.softValues() 使用软引用存储value, 如果没有强引用这个value,则GC内存不足时允许回收该条目

public static void demo(){
		/**
		 * 使用软引用存储value,GC内存不够时会回收
		 */
		LoadingCache<String,String> cache = Caffeine.newBuilder()
			.maximumSize(500)
			.expireAfterWrite(10, TimeUnit.SECONDS)
			.softValues()//注意没有softKeys方法
			.build(k -> UUID.randomUUID().toString());
	}

Java4种引用的级别由高到低依次为:强引用 > 软引用 > 弱引用 > 虚引用

引用类型 被垃圾回收时间 用途 生存时间
强引用 从来不会 对象的一般状态 JVM停止运行时终止
软引用 在内存不足时 对象缓存 内存不足时终止
弱引用 在垃圾回收时 对象缓存 gc运行后终止
虚引用 Unknown Unknown Unknown

四. 驱逐监听

  • eviction 指受策略影响而被删除
  • invalidation 值被调用者手动删除
  • removal 值因eviction或invalidation而发生的一种行为

1. 手动触发删除

// individual key
cache.invalidate(key)
// bulk keys
cache.invalidateAll(keys)
// all keys
cache.invalidateAll()

2. 被驱逐的原因

  • EXPLICIT:如果原因是这个,那么意味着数据被我们手动的remove掉了
  • REPLACED:就是替换了,也就是put数据的时候旧的数据被覆盖导致的移除
  • COLLECTED:这个有歧义点,其实就是收集,也就是垃圾回收导致的,一般是用弱引用或者软引用会导致这个情况
  • EXPIRED:数据过期,无需解释的原因。
  • SIZE:个数超过限制导致的移除

3. 监听器

public static void demo(){
		LoadingCache<String,String> cache = Caffeine.newBuilder()
			.maximumSize(5)
			.recordStats()
			.expireAfterWrite(2, TimeUnit.SECONDS)
			.removalListener((String key, String value, RemovalCause cause) -> {
				System.out.printf(\"Key %s was removed (%s)%n\", key, cause);
			})
			.build(key -> UUID.randomUUID().toString());
		for (int i = 0; i < 15; i++) {
			cache.get(i+\"\");
			try {
				Thread.sleep(200);
			} catch (InterruptedException e) {
			}
		}
		//因为evict是异步线程去执行,为了看到效果稍微停顿一下
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
		}
	}

日志打印如下:

Key 0 was removed (SIZE)
Key 1 was removed (SIZE)
Key 6 was removed (SIZE)
Key 7 was removed (SIZE)
Key 8 was removed (SIZE)
Key 9 was removed (SIZE)
Key 10 was removed (SIZE)
Key 2 was removed (EXPIRED)
Key 3 was removed (EXPIRED)
Key 4 was removed (EXPIRED)

五. 统计

public static void demo(){
		LoadingCache<Integer,String> cache = Caffeine.newBuilder()
			.maximumSize(10)
			.expireAfterWrite(10, TimeUnit.SECONDS)
			.recordStats()
			.build(key -> {
				if(key % 6 == 0 ){
					return null;
				}
				return  UUID.randomUUID().toString();
			});
		for (int i = 0; i < 20; i++) {
			cache.get(i);
			printStats(cache.stats());
		}
		for (int i = 0; i < 10; i++) {
			cache.get(i);
			printStats(cache.stats());
		}
	}
	private static void printStats(CacheStats stats){
		System.out.println(\"---------------------\");
		System.out.println(\"stats.hitCount():\"+stats.hitCount());//命中次数
		System.out.println(\"stats.hitRate():\"+stats.hitRate());//缓存命中率
		System.out.println(\"stats.missCount():\"+stats.missCount());//未命中次数
		System.out.println(\"stats.missRate():\"+stats.missRate());//未命中率
		System.out.println(\"stats.loadSuccessCount():\"+stats.loadSuccessCount());//加载成功的次数
		System.out.println(\"stats.loadFailureCount():\"+stats.loadFailureCount());//加载失败的次数,返回null
		System.out.println(\"stats.loadFailureRate():\"+stats.loadFailureRate());//加载失败的百分比
		System.out.println(\"stats.totalLoadTime():\"+stats.totalLoadTime());//总加载时间,单位ns
		System.out.println(\"stats.evictionCount():\"+stats.evictionCount());//驱逐次数
		System.out.println(\"stats.evictionWeight():\"+stats.evictionWeight());//驱逐的weight值总和
		System.out.println(\"stats.requestCount():\"+stats.requestCount());//请求次数
		System.out.println(\"stats.averageLoadPenalty():\"+stats.averageLoadPenalty());//单次load平均耗时
	}

六. 其他

1. Ticker

时钟, 方便测试模拟时间流逝

public static void demo(){
		MyTicker ticker = new MyTicker();
		LoadingCache<String,String> cache = Caffeine.newBuilder()
			.maximumSize(500)
			.ticker(ticker)
			.expireAfterWrite(1, TimeUnit.SECONDS)
			.build(k ->  UUID.randomUUID().toString());
		cache.get(\"key1\");//触发expireAfterCreate
		ticker.advance(1, TimeUnit.SECONDS);//模拟时间消逝
		cache.get(\"key1\");//触发expireAfterRead,剩余生存时间4s
		ticker.advance(2, TimeUnit.SECONDS);//模拟时间消逝
		cache.put(\"key1\",\"value1\");//触发expireAfterUpdate,重置生存时间为5s
	}
public class MyTicker implements Ticker {
	private final AtomicLong nanos = new AtomicLong();
	//模拟时间消逝
	public void advance(long time, TimeUnit unit) {
		this.nanos.getAndAdd(unit.toNanos(time));
	}
	@Override
	public long read() {
		return this.nanos.get();
	}
}

2. Scheduler

3. 类图及API

资源下载此资源下载价格为1小猪币,终身VIP免费,请先
由于本站资源来源于互联网,以研究交流为目的,所有仅供大家参考、学习,不存在任何商业目的与商业用途,如资源存在BUG以及其他任何问题,请自行解决,本站不提供技术服务! 由于资源为虚拟可复制性,下载后不予退积分和退款,谢谢您的支持!如遇到失效或错误的下载链接请联系客服QQ:442469558

:本文采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可, 转载请附上原文出处链接。
1、本站提供的源码不保证资源的完整性以及安全性,不附带任何技术服务!
2、本站提供的模板、软件工具等其他资源,均不包含技术服务,请大家谅解!
3、本站提供的资源仅供下载者参考学习,请勿用于任何商业用途,请24小时内删除!
4、如需商用,请购买正版,由于未及时购买正版发生的侵权行为,与本站无关。
5、本站部分资源存放于百度网盘或其他网盘中,请提前注册好百度网盘账号,下载安装百度网盘客户端或其他网盘客户端进行下载;
6、本站部分资源文件是经压缩后的,请下载后安装解压软件,推荐使用WinRAR和7-Zip解压软件。
7、如果本站提供的资源侵犯到了您的权益,请邮件联系: 442469558@qq.com 进行处理!

猪小侠源码-最新源码下载平台 Java教程 Caffeine本地缓存示例详解 http://www.20zxx.cn/806723/xuexijiaocheng/javajc.html

猪小侠源码,优质资源分享网

常见问题
  • 本站所有资源版权均属于原作者所有,均只能用于参考学习,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担
查看详情
  • 最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,建议提前注册好百度网盘账号,使用百度网盘客户端下载
查看详情

相关文章

官方客服团队

为您解决烦忧 - 24小时在线 专业服务