guava cache详细介绍

11 篇文章 2 订阅

官方文档:https://github.com/google/guava/wiki/CachesExplained

guava cache是google开源的一款本地缓存工具库,它的设计灵感来源于ConcurrentHashMap,使用多个segments方式的细粒度锁,在保证线程安全的同时,支持高并发场景需求,同时支持多种类型的缓存清理策略,包括基于容量的清理、基于时间的清理、基于引用的清理等。

<dependency>
		<groupId>com.google.guava</groupId>
		<artifactId>guava</artifactId>
		<version>19.0</version>
</dependency>

一、创建缓存的两种方式

根据官网上介绍,使用guava cache先问自己一个问题:是否存在一个默认函数来加载或计算与键关联的值?如果是这样,则应使用CacheLoader。如果不是这样,但仍希望使用原子的“ get-if-absent-compute”语义,则应将Callable传递给get调用。虽然可以使用Cache.put直接插入元素,但是首选自动缓存加载,因为这样可以更轻松地推断所有缓存内容的一致性(毕竟是原子的语义操作)。

1、CacheLoader的方式:

LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
       .maximumSize(1000)
       .build(
           new CacheLoader<Key, Graph>() {
             public Graph load(Key key) throws AnyException {
               return createExpensiveGraph(key);
             }
           });
...
try {
  return graphs.get(key);
} catch (ExecutionException e) {
  throw new OtherException(e.getCause());
}

...
//或者使用下面方法,不抛出异常
return graphs.getUnchecked(key);

在每次从cache中get(K)时,如果不存在会自动调用load方法原子的将值计算出来并加到缓存中。(调用load方法是同步的

1)get(K)和getUnchecked(K)方法:

由于CacheLoader可能会引发异常,因此LoadingCache.get(K)会引发ExecutionException。还可以选择使用getUnchecked(K)方法获取值,不抛出异常。

2)批量get和批量load:

getAll(Iterable<? extendsK>)方法用来执行批量查询。默认情况下,对每个不在缓存中的键,getAll方法会单独调用CacheLoader.load来加载缓存项。如果批量的加载比多个单独加载更高效,可以重载CacheLoader.loadAll来利用这一点提示getAll(Iterable)的性能。看一个例子:

public LoadingCache<String, String> caches = CacheBuilder
	.newBuilder().maximumSize(100)
	.expireAfterWrite(100, TimeUnit.SECONDS) // 根据写入时间过期
	.build(new CacheLoader<String, String>() {
		@Override
		public String load(final String key) {
			return getSchema(key);
		}
				
		@Override
		public Map<String,String> loadAll(final Iterable<? extends String> keys) throws Exception {
            //com.google.common.collect.Lists
			ArrayList<String> keysList = Lists.newArrayList(keys);
			return getSchemas(keysList);
		}
});

private static Map<String,String> getSchemas(List<String> keys) {
		Map<String,String> map = new HashMap<>();
		
		//...
		System.out.println("loadall...");
		return map;
}

List<String> keys = new ArrayList<>();
keys.add("key2");
keys.add("key3");

try {
	caches.getAll(keys);
} catch (ExecutionException e1) {
	e1.printStackTrace();
}

注意:expireAfterWrite时guava重新加载数据时使用的是load方法,不会调用loadAll。

见:guava缓存批量获取的一个坑_赶路人儿的博客-CSDN博客_guva缓存的坑

2、callable方式:

LoadingCache<String, String> cache = CacheBuilder
	.newBuilder().maximumSize(100)
	.expireAfterWrite(100, TimeUnit.SECONDS) // 根据写入时间过期
	.build(new CacheLoader<String, String>() {
		@Override
		public String load(String key) {
			return getSchema(key);
		}
});

private static String getSchema(String key) {
    System.out.println("load...");
    return key+"schema";
}

try {
	String value = cache.get("key4", new Callable<String>() {
			@Override
			public String call() throws Exception {
				System.out.println("i am callable...");
				return "i am callable...";
			}
	  });
	System.out.println(value);
} catch (ExecutionException e1) {
	e1.printStackTrace();
}
//输出值:i am callable... 

或者:

