本地缓存框架之Caffeine

写在前面

Caffeine是一个基于Java8开发的提供了近乎最佳命中率的高性能的缓存库。

缓存和ConcurrentMap有点相似,但还是有所区别。

最根本的区别是ConcurrentMap将会持有所有加入到缓存当中的元素,直到它们被从缓存当中手动移除。

但是,Caffeine的缓存Cache 通常会被配置成自动驱逐缓存中元素,以限制其内存占用。在某些场景下,LoadingCache和AsyncLoadingCache 因为其自动加载缓存的能力将会变得非常实用。

Caffeine提供了灵活的构造器去创建一个拥有下列特性的缓存:

  • 自动加载元素到缓存当中,异步加载的方式也可供选择
  • 当达到最大容量的时候可以使用基于就近度和频率的算法进行基于容量的驱逐
  • 将根据缓存中的元素上一次访问或者被修改的时间进行基于过期时间的驱逐
  • 当向缓存中一个已经过时的元素进行访问的时候将会进行异步刷新
  • key将自动被弱引用所封装
  • value将自动被弱引用或者软引用所封装
  • 驱逐(或移除)缓存中的元素时将会进行通知
  • 写入传播到一个外部数据源当中
  • 持续计算缓存的访问统计指标

为了提高集成度,扩展模块提供了JSR-107 JCache和Guava适配器。JSR-107规范了基于Java 6的API,在牺牲了功能和性能的代价下使代码更加规范。Guava的Cache是Caffeine的原型库并且Caffeine提供了适配器以供简单的迁移策略。

术语

  • 驱逐 缓存元素因为策略被移除
  • 失效 缓存元素被手动移除
  • 移除 由于驱逐或者失效而最终导致的结果

添加策略

简介

Caffeine提供了四种缓存添加策略:手动加载,自动加载,手动异步加载和自动异步加载。

手动加载

Cache<Key, Graph> cache = Caffeine.newBuilder()
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .maximumSize(10_000)
    .build();

// 查找一个缓存元素, 没有查找到的时候返回null
Graph graph = cache.getIfPresent(key);
// 查找缓存,如果缓存不存在则生成缓存元素,  如果无法生成则返回null
graph = cache.get(key, k -> createExpensiveGraph(key));
// 添加或者更新一个缓存元素
cache.put(key, graph);
// 移除一个缓存元素
cache.invalidate(key);

Cache 接口提供了显式搜索查找、更新和移除缓存元素的能力。

手动插入

  • 通过调用 cache.put(key, value)方法被加入到缓存当中。如果缓存中指定的key已经存在对应的缓存元素的话,那么先前的缓存的元素将会被直接覆盖掉。
  • 通过 cache.get(key, k -> value) 的方式将要缓存的元素通过原子计算的方式 插入到缓存中,以避免和其他写入进行竞争。值得注意的是,当缓存的元素无法生成或者在生成的过程中抛出异常而导致生成元素失败,cache.get 也许会返回 null
  • 也可以使用Cache.asMap()所暴露出来的ConcurrentMap的方法对缓存进行操作。

手动异步加载

AsyncCache<Key, Graph> cache = Caffeine.newBuilder()
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .maximumSize(10_000)
    .buildAsync();

// 查找一个缓存元素, 没有查找到的时候返回null
CompletableFuture<Graph> graph = cache.getIfPresent(key);
// 查找缓存元素,如果不存在,则异步生成
graph = cache.get(key, k -> createExpensiveGraph(key));
// 添加或者更新一个缓存元素
cache.put(key, graph);
// 移除一个缓存元素
cache.synchronous().invalidate(key);

一个AsyncCache 是 Cache 的一个变体,AsyncCache提供了在 Executor上生成缓存元素并返回 CompletableFuture的能力。这给出了在当前流行的响应式编程模型中利用缓存的能力。

synchronous()方法给 Cache提供了阻塞直到异步缓存生成完毕的能力。

