高性能内存缓存库caffeine入门实践

3,315 阅读15分钟

一、学习目标

1、掌握Caffeine的3种填充策略:

  • 手动填充数据
  • 同步加载
  • 异步加载

2、掌握Caffeine提供了3种回收策略:

  • 基于大小回收
  • 基于时间回收
  • 基于引用回收

3、Caffeine的移除监听器

如果我们需要在缓存被移除的时候,得到通知产生回调,并做一些额外处理工作。这个时候RemovalListener就派上用场了。

删除侦听器的里面的操作是使用Executor来异步执行的。默认执行程序是ForkJoinPool.commonPool(),可以通过Caffeine.executor(Executor)覆盖。

4、Caffeine的CacheWriter

CacheWriter允许缓存充当一个底层资源的代理,当与CacheLoader结合使用时,所有对缓存的读写操作都可以通过Writer进行传递。Writer可以把操作缓存和操作外部资源扩展成一个同步的原子性操作。并且在缓存写入完成之前,它将会阻塞后续的更新缓存操作,但是读取(get)将直接返回原有的值。 如果写入程序失败,那么原有的key和value的映射将保持不变,如果出现异常将直接抛给调用者。

能监听到的动作:

  • CacheWriter可以同步的监听到缓存的创建、变更和删除操作。

不能监听到的动作:

  • 加载(例如,LoadingCache.get)、重新加载(例如,LoadingCache.refresh)和计算(例如Map.computeIfPresent)的操作不被CacheWriter监听到。

注意事项:

  • CacheWriter不能与弱键或AsyncLoadingCache一起使用。

应用场景:

  • 可以配合外部存储使用

5、Caffeine的统计

6、Caffeine的刷新

  • 刷新并不是到期就刷新,而是对这个数据再次访问之后,才会刷新。只阻塞加载数据的线程,其余线程返回旧数据。

二、Caffeine简介

Caffeine是基于Java8的高性能缓存库,可提供接近最佳的命中率。Caffeine的底层使用了ConcurrentHashMap,支持按照一定的规则或者自定义的规则使缓存的数据过期,然后销毁。

Caffeine的特性:

  • 自动把数据加载到本地缓存中,并且可以配置异步;
  • 淘汰策略:基于数量剔除策略;基于失效时间剔除策略,这个时间是从最后一次操作算起【访问或者写入】;
  • 异步刷新;
  • Key会被包装成Weak引用;Value会被包装成Weak或者Soft引用,从而能被GC掉,而不至于内存泄漏;
  • 数据剔除提醒;
  • 写入广播机制;
  • 缓存访问可以统计;

Caffeine的内部结构:

  • Cache的内部包含着一个ConcurrentHashMap:这也是存放我们所有缓存数据的地方,众所周知,ConcurrentHashMap是一个并发安全的容器,这点很重要,可以说Caffeine其实就是一个被强化过的ConcurrentHashMap。
  • Scheduler:定期清空数据的一个机制,可以不设置,如果不设置则不会主动的清空过期数据。
  • Executor:指定运行异步任务时要使用的线程池。可以不设置,如果不设置则会使用默认的线程池,也就是ForkJoinPool.commonPool()。

三、Caffeine的3种填充策略

Caffeine提供了3种填充策略:手动填充数据,同步加载,异步加载。

1、手动填充数据:

public class CaffeineManualTest {

	@Test
	public void test() {
		// 初始化缓存,设置了1分钟的写过期,100的缓存最大个数
        Cache<Integer, String> cache = Caffeine.newBuilder()
                .expireAfterWrite(1, TimeUnit.MINUTES)
                .maximumSize(100)
                .build();

        int key1 = 1;

        // getIfPresent(Object key):根据key从缓存中获取值,如果没有返回NULL
        System.out.println(cache.getIfPresent(key1));

        // get(key, Function):根据key查询一个缓存,如果没有将返回提供默认值,并保存到缓存
        // 通过cache.put(key, value)方法显示的将数控放入缓存,但是这样子会覆盖缓原来key的数据,更加建议使用cache.get(key,k - > value) 的方式
        System.out.println(cache.get(key1, new Function<Integer, String>() {
            @Override
            public String apply(Integer key) {
                System.out.println(key);
                return "abcd";
            }
        }));

        System.out.println(cache.getIfPresent(key1));

        // put(key, value):会覆盖缓原来key的数据
        String value1 = "wxyz";
        cache.put(key1, value1);

        System.out.println(cache.getIfPresent(key1));

        // invalidate(key):移除数据,让数据失效
        cache.invalidate(key1);
        System.out.println(cache.getIfPresent(key1));
	}

}