Cache<String, String> cache2 = CacheBuilder.newBuilder()
		.maximumSize(1000)
		.expireAfterWrite(100, TimeUnit.SECONDS) // 根据写入时间过期
		.build(); // look Ma, no CacheLoader

try {
	String value = cache2.get("key4", new Callable<String>() {
			@Override
			public String call() throws Exception {
				System.out.println("i am callable...");
				return "i am callable...";
			}
	  });
	System.out.println(value);
} catch (ExecutionException e1) {
	e1.printStackTrace();
}

 callable同样实现了原子的“ get-if-absent-compute”语义。上面两个例子说明:无论是LoadingCache还是Cache都可以使用callable的方式,需要说明的是:

1)Cache类型的缓存只能使用Callable的方式get(K,Callable)方法;

2)LoadingCache类型的缓存,可以使用get(K)或get(K,Callable)方法,并且如果使用的是get(K,Callable)方法,当K值不存在时,使用的是Callable计算值,不走load方法计算,然后将值放入缓存。

3、显示插入:

除了上面两种方式创建缓存外,还可以显示的使用put(K,V)方法,将值放入缓存中。但是这种方法没有“ get-if-absent-compute”语义。

二、回收(逐出)策略

由于guava是本地缓存,所以需要一个回收策略。guava提供了三种回收策略。

1、基于size回收:

通过CacheBuilder.maximumSize(long)设置缓存项的最大数目,当达到最大数目后,继续添加缓存项,Guava 默认会根据LRU策略回收缓存项来保证不超过最大数目;​ 另外,可以通过CacheBuilder.weigher(Weigher)设置不同缓存项的权重,Guava Cache根据权重来回收缓存项。

LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()  
	.maximumWeight(100000)
	.weigher(new Weigher<Key, Graph>() {
		public int weigh(Key k, Graph g) {
			return g.vertices().size();
		}
	})
	.build(new CacheLoader<Key, Graph>() {
			public Graph load(Key key) { // no checked exception
				return createExpensiveGraph(key);
			}
	});

2、定时回收:

​guava Cache提供两种定时回收的方法:

  • expireAfterAccess(long, TimeUnit):缓存项(Key)在给定时间范围内没有读/写访问,那么下次访问时,会回收该Key,然后同步load(),这种方式类似于基于size的LRU回收。()
  • expireAfterWrite(long, TimeUnit):缓存项(Key)在给定时间范围内没有写访问,那么下次访问时,会回收该Key,然后同步load()

注:Guava Cache不会专门维护一个线程来回收这些过期的缓存项只有在读/写访问,才去判断该缓存项是否过期,如果过期,则会回收。而且注意,回收后会同步调用load方法来加载新值到cache中

3、基于引用回收:

通过使用弱引用的键、或弱引用的值、或软引用的值,Guava Cache可以把缓存设置为允许垃圾回收:

  • CacheBuilder.weakKeys():使用弱引用存储键。当键没有其它(强或软)引用时,缓存项可以被垃圾回收。
  • CacheBuilder.weakValues():使用弱引用存储值。当值没有其它(强或软)引用时,缓存项可以被垃圾回收
  • CacheBuilder.softValues():使用软引用存储值。软引用只有在响应内存需要时,才按照全局最近最少使用的顺序回收。考虑到使用软引用的性能影响,我们通常建议使用更有性能预测性的缓存大小限定(见上文,基于容量回收)

以上是guava自动维护的,当然我们也可以手动将缓存值清理出cache,见下面。

三、删除缓存和删除监听器

1、删除缓存Key方法:

在任何时候,可以通过一下方法将Key从缓存中移除,而不用等guava的回收。

  • 清除单个Key:Cache.invalidate(key)
  • 批量清除:Cache.invalidateAll(keys)
  • 清除所有缓存项:Cache.invalidateAll()

2、删除监听器:

通过CacheBuilder.removalListener(RemovalListener),你可以声明一个监听器,以便缓存项被移除时做一些额外操作。缓存项被移除时,RemovalListener会获取移除通知RemovalNotification,其中包含移除原因RemovalCause、键和值

RemovalListener<String, String> removalListener = new RemovalListener<String,String>() {  
	@Override
	public void onRemoval(RemovalNotification<String, String> notification) {
		String key = notification.getKey();
		//例如是数据库连接,这里可以close该连接
		String value = notification.getValue();
	}
};