当然,也可以使用 AsyncCache.asMap()所暴露出来的ConcurrentMap的方法对缓存进行操作。

默认的线程池实现是 ForkJoinPool.commonPool() ,当然你也可以通过覆盖并实现 Caffeine.executor(Executor)方法来自定义你的线程池选择。

自动加载

LoadingCache<Key, Graph> cache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .build(key -> createExpensiveGraph(key));

// 查找缓存,如果缓存不存在则生成缓存元素,  如果无法生成则返回null
Graph graph = cache.get(key);
// 批量查找缓存,如果缓存不存在则生成缓存元素
Map<Key, Graph> graphs = cache.getAll(keys);

一个LoadingCache是一个Cache 附加上 CacheLoader能力之后的缓存实现。

通过 getAll可以达到批量查找缓存的目的。 默认情况下,在getAll 方法中,将会对每个不存在对应缓存的key调用一次 CacheLoader.load 来生成缓存元素。 在批量检索比单个查找更有效率的场景下,你可以覆盖并开发CacheLoader.loadAll 方法来使你的缓存更有效率。

值得注意的是,你可以通过实现一个 CacheLoader.loadAll并在其中为没有在参数中请求的key也生成对应的缓存元素。打个比方,如果对应某个key生成的缓存元素与包含这个key的一组集合剩余的key所对应的元素一致,那么在loadAll中也可以同时加载剩下的key对应的元素到缓存当中。

自动异步加载

AsyncLoadingCache<Key, Graph> cache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(10, TimeUnit.MINUTES)
    // 你可以选择: 去异步的封装一段同步操作来生成缓存元素
    .buildAsync(key -> createExpensiveGraph(key));
    // 你也可以选择: 构建一个异步缓存元素操作并返回一个future
    .buildAsync((key, executor) -> createExpensiveGraphAsync(key, executor));

// 查找缓存元素,如果其不存在,将会异步进行生成
CompletableFuture<Graph> graph = cache.get(key);
// 批量查找缓存元素,如果其不存在,将会异步进行生成
CompletableFuture<Map<Key, Graph>> graphs = cache.getAll(keys);

一个 AsyncLoadingCache是一个 AsyncCache 加上 AsyncCacheLoader能力的实现。

在需要同步的方式去生成缓存元素的时候,CacheLoader是合适的选择。而在异步生成缓存的场景下, AsyncCacheLoader则是更合适的选择并且它会返回一个CompletableFuture。

通过 getAll可以达到批量查找缓存的目的。 默认情况下,在getAll 方法中,将会对每个不存在对应缓存的key调用一次 AsyncCacheLoader.asyncLoad 来生成缓存元素。 在批量检索比单个查找更有效率的场景下,你可以覆盖并开发AsyncCacheLoader.asyncLoadAll 方法来使你的缓存更有效率。

值得注意的是,你可以通过实现一个 AsyncCacheLoader.asyncLoadAll并在其中为没有在参数中请求的key也生成对应的缓存元素。打个比方,如果对应某个key生成的缓存元素与包含这个key的一组集合剩余的key所对应的元素一致,那么在asyncLoadAll中也可以同时加载剩下的key对应的元素到缓存当中。

驱逐策略

简介

Caffeine 提供了三种驱逐策略,分别是基于容量,基于时间和基于引用三种类型。

基于容量

简介

如果你的缓存容量不希望超过某个特定的大小,那么记得使用Caffeine.maximumSize(long)。缓存将会尝试通过基于就近度和频率的算法来驱逐掉不会再被使用到的元素。

示例

// 基于缓存内的元素个数进行驱逐
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提供了三种方法进行基于时间的驱逐

expireAfterAccess