如果同时有多个线程进行get,那么这个Function对象是否会被执行多次呢?

  • 实际上不会的,可以从结构图看出,Caffeine内部最主要的数据结构就是一个ConcurrentHashMap,而get的过程最终执行的便是ConcurrentHashMap.compute,这里仅会被执行一次。
  • get 方法是以阻塞方式执行调用,即使多个线程同时请求该值也只会调用一次Function方法。这样可以避免与其他线程的写入竞争,这也是为什么使用 get 优于 getIfPresent 的原因。

2、同步加载:

利用这个同步机制,也就是在CacheLoader对象中的load函数中,当从Caffeine缓存中取不到数据的时候,则从数据库中读取数据,将其插入缓存中。通过这个机制和数据库结合使用。

public class CaffeineLoadingTest {

	/**
	 * 从数据库获取数据
	 *
	 * @param key
	 * @return
	 */
	private String getFromDataBase(int key) {
		return key + RandomStringUtils.randomAlphabetic(8);
	}

	@Test
	public void test() {
		// 初始化缓存,设置了1分钟的写过期,100的缓存最大个数
		LoadingCache<Integer, String> cache = Caffeine.newBuilder()
				.expireAfterWrite(1, TimeUnit.MINUTES)
				.maximumSize(100)
				.build(
						new CacheLoader<Integer, String>() {

							/**
							 * 默认情况下,get、getAll将会对缓存中没有值的key分别调用CacheLoader.load方法来构建缓存的值。
							 *
							 * @param key
							 * @return
							 */
							@Override
							public String load(@NonNull Integer key) {
								return getFromDataBase(key);
							}

							/**
							 * 在loadAll被重写的情况下,getAll将会对缓存中没有值的key分别调用CacheLoader.loadAll方法来构建缓存的值。
							 *
							 * @param keys
							 * @return
							 */
							@Override
							public @NonNull Map<Integer, String> loadAll(@NonNull Iterable<? extends Integer> keys) {
								Map<Integer, String> map = new HashMap<>();
								for (Integer key : keys) {
									map.put(key, getFromDataBase(key) + "abcd");
								}

								return map;
							}
						}
				);

		int key1 = 100;

		// get(key, Function):根据key查询一个缓存,如果没有将返回提供默认值,并保存到缓存
		// 通过cache.put(key, value)方法显示的将数控放入缓存,但是这样子会覆盖缓原来key的数据,更加建议使用cache.get(key,k - > value) 的方式
		String value1 = cache.get(key1);
		System.out.println(value1);

		//  getAll(keys):批量查找
		Map<Integer, String> dataMap = cache.getAll(Arrays.asList(1, 2, 3, 100));
		System.out.println(dataMap);
	}
}

运行结果:

100FDQHepbj
{1=1UwoFIuYoabcd, 2=2eDQKMwqpabcd, 3=3HYnJXUsRabcd, 100=100FDQHepbj}

3、异步加载:

public class CaffeineAsynchronousTest {