CacheBuilder.newBuilder()  
    .expireAfterWrite(2, TimeUnit.MINUTES)
    .removalListener(removalListener)
    .build(loader);

四、刷新策略

1、什么时候进行清理?

使用CacheBuilder构建的缓存不会“自动”执行清理和逐出值,也不会在值过期后立即执行清理或逐出值,或类似的任何操作。取而代之的是,如果写操作很少,它会在写操作期间或偶尔的读操作期间执行少量维护。

原因如下:如果我们要连续执行Cache维护,则需要创建一个线程,并且该线程的操作将与用户操作争夺共享锁。此外,某些环境限制了线程的创建,这会使CacheBuilder在该环境中无法使用。

相反,我们会将选择权交给您。如果您的缓存是高吞吐量的,那么您不必担心执行缓存维护以清理过期的条目等。如果您的缓存确实很少写入,并且您不想清理来阻止缓存读取,则您可能希望创建自己的维护线程,该线程定期调用Cache.cleanUp()。

最后一句话怎么理解:如果流量很大,每时每刻都在访问cache,那么guava会自动根据回收策略进行清理数据。如果量很小,由于guava的惰性原理,不会及时回收,用户可以自己定时清理。

2、刷新缓存策略:

1)刷新和回收区别:

刷新策略和上面说的回收策略不太一样:刷新表示为key加载新值,这个过程可以是异步的(需要重写CacheLoader的reload方法,否则仍然是同步的调用load),而回收Key的时候,会调用load方法加载值,这个过程是同步的。

二者的相同点是:都是在访问缓存项(Key)的时候才会触发

2)刷新方法:

可以调用LoadingCache.refresh(K) 来刷新某个Key。(:只有LoadingCache类才有refresh方法)

public static ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 50, 300, TimeUnit.SECONDS, 
	new ArrayBlockingQueue<Runnable>(50),  
	new ThreadFactory(){ public Thread newThread(Runnable r) {
		return new Thread(r, "pool_" + r.hashCode());
	}}, new ThreadPoolExecutor.DiscardOldestPolicy());


public static LoadingCache<String, String> cache = CacheBuilder
	.newBuilder().maximumSize(100)
	.expireAfterWrite(100, TimeUnit.SECONDS) // 根据写入时间过期
	.build(new CacheLoader<String, String>() {
		@Override
		public String load(String key) {
			return getSchema(key);
		}
		
		public ListenableFuture<String> reload(String key, String oldValue) throws Exception {
			ListenableFutureTask<String> task =  ListenableFutureTask.create(new Callable<String>() {
				@Override
				public String call() throws Exception {
					Thread.sleep(1000);
					System.out.println("async....");
					return getSchema(key);
				}
			});
			threadPool.submit(task);
			return task;
		}
	});

//调用
cache.refresh(key1);//void
System.out.println("after refresh");

输出:

after refresh

async...

证明refresh方法是一个异步的。如果我们没有重写reload方法,那么看reload源码,就是默认的同步调用load,如:

3、刷新+回收策略:

我们对比一下几种策略:

1)定时过期回收:

前面我们知道可以配置expireAfterWrite或expireAfterAccess来设置定期回收,那我们现在来看下这种策略在高并发情况下是否存在“缓存击穿”问题?当高并发条件下同时进行get操作,而此时缓存值已过期时,会导致大量线程都调用生成缓存值的方法,比如从数据库读取。这时候就容易造成大量请求同时查询数据库中该条记录,也就是“缓存击穿”。

Guava cache则对此种情况有一定控制。当大量线程用相同的key获取缓存值时,只会有一个线程进入load方法,而其他线程则等待,直到缓存值被生成。这样也就避免了缓存击穿的危险。

2)定时刷新:

guava虽然不会有缓存击穿的情况,但是每当某个缓存值过期时,老是会导致大量的请求线程被阻塞。而Guava则提供了另一种缓存策略,缓存值定时刷新:更新线程调用load方法更新该缓存,其他请求线程返回该缓存的旧值。这样对于某个key的缓存来说,只会有一个线程被阻塞,用来生成缓存值,而其他的线程都返回旧的缓存值,不会被阻塞

这里就需要用到Guava cache的refreshAfterWrite方法。例如:

LoadingCache<String, Object> caches = CacheBuilder.newBuilder()
                .maximumSize(100)
                .refreshAfterWrite(10, TimeUnit.MINUTES)
                .build(new CacheLoader<String, Object>() {
                    @Override
                    public Object load(String key) throws Exception {
                        return generateValueByKey(key);
                    }
                });
try {
    System.out.println(caches.get("key-zorro"));
} catch (ExecutionException e) {
    e.printStackTrace();
}

 注:前面两种策略中的定时,不是真正意义上的定时。Guava cache的刷新和回收都是需要依靠用户请求触发的。

3)异步刷新策略:

上面解决了同一个key的缓存过期时会让多个线程阻塞的问题,只会让用来执行刷新缓存操作的一个用户线程会被阻塞。由此可以想到另一个问题,当缓存的key很多时,高并发条件下大量线程同时获取不同key对应的缓存,此时依然会造成大量线程阻塞,并且给数据库带来很大压力。这个问题的解决办法就是将刷新缓存值的任务交给后台线程,所有的用户请求线程均返回旧的缓存值,这样就不会有用户线程被阻塞了。

ListeningExecutorService backgroundRefreshPools = 
				MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(20));
        LoadingCache<String, Object> caches = CacheBuilder.newBuilder()
                .maximumSize(100)
                .refreshAfterWrite(10, TimeUnit.MINUTES)
                .build(new CacheLoader<String, Object>() {
                    @Override
                    public Object load(String key) throws Exception {
                        return generateValueByKey(key);
                    }
                    
                    @Override
                    public ListenableFuture<Object> reload(String key,
                    		Object oldValue) throws Exception {
                    	return backgroundRefreshPools.submit(new Callable<Object>() {

							@Override
							public Object call() throws Exception {
								return generateValueByKey(key);
							}
						});
                    }
                });
try {
    System.out.println(caches.get("key-zorro"));
} catch (ExecutionException e) {
    e.printStackTrace();
}

重写了CacheLoader的reload方法,在该方法中建立缓存刷新的任务并提交到线程池。

注意:因为刷新动作和回收一样,都是在检索的时候才会触发,所以当你的缓存配置了CacheBuilder.refreshAfterWrite(long, TimeUnit)时,如果部分缓存项很久没有被访问,那么再次被访问时,可能会获得过期很久的数据,这显然是不行的。而单独配置expireAfterWrite(long, TimeUnit)也是有问题的,如果热点数据突然过期,因为同步load()必然会影响读效率。

​所以,通常我们都是CacheBuilder.refreshAfterWrite(long, TimeUnit)和expireAfterWrite(long, TimeUnit) 同时配置,并且刷新的时间间隔要比过期的时间间隔短!这样当较长时间没有被访问的缓存项突然被访问时,会触发过期回收而不是刷新,后面会分析这一块的源码,而热点数据只会触发刷新操作不会触发回收操作。
 

参考:

https://juejin.im/post/6844904142645755918

Guava Cache使用介绍 - 掘金

https://blog.csdn.net/u012859681/article/details/75220605

  • 11
    点赞
  • 50
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Guava Cache是一个全内存的本地缓存实现,是Guava工具包中的一个模块。它提供了简单易用且性能好的线程安全的本地缓存机制。Guava Cache适用于对性能有高要求、数据不经常变化、占用内存不大、需要访问整个集合、数据允许不时一致的场景。Guava Cache的优势包括缓存过期和淘汰机制、并发处理能力、更新锁定、集成数据源以及监控缓存加载/命中情况等。在创建Guava Cache对象时,可以使用CacheLoader来自动加载数据到缓存中,也可以使用Callable Callback来在缓存中获取数据并回填缓存。要删除缓存数据,可以使用Cache的invalidate方法来实现。123 #### 引用[.reference_title] - *1* [Guava Cache本地缓存介绍及使用](https://blog.csdn.net/unbelievevc/article/details/128365002)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}} ] [.reference_item] - *2* *3* [Guava Cache简介、应用场景分析、代码实现以及核心的原理](https://blog.csdn.net/weixin_44795847/article/details/123702038)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

赶路人儿

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

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

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

打赏作者

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

抵扣说明:

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

余额充值