expireAfterAccess(long, TimeUnit):

  • 超时未访问则失效: 访问包括读和写
  • 只有在读或者写操作后才会重置过期时间。
  • 一个元素在上一次读写操作后一段时间之后,在指定的时间后没有被再次访问将会被认定为过期项。在当被缓存的元素时被绑定在一个session上时,当session因为不活跃而使元素过期的情况下,这是理想的选择。
  • 数据失效后不会主动重新加载, 必须依赖下一次访问. (言外之意: 失效和回源是两个动作)
  • key超时失效或不存在,若多个线程并发访问, 只有1个线程回源数据,其他线程阻塞等待数据返回
  • 对同一数据一直访问, 且间隔小于失效时间, 则不会去load数据, 一直读到的是脏数据,什么也不做才会失效。
private static LoadingCache<String,String> cache = Caffeine.newBuilder()
  .expireAfterAccess(1, TimeUnit.SECONDS)
  .build(key -> UUID.randomUUID().toString());

expireAfterWrite

expireAfterWrite(long, TimeUnit):

  • 只有在写操作后才会重置过期时间。
  • 一个元素将会在其创建或者最近一次被更新之后的一段时间后被认定为过期项。在对被缓存的元素的时效性存在要求的场景下,这是理想的选择。
  • 数据失效后不会主动重新加载, 必须依赖下一次访问. (言外之意: 失效和回源是两个动作)
  • key超时失效或不存在,若多个线程并发访问, 只有1个线程回源数据,其他线程阻塞等待数据返回
  • 对同一数据一直访问,expire后来访问一定能保证拿到最新的数据
private static LoadingCache<String,String> cache = Caffeine.newBuilder()
  .expireAfterWrite(1, TimeUnit.SECONDS)
  .build(key -> UUID.randomUUID().toString());

expireAfter

expireAfter(Expiry):

  • 一个元素将会在指定的时间后被认定为过期项。当被缓存的元素过期时间收到外部资源影响的时候,这是理想的选择。

在写操作,和偶尔的读操作中将会进行周期性的过期事件的执行。过期事件的调度和触发将会在O(1)的时间复杂度内完成。

为了使过期更有效率,可以通过在你的Cache构造器中通过Scheduler接口和Caffeine.scheduler(Scheduler) 方法去指定一个调度线程代替在缓存活动中去对过期事件进行调度。使用Java 9以上版本的用户可以选择Scheduler.systemScheduler()利用系统范围内的调度线程。

当测试基于时间的驱逐策略的时候,不需要坐在板凳上等待现实时钟的转动。使用Ticker接口和 Caffeine.ticker(Ticker)方法在你的Cache构造器中去指定一个时间源可以避免苦苦等待时钟转动的麻烦。Guava的测试库也提供了FakeTicker去达到同样的目的。

示例

1

// 基于固定的过期时间驱逐策略
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));

// 基于不同的过期驱逐策略
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .expireAfter(new Expiry<Key, Graph>() {
      public long expireAfterCreate(Key key, Graph graph, long currentTime) {
        // Use wall clock time, rather than nanotime, if from an external resource
        long seconds = graph.creationDate().plusHours(5)
            .minus(System.currentTimeMillis(), MILLIS)
            .toEpochSecond();
        return TimeUnit.SECONDS.toNanos(seconds);
      }
      public long expireAfterUpdate(Key key, Graph graph, 
          long currentTime, long currentDuration) {
        return currentDuration;
      }
      public long expireAfterRead(Key key, Graph graph,
          long currentTime, long currentDuration) {
        return currentDuration;
      }
    })
    .build(key -> createExpensiveGraph(key));

2

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();
    }
}

基于引用

简介

Caffeine 允许你配置你的缓存去让GC去帮助清理缓存当中的元素,其中key支持弱引用,而value则支持弱引用和软引用。记住 AsyncCache不支持软引用和弱引用。

Caffeine.weakKeys() 在保存key的时候将会进行弱引用。这允许在GC的过程中,当key没有被任何强引用指向的时候去将缓存元素回收。由于GC只依赖于引用相等性。这导致在这个情况下,缓存将会通过引用相等(==)而不是对象相等 equals()去进行key之间的比较。