	/**
	 * 从数据库获取数据
	 *
	 * @param key
	 * @return
	 */
	private String getFromDataBase(int key) {
		System.out.println("2当前所在线程:" + Thread.currentThread().getName());

		try {
			Thread.sleep(10_000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

		return key + RandomStringUtils.randomAlphabetic(8);
	}

	private CompletableFuture<String> getValue(Integer key) {
		System.out.println("2当前所在线程:" + Thread.currentThread().getName());

		try {
			Thread.sleep(10_000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

		return CompletableFuture.supplyAsync(() -> key + RandomStringUtils.randomAlphabetic(8));
	}

	@Test
	public void test2() throws ExecutionException, InterruptedException {
		AsyncLoadingCache<Integer, String> asyncLoadingCache = Caffeine.newBuilder()
				.expireAfterWrite(10, TimeUnit.MINUTES)
				.maximumSize(10_000)

				// 使用executor设置线程池
				// .executor(Executors.newSingleThreadExecutor())

				.buildAsync(key -> getFromDataBase(key));

		// .buildAsync(key -> getValue(key).get(1, TimeUnit.SECONDS));


		Integer key = 1;

		// put(key, value):会覆盖缓原来key的数据
		// 此时不会执行setAsyncValue()、getFromDataBase()
		String value1 = "wxyz1";
		asyncLoadingCache.put(key, CompletableFuture.completedFuture(value1));

		System.out.println("1当前所在线程:" + Thread.currentThread().getName());

		CompletableFuture<String> future = asyncLoadingCache.get(key);
		String value = future.get();

		System.out.println(value);
	}
}

四、Caffeine的3种回收策略

Caffeine提供了3种回收策略:基于大小回收,基于时间回收,基于引用回收。

1、基于大小回收:

基于最大缓存数量:

@Test
public void test() throws InterruptedException {
    // 初始化缓存,设置了1分钟的写过期,2的缓存最大个数
    Cache<Integer, String> cache = Caffeine.newBuilder()
            .maximumSize(2)
            .build();

    cache.put(1, "a");
    System.out.println(cache.estimatedSize());

    cache.put(2, "b");
    System.out.println(cache.estimatedSize());

    System.out.println(cache.getAllPresent(Lists.newArrayList(1, 2)));

    // 触发缓存回收(异步)
    cache.put(3, "c");
    System.out.println(cache.estimatedSize());

    System.out.println(cache.getAllPresent(Lists.newArrayList(1, 2, 3)));

    // 触发缓存回收(异步)
    cache.put(4, "d");
    System.out.println(cache.estimatedSize());

    // 缓存回收是异步执行,cleanUp()可以等待异步执行完成
    // cache.cleanUp();

    Thread.sleep(1000);

    System.out.println(cache.getAllPresent(Lists.newArrayList(1, 2, 3, 4)));
}

运行结果:

1
2
{1=a, 2=b}
3
{1=a, 2=b, 3=c}
4
{2=b, 4=d}

基于权重:

/**
 * 基于权重
 */
@Test
public void testMaximumWeight() {
    Cache<Integer, Integer> cache = Caffeine.newBuilder()
            // TODO 指定指定缓存最大权重值,配合weigher使用,随着缓存大小逐渐接近最大值,会回收最近或被很少使用的缓存,maximumWeight与maximumSize不能同时使用
            .maximumWeight(11)
            // 指定权重计算方式,缓存创建或更新时触发
            .weigher(new Weigher<Object, Object>() {
                @Override
                public @NonNegative int weigh(@NonNull Object key, @NonNull Object value) {
                    if (value instanceof Integer) {
                        System.out.println("是Integer类型的: " + value);

                        return (Integer) value;
                    }

                    System.out.println("不是Integer类型的: " + value);

                    return 0;
                }
            })
            .build();

    List<Integer> keys = new ArrayList<>();
    for (int i = 0; i < 100; i++) {
        cache.put(i, i);
        keys.add(i);
    }

    Map<Integer, Integer> map = cache.getAllPresent(keys);
    map.forEach((k, v) -> System.out.println(k + ":" + v));

    cache.cleanUp();

    System.out.println();

    map = cache.getAllPresent(keys);
    map.forEach((k, v) -> System.out.println(k + ":" + v));
    int sum = 0;
    for (Integer ele : map.keySet()) {
        sum += ele;
    }
    System.out.println("元素总和: " + sum );

    // Thread.sleep(1000);

    System.out.println(cache.estimatedSize());
}

运行结果:

是Integer类型的: 0
是Integer类型的: 1
是Integer类型的: 2
是Integer类型的: 3
...
是Integer类型的: 97
是Integer类型的: 98
是Integer类型的: 99
0:0
1:1
2:2
3:3
...
97:97
98:98
99:99

0:0
8:8
元素总和: 8
2

2、基于时间回收:

  • expireAfterAccess(long, TimeUnit):在最后一次访问或者写入后开始计时,在指定的时间后过期。假如一直有请求访问该key,那么这个缓存将一直不会过期。
  • expireAfterWrite(long, TimeUnit):在最后一次写入缓存后开始计时,在指定的时间后过期。
  • expireAfter(Expiry):自定义策略,过期时间由Expiry实现独自计算。

注意事项:

  • expireAfterWrite和expireAfterAccess同时存在时,以expireAfterWrite为准。
  • 缓存的删除策略使用的是惰性删除和定时删除。这两个删除策略的时间复杂度都是O(1)。
public class CaffeineEvictionPolicyBaseOnTimeTest {

	/**
	 * 从数据库获取数据
	 *
	 * @param key
	 * @return
	 */
	private String getFromDataBase(int key) {
		return key + RandomStringUtils.randomAlphabetic(8);
	}

	/**
	 * 到期策略
	 *
	 * @throws InterruptedException
	 */
	@Test
	public void testExpirePolicy() throws InterruptedException {
		// 基于固定的到期策略进行回收
		LoadingCache<Integer, Object> cache = Caffeine.newBuilder()
				// TODO 在最后一次访问或者写入后开始计时,在指定的时间后过期。假如一直有请求访问该key,那么这个缓存将一直不会过期。
				.expireAfterAccess(5, TimeUnit.MINUTES)
				.build(key -> getFromDataBase(key));

		LoadingCache<Integer, Object> cache1 = Caffeine.newBuilder()
				// TODO 在最后一次写入缓存后开始计时,在指定的时间后过期。
				.expireAfterWrite(10, TimeUnit.MINUTES)
				.build(key -> getFromDataBase(key));
	}

	/**
	 * 自定义到期策略
	 */
	@Test
	public void testCustomerExpirePolicy() {
		LoadingCache<Integer, Object> cache2 = Caffeine.newBuilder()
				.expireAfter(new Expiry<Object, Object>() {
					@Override
					public long expireAfterCreate(@NonNull Object key, @NonNull Object value, long currentTime) {
						return 0;
					}

					@Override
					public long expireAfterUpdate(@NonNull Object key, @NonNull Object value, long currentTime, @NonNegative long currentDuration) {
						return 0;
					}

					@Override
					public long expireAfterRead(@NonNull Object key, @NonNull Object value, long currentTime, @NonNegative long currentDuration) {
						return 0;
					}
				}).build(key -> getFromDataBase(key));
	}
}

3、基于引用回收:

  • weakKeys(): 使用弱引用存储key。如果没有其他地方对该key有强引用,那么该缓存就会被垃圾回收器回收。由于垃圾回收器只依赖于身份(identity)相等,因此这会导致整个缓存使用身份 (==) 相等来比较 key,而不是使用 equals()。
  • weakValues() :使用弱引用存储value。如果没有其他地方对该value有强引用,那么该缓存就会被垃圾回收器回收。由于垃圾回收器只依赖于身份(identity)相等,因此这会导致整个缓存使用身份 (==) 相等来比较 key,而不是使用 equals()。
  • softValues() :使用软引用存储value。当内存满了过后,软引用的对象以将使用最近最少使用(least-recently-used ) 的方式进行垃圾回收。由于使用软引用是需要等到内存满了才进行回收,所以我们通常建议给缓存配置一个使用内存的最大值。 softValues() 将使用身份相等(identity) (==) 而不是equals() 来比较值。

注意事项:

  • AsyncLoadingCache不支持弱引用和软引用
  • Caffeine.weakValues()和Caffeine.softValues()不可以一起使用
  • 使用到的回收策略时LRU算法
public class CaffeineEvictionPolicyBaseOnReferenceTest {

	/**
	 * 从数据库获取数据
	 *
	 * @param key
	 * @return
	 */
	private String getFromDataBase(int key) {
		return key + RandomStringUtils.randomAlphabetic(8);
	}

	@Test
	public void test() {

		LoadingCache<Integer, String> cache1 = Caffeine.newBuilder()
				// 当key和value都没有引用时(都为null)驱逐缓存
				.weakKeys()
				.weakValues()
				.build(key -> getFromDataBase(key));


		LoadingCache<Integer, String> cache2 = Caffeine.newBuilder()
				// 当垃圾收集器需要释放内存时驱逐
				.softValues()
				.build(key -> getFromDataBase(key));
	}
}

五、Caffeine的移除监听器

如果我们需要在缓存被移除的时候,得到通知产生回调,并做一些额外处理工作。这个时候RemovalListener就派上用场了。

删除侦听器的里面的操作是使用Executor来异步执行的。默认执行程序是ForkJoinPool.commonPool(),可以通过Caffeine.executor(Executor)覆盖。

public class CaffeineRemovalTest {

	@Test
	public void test() throws InterruptedException {
		Cache<Integer, String> cache = Caffeine.newBuilder()
				// TODO 指定最大缓存数量,如果超过这个值,则会剔除很久没有被访问过或者不经常使用的缓存
				.maximumSize(2)
				.removalListener(new RemovalListener<Object, Object>() {
					@Override
					public void onRemoval(@Nullable Object key, @Nullable Object value, @NonNull RemovalCause cause) {
						System.out.println("【日志打印】触发了 " + cause.name() + " 策略, 清除的key = " + key + ", value=" + value);
					}
				})
				.build();

		cache.put(1, "a");
		System.out.println(cache.estimatedSize());

		cache.put(2, "b");
		System.out.println(cache.estimatedSize());

		System.out.println(cache.getAllPresent(Lists.newArrayList(1, 2)));

		// 触发缓存回收(异步)
		cache.put(3, "c");
		System.out.println(cache.estimatedSize());

		System.out.println(cache.getAllPresent(Lists.newArrayList(1, 2, 3)));

		// 触发缓存回收(异步)
		cache.put(4, "d");
		System.out.println(cache.estimatedSize());

		// 缓存回收是异步执行,cleanUp()可以等待异步执行完成
		// cache.cleanUp();

		Thread.sleep(1000);

		System.out.println(cache.getAllPresent(Lists.newArrayList(1, 2, 3, 4)));

		cache.put(5, "e");
		// 手动删除缓存
		cache.invalidate(5);

		System.out.println(cache.getAllPresent(Lists.newArrayList(1, 2, 3, 4)));
	}
}

运行结果:

1
2
{1=a, 2=b}
3
{1=a, 2=b, 3=c}
4
【日志打印】触发了 SIZE 策略, 清除的key = 1, value=a
【日志打印】触发了 SIZE 策略, 清除的key = 3, value=c
{2=b, 4=d}
【日志打印】触发了 EXPLICIT 策略, 清除的key = 5, value=e
【日志打印】触发了 SIZE 策略, 清除的key = 4, value=d
{2=b}

六、Caffeine的CacheWriter

CacheWriter允许缓存充当一个底层资源的代理,当与CacheLoader结合使用时,所有对缓存的读写操作都可以通过Writer进行传递。Writer可以把操作缓存和操作外部资源扩展成一个同步的原子性操作。并且在缓存写入完成之前,它将会阻塞后续的更新缓存操作,但是读取(get)将直接返回原有的值。 如果写入程序失败,那么原有的key和value的映射将保持不变,如果出现异常将直接抛给调用者。

能监听到的动作:

  • CacheWriter可以同步的监听到缓存的创建、变更和删除操作。

不能监听到的动作:

  • 加载(例如,LoadingCache.get)、重新加载(例如,LoadingCache.refresh)和计算(例如Map.computeIfPresent)的操作不被CacheWriter监听到。

注意事项:

  • CacheWriter不能与弱键或AsyncLoadingCache一起使用。

应用场景:

  • 可以配合外部存储使用
public class CaffeineWriterTest {

	/**
	 * 充当二级缓存用,生命周期仅活到下个gc
	 */
	private Map<Integer, WeakReference<Integer>> secondCacheMap = new ConcurrentHashMap<>();

	@Test
	public void test() throws InterruptedException {
		LoadingCache<Integer, Integer> cache = Caffeine.newBuilder()
				.maximumSize(1)
				.writer(
						new CacheWriter<Integer, Integer>() {

							/**
							 * 写入到外部存储
							 *
							 * @param key
							 * @param value
							 */
							@Override
							public void write(@NonNull Integer key, @NonNull Integer value) {
								secondCacheMap.put(key, new WeakReference<>(value));

								System.out.println("写入到外部存储,将key = " + key + "放入二级缓存中");
								System.out.println("二级缓存: " + secondCacheMap);
								System.out.println();
							}

							/**
							 * 删除外部存储
							 *
							 * @param key
							 * @param value
							 * @param cause
							 */
							@Override
							public void delete(@NonNull Integer key, @Nullable Integer value, @NonNull RemovalCause cause) {
								switch (cause) {
									// 手动调用invalidate或remove等方法
									case EXPLICIT:
										secondCacheMap.remove(key);
										System.out.println("删除外部存储, " + cause.name() + " 策略, 将key = " + key);
										System.out.println("二级缓存: " + secondCacheMap);
										System.out.println();
										break;

									// 超出缓存设定大小
									case SIZE:
										secondCacheMap.remove(key);
										System.out.println("删除外部存储, " + cause.name() + " 策略, 将key = " + key);
										System.out.println("二级缓存: " + secondCacheMap);
										System.out.println();
										break;

									// 调用put等方法进行修改
									case REPLACED:
										secondCacheMap.remove(key);
										System.out.println("删除外部存储, " + cause.name() + " 策略, 将key = " + key);
										System.out.println("二级缓存: " + secondCacheMap);
										System.out.println();
										break;

									// 设置了key或value的引用方式
									case COLLECTED:
										secondCacheMap.remove(key);
										System.out.println("删除外部存储, " + cause.name() + " 策略, 将key = " + key);
										System.out.println("二级缓存: " + secondCacheMap);
										System.out.println();
										break;

									// 设置了过期时间
									case EXPIRED:
										secondCacheMap.remove(key);
										System.out.println("删除外部存储, " + cause.name() + " 策略, 将key = " + key);
										System.out.println("二级缓存: " + secondCacheMap);
										System.out.println();
										break;

									default:
										break;
								}
							}
						}
				)
				.removalListener(new RemovalListener<Object, Object>() {
					@Override
					public void onRemoval(@Nullable Object key, @Nullable Object value, @NonNull RemovalCause cause) {
						System.out.println("【日志打印】触发了 " + cause.name() + " 策略, 清除的key = " + key + ", value=" + value);
					}
				})
				.build(new CacheLoader<Integer, Integer>() {
						   /**
							* 默认情况下,get、getAll将会对缓存中没有值的key分别调用CacheLoader.load方法来构建缓存的值。
							*
							* @param key
							* @return
							*/
						   @Nullable
						   @Override
						   public Integer load(@NonNull Integer key) {
							   WeakReference<Integer> value = secondCacheMap.get(key);
							   if (value == null) {
								   return 100;
							   }

							   System.out.println("触发CacheLoader#load(),从二级缓存读取key = " + key);
							   System.out.println();

							   return value.get();
						   }
					   }
				);

		cache.put(1, 1);
		cache.put(2, 2);

		// 缓存回收是异步执行,cleanUp()可以等待异步执行完成
		// cache.cleanUp();

		Thread.sleep(1000);

		// 根据key查询一个缓存,如果没有将返回提供默认值,并保存到缓存,又超过了缓存最大值,触发清理操作
		System.out.println(cache.get(1));

		Thread.sleep(1000);

		System.out.println(cache.getIfPresent(2));

	}
}

运行结果:

写入到外部存储,将key = 1放入二级缓存中
二级缓存: {1=java.lang.ref.WeakReference@47d384ee}

写入到外部存储,将key = 2放入二级缓存中
二级缓存: {1=java.lang.ref.WeakReference@47d384ee, 2=java.lang.ref.WeakReference@1ff8b8f}

删除外部存储, SIZE 策略, 将key = 1
二级缓存: {2=java.lang.ref.WeakReference@1ff8b8f}

【日志打印】触发了 SIZE 策略, 清除的key = 1, value=1
100
删除外部存储, SIZE 策略, 将key = 2
二级缓存: {}

【日志打印】触发了 SIZE 策略, 清除的key = 2, value=2
null

六、Caffeine的统计

public class CaffeineStatsTest {

	@Test
	public void test() {
		Cache<Integer, String> cache = Caffeine.newBuilder()
				.maximumSize(10_00)
				.recordStats()
				.build();

		for (int i = 0; i < 100; i++) {
			cache.get(i, new Function<Integer, String>() {
				@Override
				public String apply(Integer integer) {
					return  RandomStringUtils.randomAlphanumeric(8);
				}
			});
		}

		for (int i = 0; i < 100; i++) {
			if (i % 2 == 0) {
				System.out.println(i);
				System.out.println(cache.getIfPresent(i));
				System.out.println();
			}
		}

		System.out.println("加载新值所花费的平均时间 totalLoadTime / (loadSuccessCount + loadFailureCount): " + cache.stats().averageLoadPenalty());
		System.out.println("缓存逐出的数量: " + cache.stats().evictionCount());
		System.out.println("缓存逐出的数量(权重): " + cache.stats().evictionWeight());
		System.out.println("返回命中缓存的总数: " + cache.stats().hitCount());
		System.out.println("返回命中与请求的比率: " + cache.stats().hitRate());
		System.out.println("加载新值的总次数: " + cache.stats().loadCount());
		System.out.println("加载新值成功的总次数: " + cache.stats().loadSuccessCount());
		System.out.println("加载新值失败的总次数: " + cache.stats().loadFailureCount());
		System.out.println("加载新值失败的比率: " + cache.stats().loadFailureRate());
		System.out.println("missCount: " + cache.stats().missCount());
		System.out.println("missRate: " + cache.stats().missRate());
		System.out.println("hitCount + missCount: " + cache.stats().requestCount());
		System.out.println("加载新值所花费的总纳秒数: " + cache.stats().totalLoadTime());
	}
}

运行结果:

加载新值所花费的平均时间 totalLoadTime / (loadSuccessCount + loadFailureCount): 23188.66
缓存逐出的数量: 0
缓存逐出的数量(权重): 0
返回命中缓存的总数: 50
返回命中与请求的比率: 0.3333333333333333
加载新值的总次数: 100
加载新值成功的总次数: 100
加载新值失败的总次数: 0
加载新值失败的比率: 0.0
missCount: 100
missRate: 0.6666666666666666
hitCount + missCount: 150
加载新值所花费的总纳秒数: 2318866

七、Caffeine的刷新

刷新并不是到期就刷新,而是对这个数据再次访问之后,才会刷新。只阻塞加载数据的线程,其余线程返回旧数据。

public class CaffeineRefreshTest {

	@Test
	public void test() throws InterruptedException {
		System.out.println("开始时间: " + LocalDateTime.now());
		System.out.println();

		LoadingCache<Integer, String> cache = Caffeine.newBuilder()
				.expireAfterWrite(Duration.ofSeconds(5))
				// TODO 注意这里的刷新并不是到期就刷新,而是对这个数据再次访问之后,才会刷新。只阻塞加载数据的线程,其余线程返回旧数据。
				.refreshAfterWrite(Duration.ofSeconds(2))
				.build(new CacheLoader<Integer, String>() {
						   /**
							* 刷新数据逻辑,调用此方法的线程,获取的是旧数据
							*
							* @param key
							* @return
							* @throws Exception
							*/
						   @Nullable
						   @Override
						   public String load(@NonNull Integer key) throws Exception {
							   System.out.println("刷新时间: " + LocalDateTime.now());
							   System.out.println("2当前所在线程:" + Thread.currentThread().getName());
							   int i = RandomUtils.nextInt(0, 20);
							   System.out.println(i);
							   System.out.println();
							   if (i % 2 == 0) {
								   return "wxyz";
							   } else {
								   return "WXYZ";
							   }


						   }
					   }
				);

		cache.put(1, "abcd");
		String value1 = cache.get(1);
		System.out.println("第1次访问key=1:" + value1);
		System.out.println("1当前所在线程:" + Thread.currentThread().getName());
		System.out.println();

		// 触发刷新后,第一次返回旧数据
		Thread.sleep(3000);
		System.out.println("3s后访问key=1:" + cache.get(1));
		System.out.println();

		// 返回新数据
		String value2 = cache.get(1);
		String result2 = value2.equals(value1) ? "false" : "true";
		System.out.println("第3次获取key=1:" + value2 + ",是否刷新值=" + result2);
		System.out.println();

		// 触发刷新后,第一次返回旧数据
		Thread.sleep(3000);
		String value3 = cache.get(1);
		String result3 = value3.equals(value2) ? "false" : "true";
		System.out.println("6s次后访问key=1:" + value3 + ",是否刷新值=" + result3);

		Thread.sleep(100);

		// 返回新数据
		String value4 = cache.get(1);
		String result4 = value4.equals(value3) ? "false" : "true";
		System.out.println("第4次获取key=1:" + value4 + ",是否刷新值=" + result4);
	}
}

运行结果:

开始时间: 2020-12-31T17:42:02.721

第1次访问key=1:abcd
1当前所在线程:main

刷新时间: 2020-12-31T17:42:05.784
2当前所在线程:ForkJoinPool.commonPool-worker-1
11

3s后访问key=1:abcd

第3次获取key=1:WXYZ,是否刷新值=true

6s次后访问key=1:WXYZ,是否刷新值=false
刷新时间: 2020-12-31T17:42:08.799
2当前所在线程:ForkJoinPool.commonPool-worker-1
6

第4次获取key=1:wxyz,是否刷新值=true

八、总结:

1、掌握Caffeine的3种填充策略:

  • 手动填充数据
  • 同步加载
  • 异步加载

2、掌握Caffeine提供了3种回收策略:

  • 基于大小回收
  • 基于时间回收
  • 基于引用回收

3、Caffeine的移除监听器

如果我们需要在缓存被移除的时候,得到通知产生回调,并做一些额外处理工作。这个时候RemovalListener就派上用场了。

删除侦听器的里面的操作是使用Executor来异步执行的。默认执行程序是ForkJoinPool.commonPool(),可以通过Caffeine.executor(Executor)覆盖。

4、Caffeine的CacheWriter

CacheWriter允许缓存充当一个底层资源的代理,当与CacheLoader结合使用时,所有对缓存的读写操作都可以通过Writer进行传递。Writer可以把操作缓存和操作外部资源扩展成一个同步的原子性操作。并且在缓存写入完成之前,它将会阻塞后续的更新缓存操作,但是读取(get)将直接返回原有的值。 如果写入程序失败,那么原有的key和value的映射将保持不变,如果出现异常将直接抛给调用者。

能监听到的动作:

  • CacheWriter可以同步的监听到缓存的创建、变更和删除操作。

不能监听到的动作:

  • 加载(例如,LoadingCache.get)、重新加载(例如,LoadingCache.refresh)和计算(例如Map.computeIfPresent)的操作不被CacheWriter监听到。

注意事项:

  • CacheWriter不能与弱键或AsyncLoadingCache一起使用。

应用场景:

  • 可以配合外部存储使用

5、Caffeine的统计

6、Caffeine的刷新

  • 刷新并不是到期就刷新,而是对这个数据再次访问之后,才会刷新。只阻塞加载数据的线程,其余线程返回旧数据。