Caffeine.weakValues()在保存value的时候将会使用弱引用。这允许在GC的过程中,当value没有被任何强引用指向的时候去将缓存元素回收。由于GC只依赖于引用相等性。这导致在这个情况下,缓存将会通过引用相等(==)而不是对象相等 equals()去进行value之间的比较。

Caffeine.softValues()在保存value的时候将会使用软引用。为了相应内存的需要,在GC过程中被软引用的对象将会被通过LRU算法回收。由于使用软引用可能会影响整体性能,我们还是建议通过使用基于缓存容量的驱逐策略代替软引用的使用。同样的,使用 softValues() 将会通过引用相等(==)而不是对象相等 equals()去进行value之间的比较。

示例

// 当key和缓存元素都不再存在其他强引用的时候驱逐
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .weakKeys()
    .weakValues()
    .build(key -> createExpensiveGraph(key));

// 当进行GC的时候进行驱逐
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .softValues()
    .build(key -> createExpensiveGraph(key));

移除

显式移除

在任何时候,你都可以手动去让某个缓存元素失效而不是只能等待其因为策略而被驱逐。

// 失效key
cache.invalidate(key)
// 批量失效key
cache.invalidateAll(keys)
// 失效所有的key
cache.invalidateAll()

移除监听器

Cache<Key, Graph> graphs = Caffeine.newBuilder()
    .evictionListener((Key key, Graph graph, RemovalCause cause) ->
        System.out.printf("Key %s was evicted (%s)%n", key, cause))
    .removalListener((Key key, Graph graph, RemovalCause cause) ->
        System.out.printf("Key %s was removed (%s)%n", key, cause))
    .build();

你可以为你的缓存通过Caffeine.removalListener(RemovalListener)方法定义一个移除监听器在一个元素被移除的时候进行相应的操作。这些操作是使用 Executor 异步执行的,其中默认的 Executor 实现是 ForkJoinPool.commonPool() 并且可以通过覆盖Caffeine.executor(Executor)方法自定义线程池的实现。

当移除之后的自定义操作必须要同步执行的时候,你需要使用 Caffeine.evictionListener(RemovalListener) 。这个监听器将在 RemovalCause.wasEvicted() 为 true 的时候被触发。为了移除操作能够明确生效, Cache.asMap() 提供了方法来执行原子操作。

记住任何在 RemovalListener中被抛出的异常将会被打印日志 (通过Logger)并被吞食。

刷新

简介

refreshAfterWrite

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

特征:

  • 数据失效后不会主动重新加载, 必须依赖下一次访问. (言外之意: 失效和回源是两个动作)
  • 当cache命中未命中时, 若多个线程并发访问时, 只有1个线程回源数据,其他线程阻塞等待数据返回
  • 当cache命中失效数据时, 若多个线程并发访问时, 第一个访问的线程提交一个load数据的任务到公共线程池,然后和所有其他访问线程一样直接返回旧值
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .maximumSize(10_000)
    .refreshAfterWrite(1, TimeUnit.MINUTES)
    .build(key -> createExpensiveGraph(key));

刷新和驱逐并不相同。可以通过LoadingCache.refresh(K)方法,异步为key对应的缓存元素刷新一个新的值。与驱逐不同的是,在刷新的时候如果查询缓存元素,其旧值将仍被返回,直到该元素的刷新完毕后结束后才会返回刷新后的新值。(简单说,假如设置了10分钟后刷新,10分钟后第一次get会得到旧值,同时触发刷新,第二次get会得到新的值)

与 expireAfterWrite相反,refreshAfterWrite 将会使在写操作之后的一段时间后允许key对应的缓存元素进行刷新,但是只有在这个key被真正查询到的时候才会正式进行刷新操作。所以打个比方,你可以在同一个缓存中同时用到 refreshAfterWrite和expireAfterWrite ,这样缓存元素的在被允许刷新的时候不会直接刷新使得过期时间被盲目重置。当一个元素在其被允许刷新但是没有被主动查询的时候,这个元素也会被视为过期。

一个CacheLoader可以通过覆盖重写 CacheLoader.reload(K, V) 方法使得在刷新中可以将旧值也参与到更新的过程中去,这也使得刷新操作显得更加智能。

更新操作将会异步执行在一个Executor上。默认的线程池实现是ForkJoinPool.commonPool()当然也可以通过覆盖Caffeine.executor(Executor)方法自定义线程池的实现。

在刷新的过程中,如果抛出任何异常,都会使旧值被保留,并且异常将会被打印日志 (通过 System.Logger )并被吞食。

注意事项

一般expireAfterWrite和refreshAfterWrite结合使用, expire的时间t1大于refresh的时间t2, 在t2~t1内数据更新允许脏数据, t1之后必须要重新同步加载新数据

Caffeine.newBuilder()
  .refreshAfterWrite(1, TimeUnit.MINUTES)
  .build(k -> DataObject.get("Data for " + k));

expireAfter和refreshAfter的一个区别:当请求过期条目时,执行会阻塞,直到build函数计算出新值。但是如果该条目符合刷新条件,则缓存将返回一个旧值并异步重新加载该值。

expireAfterWrite和refreshAfterWrite区别

原始代码

	public static void main(String[] args) throws InterruptedException {

		LoadingCache<String, Object> cache = Caffeine.newBuilder()
				.expireAfterWrite(2, TimeUnit.SECONDS)
				// .refreshAfterWrite(2, TimeUnit.SECONDS)
				.build(new CacheLoader<String, Object>() {
					@Override
					public Object load(String key) throws Exception {
						return key + "_load";
					}
					@Override
					public Object reload(String key, Object oldValue) throws Exception {
						return key + oldValue + "_reload";
					}
				});
		cache.put("hi", "tom");
		while (true) {
			Thread.sleep(1500);
			log.info("{}", cache.get("hi"));
		}
	}

只用expire

expireAfterWrite(2, TimeUnit.SECONDS);

while (true) {
  log.info("{}", cache.get("hi"));
  Thread.sleep(1500);
}

结果

15:09:51.070 [main] INFO com.admin.web.controller.v1.EnvController - tom
15:09:52.578 [main] INFO com.admin.web.controller.v1.EnvController - tom
15:09:54.092 [main] INFO com.admin.web.controller.v1.EnvController - hi_load
15:09:55.597 [main] INFO com.admin.web.controller.v1.EnvController - hi_load
15:09:57.103 [main] INFO com.admin.web.controller.v1.EnvController - hi_load

只用refresh

refreshAfterWrite(2, TimeUnit.SECONDS);

while (true) {
  log.info("{}", cache.get("hi"));
  Thread.sleep(1500);
}

结果

15:10:47.896 [main] INFO com.admin.web.controller.v1.EnvController - tom
15:10:49.403 [main] INFO com.admin.web.controller.v1.EnvController - tom
15:10:50.923 [main] INFO com.admin.web.controller.v1.EnvController - tom
15:10:52.428 [main] INFO com.admin.web.controller.v1.EnvController - hitom_reload
15:10:53.931 [main] INFO com.admin.web.controller.v1.EnvController - hitom_reload
15:10:55.436 [main] INFO com.admin.web.controller.v1.EnvController - hihitom_reload_reload
15:10:56.942 [main] INFO com.admin.web.controller.v1.EnvController - hihitom_reload_reload

两个一起用1

expireAfterWrite(4, TimeUnit.SECONDS);
refreshAfterWrite(2, TimeUnit.SECONDS);
while (true) {
  log.info("{}", cache.get("hi"));
  Thread.sleep(1500);
}

结果

15:13:39.222 [main] INFO com.admin.web.controller.v1.EnvController - tom
15:13:40.728 [main] INFO com.admin.web.controller.v1.EnvController - tom
15:13:42.244 [main] INFO com.admin.web.controller.v1.EnvController - tom
15:13:43.746 [main] INFO com.admin.web.controller.v1.EnvController - hitom_reload
15:13:45.252 [main] INFO com.admin.web.controller.v1.EnvController - hitom_reload
15:13:46.756 [main] INFO com.admin.web.controller.v1.EnvController - hihitom_reload_reload
15:13:48.262 [main] INFO com.admin.web.controller.v1.EnvController - hihitom_reload_reload

两个一起用2

expireAfterWrite(4, TimeUnit.SECONDS);
refreshAfterWrite(2, TimeUnit.SECONDS);
while (true) {
  log.info("{}", cache.get("hi"));
  Thread.sleep(2500);
}

结果

15:14:38.377 [main] INFO com.admin.web.controller.v1.EnvController - tom
15:14:40.893 [main] INFO com.admin.web.controller.v1.EnvController - tom
15:14:43.399 [main] INFO com.admin.web.controller.v1.EnvController - hitom_reload
15:14:45.905 [main] INFO com.admin.web.controller.v1.EnvController - hihitom_reload_reload
15:14:48.408 [main] INFO com.admin.web.controller.v1.EnvController - hihihitom_reload_reload_reload

两个一起用2

expireAfterWrite(4, TimeUnit.SECONDS);
refreshAfterWrite(2, TimeUnit.SECONDS);
while (true) {
  log.info("{}", cache.get("hi"));
  Thread.sleep(4500);
}

结果

15:17:42.205 [main] INFO com.admin.web.controller.v1.EnvController - tom
15:17:46.721 [main] INFO com.admin.web.controller.v1.EnvController - hi_load
15:17:51.226 [main] INFO com.admin.web.controller.v1.EnvController - hi_load
15:17:55.732 [main] INFO com.admin.web.controller.v1.EnvController - hi_load

总结

0----t2(refresh)----t1(expire)

expire的时间t1大于refresh的时间t2, 在t2~t1内数据更新允许脏数据, t1之后必须要重新同步加载新数据

清除

在默认情况下,当一个缓存元素过期的时候,Caffeine不会自动立即将其清理和驱逐。而它将会在写操作之后进行少量的维护工作,在写操作较少的情况下,也偶尔会在读操作之后进行。如果你的缓存吞吐量较高,那么你不用去担心你的缓存的过期维护问题。但是如果你的缓存读写操作都很少,可以像下文所描述的方式额外通过一个线程去通过Cache.cleanUp() 方法在合适的时候触发清理操作。

LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .scheduler(Scheduler.systemScheduler())
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .build(key -> createExpensiveGraph(key));

Scheduler可以提前触发过期元素清理移除。在过期事件之间进行调度,以期在短时间内最小化连续的批处理操作的数量。这里的调度是尽可能做到合理,并不能保证在一个元素过期的时候就将其清除。Java 9以上的用户可以通过Scheduler.systemScheduler()来利用专用的系统范围内的调度线程。

Cache<Key, Graph> graphs = Caffeine.newBuilder().weakValues().build();
Cleaner cleaner = Cleaner.create();

cleaner.register(graph, graphs::cleanUp);
graphs.put(key, graph);

Java 9以上的用户也可以通过Cleaner去触发移除关于基于引用的元素(在使用了 weakKeys, weakValues, 或者 softValues的情况下)。只要将key或者缓存的元素value注册到Cleaner上,就可以在程序运行中调用Cache.cleanUp()方法触发缓存的维护工作。

测试

简介

当测试基于时间的驱逐策略的时候,不需要等着现实时间的推进。可以使用Ticker接口和Caffeine.ticker(Ticker)方法在你的缓存构造器中去定义一个自定义的时间源而不是等待系统时钟的转动。 为了这个目的,Guava的测试库提供了FakeTicker 。过期的元素将在周期性的维护中被清除,所以在测试基于驱逐策略的时候也可以直接使用 Cache.cleanUp()方法来立即触发一次维护操作。

Caffeine 将通过Executor来执行周期维护,移除通知和异步生成。这将对于调用者来说使得响应耗时变得更加可靠,并且线程池的默认实现是 ForkJoinPool.commonPool()。可以通过在缓存构造器中使用Caffeine.executor(Executor) 方法来指定一个直接(同线程)的executor 而不是等待异步任务执行完毕。

我们推荐使用Awaitility来进行多线程下的测试。

示例

FakeTicker ticker = new FakeTicker(); // Guava的测试库
Cache<Key, Graph> cache = Caffeine.newBuilder()
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .executor(Runnable::run)
    .ticker(ticker::read)
    .maximumSize(10)
    .build();

cache.put(key, graph);
ticker.advance(30, TimeUnit.MINUTES)
assertThat(cache.getIfPresent(key), is(nullValue()));

The end.

### Java本地缓存框架概述 对于Java开发者而言,在处理应用性能优化时,合理利用本地缓存技术能够显著提升系统的响应速度和吞吐量。当前主流的几种Java本地缓存框架各有特色,适用于不同场景下的需求。 #### 1. 使用`ConcurrentHashMap`构建简易缓存机制 通过引入`java.util.concurrent.ConcurrentHashMap`类,可以快速搭建起一个具备基本并发访问能力的内存级缓存结构[^2]。此方法适合于那些对缓存管理要求不高、仅需简单的键值对存储的应用场合。然而,这种方式缺乏高级特性如自动清理过期条目等功能。 ```java import java.util.concurrent.ConcurrentHashMap; import java.util.Map; public class SimpleCache { private final Map<String, String> cache = new ConcurrentHashMap<>(); public String get(String key) { return cache.get(key); } public void put(String key, String value) { cache.put(key, value); } } ``` #### 2. Guava Cache 提供丰富的API接口 Google开源项目Guava所提供的缓存组件不仅继承了`ConcurrentHashMap`的优点,还增加了更多实用的功能,比如设置最大容量、定义加载器以及指定移除监听器等[^3]。这使得它成为许多中小型项目的首选方案之一。 ```java import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; public class GuavaCacheExample { LoadingCache<String, String> cache = CacheBuilder.newBuilder() .maximumSize(100) .expireAfterWrite(10, TimeUnit.MINUTES) .build(new CacheLoader<String, String>() { @Override public String load(String key) throws Exception { // Simulate data fetching from a remote source. return "value_for_" + key; } }); public String getValue(String key) throws ExecutionException { return cache.get(key); } } ``` #### 3. Caffeine - 高效且易用的选择 作为新一代的高性能缓存库,Caffeine凭借其接近理论极限的时间复杂度表现赢得了广泛好评[^4]。相较于其他同类产品,Caffeine拥有更好的默认参数配置,并且提供了更为直观简洁的操作界面,非常适合追求极致效率的同时又不想牺牲太多开发便利性的团队使用。 ```java import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.Cache; import java.util.concurrent.TimeUnit; public class CaffeineCacheDemo { Cache<String, String> caffeineCache = Caffeine.newBuilder() .expireAfterAccess(5, TimeUnit.MINUTES) .maximumSize(10_000) .build(); public void setKeyValue(String key, String value){ caffeineCache.put(key,value); } public String getKey(String key){ return caffeineCache.getIfPresent(key); } } ``` 综上所述,当面临具体应用场景时,可以根据实际业务逻辑特点来挑选最适合自己的本地缓存工具;如果只是单纯为了提高读取速率而不需要复杂的控制,则可以选择基于`ConcurrentHashMap`的手动实现;而对于需要更加精细管理和维护的情况来说,Guava Cache 或者 Caffeine 将会是比较理想的选择。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

伊成

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值