Guava Cache与ConcurrentMap很相似,但也不完全一样。最基本的区别是ConcurrentMap会一直保存所有添加的元素,直到显式地移除。相对地,Guava Cache为了限制内存占用,通常都设定为自动回收元素。在某些场景下,尽管LoadingCache 不回收元素,它也是很有用的,因为它会自动加载缓存。
通常来说,Guava Cache适用于:
- 你愿意消耗一些内存空间来提升速度。
- 你预料到某些键会被查询一次以上。
- 缓存中存放的数据总量不会超出内存容量。
Guava Cache是单个应用运行时的本地缓存。它不把数据存放到文件或外部服务器。
缓存回收
一个残酷的现实是,我们几乎一定没有足够的内存缓存所有数据。你你必须决定:什么时候某个缓存项就不值得保留了?Guava Cache提供了三种基本的缓存回收方式:基于容量回收、定时回收和基于引用回收。
基于容量的回收(size-based eviction)
如果要规定缓存项的数目不超过固定值,只需使用CacheBuilder.maximumSize(long)。缓存将尝试回收最近没有使用或总体上很少使用的缓存项。——警告:在缓存项的数目达到限定值之前,缓存就可能进行回收操作——通常来说,这种情况发生在缓存项的数目逼近限定值时。
另外,不同的缓存项有不同的“权重”(weights)——例如,如果你的缓存值,占据完全不同的内存空间,你可以使用CacheBuilder.weigher(Weigher)指定一个权重函数,并且用CacheBuilder.maximumWeight(long)指定最大总重。在权重限定场景中,除了要注意回收也是在重量逼近限定值时就进行了,还要知道重量是在缓存创建时计算的,因此要考虑重量计算的复杂度。
01 |
LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder() |
02 |
.maximumWeight( 100000 )
|
03 |
.weigher( new Weigher<Key, Graph>() {
|
04 |
public int weigh(Key k, Graph g) {
|
05 |
return g.vertices().size();
|
06 |
}
|
07 |
})
|
08 |
.build(
|
09 |
new CacheLoader<Key, Graph>() {
|
10 |
public Graph load(Key key) { // no checked exception
|
11 |
return createExpensiveGraph(key);
|
12 |
}
|
13 |
});
|
定时回收(Timed Eviction)
CacheBuilder提供两种定时回收的方法:
- expireAfterAccess(long, TimeUnit):缓存项在给定时间内没有被读/写访问,则回收。请注意这种缓存的回收顺序和基于大小回收一样。
- expireAfterWrite(long, TimeUnit):缓存项在给定时间内没有被写访问(创建或覆盖),则回收。如果认为缓存数据总是在固定时候后变得陈旧不可用,这种回收方式是可取的。
如下文所讨论,定时回收周期性地在写操作中执行,偶尔在读操作中执行。
测试定时回收
对定时回收进行测试时,不一定非得花费两秒钟去测试两秒的过期。你可以使用Ticker接口和CacheBuilder.ticker(Ticker)方法在缓存中自定义一个时间源,而不是非得用系统时钟。
基于引用的回收(Reference-based Eviction)
通过使用弱引用的键、或弱引用的值、或软引用的值,Guava Cache可以把缓存设置为允许垃圾回收:
- CacheBuilder.weakKeys():使用弱引用存储键。当键没有其它(强或软)引用时,缓存项可以被垃圾回收。因为垃圾回收仅依赖恒等式(==),使用弱引用键的缓存用==而不是equals比较键。
- CacheBuilder.weakValues():使用弱引用存储值。当值没有其它(强或软)引用时,缓存项可以被垃圾回收。因为垃圾回收仅依赖恒等式(==),使用弱引用值的缓存用==而不是equals比较值。
- CacheBuilder.softValues():使用软引用存储值。软引用只有在响应内存需要时,才按照全局最近最少使用的顺序回收。考虑到使用软引用的性能影响,我们通常建议使用更有性能预测性的缓存大小限定(见上文,基于容量回收)。使用软引用值的缓存同样用==而不是equals比较值。
显式清除
任何时候,你都可以显式地清除缓存项,而不是等到它被回收:
- 个别清除:Cache.invalidate(key)
- 批量清除:Cache.invalidateAll(keys)
- 清除所有缓存项:Cache.invalidateAll()
移除监听器
通过CacheBuilder.removalListener(RemovalListener),你可以声明一个监听器,以便缓存项被移除时做一些额外操作。缓存项被移除时,RemovalListener会获取移除通知[RemovalNotification],其中包含移除原因[RemovalCause]、键和值。
请注意,RemovalListener抛出的任何异常都会在记录到日志后被丢弃[swallowed]。
01 |
CacheLoader<Key, DatabaseConnection> loader = new CacheLoader<Key, DatabaseConnection> () {
|
02 |
public DatabaseConnection load(Key key) throws Exception {
|
03 |
return openConnection(key);
|
04 |
}
|
05 |
}; |
06 |
07 |
RemovalListener<Key, DatabaseConnection> removalListener = new RemovalListener<Key, DatabaseConnection>() {
|
08 |
public void onRemoval(RemovalNotification<Key, DatabaseConnection> removal) {
|
09 |
DatabaseConnection conn = removal.getValue();
|
10 |
conn.close(); // tear down properly
|
11 |
}
|
12 |
}; |
13 |
14 |
return CacheBuilder.newBuilder()
|
15 |
.expireAfterWrite( 2 , TimeUnit.MINUTES)
|
16 |
.removalListener(removalListener)
|
17 |
.build(loader);
|
警告:默认情况下,监听器方法是在移除缓存时同步调用的。因为缓存的维护和请求响应通常是同时进行的,代价高昂的监听器方法在同步模式下会拖慢正常的缓存请求。在这种情况下,你可以使用RemovalListeners.asynchronous(RemovalListener, Executor)把监听器装饰为异步操作。
清理什么时候发生?
使用CacheBuilder构建的缓存不会"自动"执行清理和回收工作,也不会在某个缓存项过期后马上清理,也没有诸如此类的清理机制。相反,它会在写操作时顺带做少量的维护工作,或者偶尔在读操作时做——如果写操作实在太少的话。
这样做的原因在于:如果要自动地持续清理缓存,就必须有一个线程,这个线程会和用户操作竞争共享锁。此外,某些环境下线程创建可能受限制,这样CacheBuilder就不可用了。
相反,我们把选择权交到你手里。如果你的缓存是高吞吐的,那就无需担心缓存的维护和清理等工作。如果你的 缓存只会偶尔有写操作,而你又不想清理工作阻碍了读操作,那么可以创建自己的维护线程,以固定的时间间隔调用Cache.cleanUp()。ScheduledExecutorService可以帮助你很好地实现这样的定时调度。
刷新
刷新和回收不太一样。正如LoadingCache.refresh(K)所声明,刷新表示为键加载新值,这个过程可以是异步的。在刷新操作进行时,缓存仍然可以向其他线程返回旧值,而不像回收操作,读缓存的线程必须等待新值加载完成。
如果刷新过程抛出异常,缓存将保留旧值,而异常会在记录到日志后被丢弃[swallowed]。
重载CacheLoader.reload(K, V)可以扩展刷新时的行为,这个方法允许开发者在计算新值时使用旧的值。
01 |
//有些键不需要刷新,并且我们希望刷新是异步完成的 |
02 |
LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder() |
03 |
.maximumSize( 1000 )
|
04 |
.refreshAfterWrite( 1 , TimeUnit.MINUTES)
|
05 |
.build(
|
06 |
new CacheLoader<Key, Graph>() {
|
07 |
public Graph load(Key key) { // no checked exception
|
08 |
return getGraphFromDatabase(key);
|
09 |
}
|
10 |
11 |
public ListenableFuture<Key, Graph> reload( final Key key, Graph prevGraph) {
|
12 |
if (neverNeedsRefresh(key)) {
|
13 |
return Futures.immediateFuture(prevGraph);
|
14 |
} else {
|
15 |
// asynchronous!
|
16 |
ListenableFutureTask<Key, Graph> task=ListenableFutureTask.create( new Callable<Key, Graph>() {
|
17 |
public Graph call() {
|
18 |
return getGraphFromDatabase(key);
|
19 |
}
|
20 |
});
|
21 |
executor.execute(task);
|
22 |
return task;
|
23 |
}
|
24 |
}
|
25 |
});
|
CacheBuilder.refreshAfterWrite(long, TimeUnit)可以为缓存增加自动定时刷新功能。和expireAfterWrite相反,refreshAfterWrite通过定时刷新可以让缓存项保持可用,但请注意:缓存项只有在被检索时才会真正刷新(如果CacheLoader.refresh实现为异步,那么检索不会被刷新拖慢)。因此,如果你在缓存上同时声明expireAfterWrite和refreshAfterWrite,缓存并不会因为刷新盲目地定时重置,如果缓存项没有被检索,那刷新就不会真的发生,缓存项在过期时间后也变得可以回收。
其他特性
统计
CacheBuilder.recordStats()用来开启Guava Cache的统计功能。统计打开后,Cache.stats()方法会返回CacheStats对象以提供如下统计信息:
- hitRate():缓存命中率;
- averageLoadPenalty():加载新值的平均时间,单位为纳秒;
- evictionCount():缓存项被回收的总数,不包括显式清除。
此外,还有其他很多统计信息。这些统计信息对于调整缓存设置是至关重要的,在性能要求高的应用中我们建议密切关注这些数据。
asMap视图
asMap视图提供了缓存的ConcurrentMap形式,但asMap视图与缓存的交互需要注意:
- cache.asMap()包含当前所有加载到缓存的项。因此相应地,cache.asMap().keySet()包含当前所有已加载键;
- asMap().get(key)实质上等同于cache.getIfPresent(key),而且不会引起缓存项的加载。这和Map的语义约定一致。
- 所有读写操作都会重置相关缓存项的访问时间,包括Cache.asMap().get(Object)方法和Cache.asMap().put(K, V)方法,但不包括Cache.asMap().containsKey(Object)方法,也不包括在Cache.asMap()的集合视图上的操作。比如,遍历Cache.asMap().entrySet()不会重置缓存项的读取时间。
中断
缓存加载方法(如Cache.get)不会抛出InterruptedException。我们也可以让这些方法支持InterruptedException,但这种支持注定是不完备的,并且会增加所有使用者的成本,而只有少数使用者实际获益。详情请继续阅读。
Cache.get请求到未缓存的值时会遇到两种情况:当前线程加载值;或等待另一个正在加载值的线程。这两种情况下的中断是不一样的。等待另一个正在加载值的线程属于较简单的情况:使用可中断的等待就实现了中断支持;但当前线程加载值的情况就比较复杂了:因为加载值的CacheLoader是由用户提供的,如果它是可中断的,那我们也可以实现支持中断,否则我们也无能为力。
如果用户提供的CacheLoader是可中断的,为什么不让Cache.get也支持中断?从某种意义上说,其实是支持的:如果CacheLoader抛出InterruptedException,Cache.get将立刻返回(就和其他异常情况一样);此外,在加载缓存值的线程中,Cache.get捕捉到InterruptedException后将恢复中断,而其他线程中InterruptedException则被包装成了ExecutionException。
原则上,我们可以拆除包装,把ExecutionException变为InterruptedException,但这会让所有的LoadingCache使用者都要处理中断异常,即使他们提供的CacheLoader不是可中断的。如果你考虑到所有非加载线程的等待仍可以被中断,这种做法也许是值得的。但许多缓存只在单线程中使用,它们的用户仍然必须捕捉不可能抛出的InterruptedException异常。即使是那些跨线程共享缓存的用户,也只是有时候能中断他们的get调用,取决于那个线程先发出请求。
对于这个决定,我们的指导原则是让缓存始终表现得好像是在当前线程加载值。这个原则让使用缓存或每次都计算值可以简单地相互切换。如果老代码(加载值的代码)是不可中断的,那么新代码(使用缓存加载值的代码)多半也应该是不可中断的。
如上所述,Guava Cache在某种意义上支持中断。另一个意义上说,Guava Cache不支持中断,这使得LoadingCache成了一个有漏洞的抽象:当加载过程被中断了,就当作其他异常一样处理,这在大多数情况下是可以的;但如果多个线程在等待加载同一个缓存项,即使加载线程被中断了,它也不应该让其他线程都失败(捕获到包装在ExecutionException里的InterruptedException),正确的行为是让剩余的某个线程重试加载。为此,我们记录了一个bug。然而,与其冒着风险修复这个bug,我们可能会花更多的精力去实现另一个建议AsyncLoadingCache,这个实现会返回一个有正确中断行为的Future对象。
Guava Cache的get方法先在本地缓存中取,如果不存在,则会触发load方法。但load方法不能返回null。
Guava Cache的get方法先在本地缓存中取,如果不存在,则会触发load方法。但load方法不能返回null。
Guava Cache的get方法先在本地缓存中取,如果不存在,则会触发load方法。但load方法不能返回null。
保留"获取缓存-如果没有-则计算"[get-if-absent-compute]的原子语义
保留"获取缓存-如果没有-则计算"[get-if-absent-compute]的原子语义
保留"获取缓存-如果没有-则计算"[get-if-absent-compute]的原子语义
保留"获取缓存-如果没有-则计算"[get-if-absent-compute]的原子语义
保留"获取缓存-如果没有-则计算"[get-if-absent-compute]的原子语义
设想这样一个场景:进行某些热点数据查询时,如果缓存中没有,则去数据库中查询,并把查询到的结果保存到缓存中。
设想这样一个场景:进行某些热点数据查询时,如果缓存中没有,则去数据库中查询,并把查询到的结果保存到缓存中。
设想这样一个场景:进行某些热点数据查询时,如果缓存中没有,则去数据库中查询,并把查询到的结果保存到缓存中。
但假如说数据库中也没有呢?
这个时候load方法就会抛异常,例如:
- public enum TestGuavaCache {
- INSTANCE;
- private LoadingCache<String, Person> infos;
- TestGuavaCache() {
- infos = CacheBuilder.newBuilder().expireAfterAccess(10, TimeUnit.MINUTES).build(new CacheLoader<String, Person>() {
- public Person load(String key) throws Exception {
- //load from database
- Person p = loadFromDatabase();
- return p;
- }
- });
- }
- //假设数据库中不存在
- private Person loadFromDatabase() {
- return null;
- }
- public Person get(String key) {
- try {
- return infos.get(key);
- } catch (Exception e) {
- //log exception
- }
- return null;
- }
- }
这是因为Guava Cache认为cache null是无意义的,因此Guava Cache的javadoc里加粗说明:must not be null。
现实世界没那么理想,肯定会有null的情况,那怎么处理呢?我的处理一般是对Guava Cache的get方法做try-catch。
有时候cache null也是有意义的,例如对于一个key,假如数据库中也没有对应的value,那就把这个情况记录下来,
避免频繁的查询数据库(例如一些攻击性行为),直接在缓存中就把这个key挡住了。
怎么做呢?举例:
- @Test
- public void whenNullValue_thenOptional() {
- CacheLoader<String, Optional<String>> loader;
- loader = new CacheLoader<String, Optional<String>>() {
- @Override
- public Optional<String> load(String key) {
- return Optional.fromNullable(getSuffix(key));
- }
- };
- LoadingCache<String, Optional<String>> cache;
- cache = CacheBuilder.newBuilder().build(loader);
- assertEquals("txt", cache.getUnchecked("text.txt").get());
- assertFalse(cache.getUnchecked("hello").isPresent());
- }
- private String getSuffix(final String str) {
- int lastIndex = str.lastIndexOf('.');
- if (lastIndex == -1) {
- return null;
- }
- return str.substring(lastIndex + 1);
- }
3.什么时候用get,什么时候用getUnchecked
官网文档说:
- . If you have defined a CacheLoader that does not declare any checked exceptions then you can perform cache lookups using getUnchecked(K);
- however care must be taken not to call getUnchecked on caches whose CacheLoaders declare checked exceptions.
字面意思是,如果你的CacheLoader没有定义任何checked Exception,那你可以使用getUnchecked。
这一段话我也不是很理解。。官网上给了一个例子是,load方法没有声明throws Exception,那就可以使用getUnchecked:
- LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
- .expireAfterAccess(10, TimeUnit.MINUTES)
- .build(
- new CacheLoader<Key, Graph>() {
- public Graph load(Key key) { // no checked exception
- return createExpensiveGraph(key);
- }
- });
- ...
- return graphs.getUnchecked(key);
4.如何定义一个普通的Guava Cache,不需要用到load方法
假如只是简单的把Guava Cache当作HashMap或ConcurrentHashMap的替代品,不需要用到load方法,而是手动地插入,可以这样:
- com.google.common.cache.Cache<String, String> cache = CacheBuilder.newBuilder().expireAfterWrite(10, TimeUnit.MINUTES).build();
注意不能用LoadingCache了。
查找:
cache.getIfPresent("xx");
插入:
cache.put("xx", "xxx");
5.Guava Cache的超时机制不是精确的。
我曾经依赖Guava Cache的超时机制和RemovalListener,以实现类似定时任务的功能;后来发现Guava Cache的超时机制是不精确的,例如你设置cache的缓存时间是30秒,
那它存活31秒、32秒,都是有可能的。
官网说:
- Timed expiration is performed with periodic maintenance during writes and occasionally during reads, as discussed below.
- Caches built with CacheBuilder do not perform cleanup and evict values "automatically," or instantly after a value expires, or anything of the sort.
- Instead, it performs small amounts of maintenance during write operations, or during occasional read operations if writes are rare.
另一篇
google guava中有cache包,此包提供内存缓存功能。内存缓存需要考虑很多问题,包括并发问题,缓存失效机制,内存不够用时缓存释放,缓存的命中率,缓存的移除等等。 当然这些东西guava都考虑到了。
guava中使用缓存需要先声明一个CacheBuilder对象,并设置缓存的相关参数,然后调用其build方法获得一个Cache接口的实例。请看下面的代码和注释,注意在注释中指定了Cache的各个参数。
public static void main(String[] args) throws ExecutionException, InterruptedException{
//缓存接口这里是LoadingCache,LoadingCache在缓存项不存在时可以自动加载缓存
LoadingCache<Integer,Student> studentCache
//CacheBuilder的构造函数是私有的,只能通过其静态方法newBuilder()来获得CacheBuilder的实例
= CacheBuilder.newBuilder()
//设置并发级别为8,并发级别是指可以同时写缓存的线程数
.concurrencyLevel(8)
//设置写缓存后8秒钟过期
.expireAfterWrite(8, TimeUnit.SECONDS)
//设置缓存容器的初始容量为10
.initialCapacity(10)
//设置缓存最大容量为100,超过100之后就会按照LRU最近虽少使用算法来移除缓存项
.maximumSize(100)
//设置要统计缓存的命中率
.recordStats()
//设置缓存的移除通知
.removalListener(new RemovalListener<Object, Object>() {
@Override
public void onRemoval(RemovalNotification<Object, Object> notification) {
System.out.println(notification.getKey() + " was removed, cause is " + notification.getCause());
}
})
//build方法中可以指定CacheLoader,在缓存不存在时通过CacheLoader的实现自动加载缓存
.build(
new CacheLoader<Integer, Student>() {
@Override
public Student load(Integer key) throws Exception {
System.out.println("load student " + key);
Student student = new Student();
student.setId(key);
student.setName("name " + key);
return student;
}
}
); for (int i=0;i<20;i++) {
//从缓存中得到数据,由于我们没有设置过缓存,所以需要通过CacheLoader加载缓存数据
Student student = studentCache.get(1);
System.out.println(student);
//休眠1秒
TimeUnit.SECONDS.sleep(1);
} System.out.println("cache stats:");
//最后打印缓存的命中率等 情况
System.out.println(studentCache.stats().toString());
}
以上程序的输出如下:
load student 1
Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
1 was removed, cause is EXPIRED
load student 1 ...... Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
cache stats:
CacheStats{hitCount=17, missCount=3, loadSuccessCount=3, loadExceptionCount=0, totalLoadTime=1348802, evictionCount=2}
看看到在20此循环中命中次数是17次,未命中3次,这是因为我们设定缓存的过期时间是写入后的8秒,所以20秒内会失效两次,另外第一次获取时缓存中也是没有值的,所以才会未命中3次,其他则命中。
guava的内存缓存非常强大,可以设置各种选项,而且很轻量,使用方便。另外还提供了下面一些方法,来方便各种需要:
-
ImmutableMap<K, V> getAllPresent(Iterable<?> keys)
一次获得多个键的缓存值 -
put
和putAll
方法向缓存中添加一个或者多个缓存项 -
invalidate
和invalidateAll
方法从缓存中移除缓存项 -
asMap()
方法获得缓存数据的ConcurrentMap<K, V>
快照 -
cleanUp()
清空缓存 -
refresh(Key)
刷新缓存,即重新取缓存数据,更新缓存
另一篇
google guava中有cache包,此包提供内存缓存功能。内存缓存需要考虑很多问题,包括并发问题,缓存失效机制,内存不够用时缓存释放,缓存的命中率,缓存的移除等等。 当然这些东西guava都考虑到了。
guava中使用缓存需要先声明一个CacheBuilder对象,并设置缓存的相关参数,然后调用其build方法获得一个Cache接口的实例。请看下面的代码和注释,注意在注释中指定了Cache的各个参数。
public static void main(String[] args) throws ExecutionException, InterruptedException{
//缓存接口这里是LoadingCache,LoadingCache在缓存项不存在时可以自动加载缓存
LoadingCache<Integer,Student> studentCache
//CacheBuilder的构造函数是私有的,只能通过其静态方法newBuilder()来获得CacheBuilder的实例
= CacheBuilder.newBuilder()
//设置并发级别为8,并发级别是指可以同时写缓存的线程数
.concurrencyLevel(8)
//设置写缓存后8秒钟过期
.expireAfterWrite(8, TimeUnit.SECONDS)
//设置缓存容器的初始容量为10
.initialCapacity(10)
//设置缓存最大容量为100,超过100之后就会按照LRU最近虽少使用算法来移除缓存项
.maximumSize(100)
//设置要统计缓存的命中率
.recordStats()
//设置缓存的移除通知
.removalListener(new RemovalListener<Object, Object>() {
@Override
public void onRemoval(RemovalNotification<Object, Object> notification) {
System.out.println(notification.getKey() + " was removed, cause is " + notification.getCause());
}
})
//build方法中可以指定CacheLoader,在缓存不存在时通过CacheLoader的实现自动加载缓存
.build(
new CacheLoader<Integer, Student>() {
@Override
public Student load(Integer key) throws Exception {
System.out.println("load student " + key);
Student student = new Student();
student.setId(key);
student.setName("name " + key);
return student;
}
}
); for (int i=0;i<20;i++) {
//从缓存中得到数据,由于我们没有设置过缓存,所以需要通过CacheLoader加载缓存数据
Student student = studentCache.get(1);
System.out.println(student);
//休眠1秒
TimeUnit.SECONDS.sleep(1);
} System.out.println("cache stats:");
//最后打印缓存的命中率等 情况
System.out.println(studentCache.stats().toString());
}
以上程序的输出如下:
load student 1
Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
1 was removed, cause is EXPIRED
load student 1 ...... Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
cache stats:
CacheStats{hitCount=17, missCount=3, loadSuccessCount=3, loadExceptionCount=0, totalLoadTime=1348802, evictionCount=2}
看看到在20此循环中命中次数是17次,未命中3次,这是因为我们设定缓存的过期时间是写入后的8秒,所以20秒内会失效两次,另外第一次获取时缓存中也是没有值的,所以才会未命中3次,其他则命中。
guava的内存缓存非常强大,可以设置各种选项,而且很轻量,使用方便。另外还提供了下面一些方法,来方便各种需要:
-
ImmutableMap<K, V> getAllPresent(Iterable<?> keys)
一次获得多个键的缓存值 -
put
和putAll
方法向缓存中添加一个或者多个缓存项 -
invalidate
和invalidateAll
方法从缓存中移除缓存项 -
asMap()
方法获得缓存数据的ConcurrentMap<K, V>
快照 -
cleanUp()
清空缓存 -
refresh(Key)
刷新缓存,即重新取缓存数据,更新缓存
一.背景
缓存是我们在开发中为了提高系统的性能,把经常的访问业务的数据第一次把处理结果先放到缓存中,第二次就不用在对相同的业务数据在重新处理一遍,这样就提高了系统的性能。缓存分好几种:
(1)本地缓存。
(2)数据库缓存。
(3)分布式缓存。
分布式缓存比较常用的有memcached等,memcached是高性能的分布式内存缓存服务器,缓存业务处理结果,减少数据库访问次数和相同复杂逻辑处理的时间,以提高动态Web应用的速度、 提高可扩展性。
二.本地缓存在高并发下的问题以及解决
今天我们介绍的是本地缓存缓存,我们这边采用Java.util.concurrent.ConcurrentHashMap来保存,ConcurrentHashMap是一个线程安全的HashTable,并提供了一组和HashTable功能相同但是线程安全的方法,ConcurrentHashMap可以做到读取数据不加锁,提高了并发能力。我们先不考虑内存元素回收或者在保存数据会出现内存溢出的情况,我们用ConcurrentHashMap模拟本地缓存,当在高并发环境一下,会出现一些什么问题?
我们这边采用实现多个线程来模拟高并发场景。
第一种:我们先来看一下代码:
- public class TestConcurrentHashMapCache<K,V> {
- private final ConcurrentHashMap<K, V> cacheMap=new ConcurrentHashMap<K,V> ();
- public Object getCache(K keyValue,String ThreadName){
- System.out.println("ThreadName getCache=============="+ThreadName);
- Object value=null;
- //从缓存获取数据
- value=cacheMap.get(keyValue);
- //如果没有的话,把数据放到缓存
- if(value==null){
- return putCache(keyValue,ThreadName);
- }
- return value;
- }
- public Object putCache(K keyValue,String ThreadName){
- System.out.println("ThreadName 执行业务数据并返回处理结果的数据(访问数据库等)=============="+ThreadName);
- //可以根据业务从数据库获取等取得数据,这边就模拟已经获取数据了
- @SuppressWarnings("unchecked")
- V value=(V) "dataValue";
- //把数据放到缓存
- cacheMap.put(keyValue, value);
- return value;
- }
- public static void main(String[] args) {
- final TestConcurrentHashMapCache<String,String> TestGuaVA=new TestConcurrentHashMapCache<String,String>();
- Thread t1=new Thread(new Runnable() {
- @Override
- public void run() {
- System.out.println("T1======start========");
- Object value=TestGuaVA.getCache("key","T1");
- System.out.println("T1 value=============="+value);
- System.out.println("T1======end========");
- }
- });
- Thread t2=new Thread(new Runnable() {
- @Override
- public void run() {
- System.out.println("T2======start========");
- Object value=TestGuaVA.getCache("key","T2");
- System.out.println("T2 value=============="+value);
- System.out.println("T2======end========");
- }
- });
- Thread t3=new Thread(new Runnable() {
- @Override
- public void run() {
- System.out.println("T3======start========");
- Object value=TestGuaVA.getCache("key","T3");
- System.out.println("T3 value=============="+value);
- System.out.println("T3======end========");
- }
- });
- t1.start();
- t2.start();
- t3.start();
- }
- }
我们看一下执行结果,如图所示:
我们实现了本地缓存代码,我们执行一下结果,发现在多线程时,出现了在缓存里没有缓存时,会执行一样执行多次的业务数据并返回处理的数据,我们分析一下出现这种情况的:
(1)当线程T1访问cacheMap里面有没有,这时根据业务到后台处理业务数据并返回处理数据,并放入缓存。
(2)当线程T2访问cacheMap里面同样也没有,也把根据业务到后台处理业务数据并返回处理数据,并放入缓存。
第二种:
这样相同的业务并处理两遍,如果在高并发的情况下相同的业务不止执行两遍,这样这样跟我们当初做缓存不相符合,这时我们想到了Java多线程时,在执行获取缓存上加上Synchronized,代码如下:
- public class TestConcurrentHashMapCache<K,V> {
- private final ConcurrentHashMap<K, V> cacheMap=new ConcurrentHashMap<K,V> ();
- public <span style="color:#ff0000;">synchronized </span>Object getCache(K keyValue,String ThreadName){
- System.out.println("ThreadName getCache=============="+ThreadName);
- Object value=null;
- //从缓存获取数据
- value=cacheMap.get(keyValue);
- //如果没有的话,把数据放到缓存
- if(value==null){
- return putCache(keyValue,ThreadName);
- }
- return value;
- }
- public Object putCache(K keyValue,String ThreadName){
- System.out.println("ThreadName 执行业务数据并返回处理结果的数据(访问数据库等)=============="+ThreadName);
- //可以根据业务从数据库获取等取得数据,这边就模拟已经获取数据了
- @SuppressWarnings("unchecked")
- V value=(V) "dataValue";
- //把数据放到缓存
- cacheMap.put(keyValue, value);
- return value;
- }
- public static void main(String[] args) {
- final TestConcurrentHashMapCache<String,String> TestGuaVA=new TestConcurrentHashMapCache<String,String>();
- Thread t1=new Thread(new Runnable() {
- @Override
- public void run() {
- System.out.println("T1======start========");
- Object value=TestGuaVA.getCache("key","T1");
- System.out.println("T1 value=============="+value);
- System.out.println("T1======end========");
- }
- });
- Thread t2=new Thread(new Runnable() {
- @Override
- public void run() {
- System.out.println("T2======start========");
- Object value=TestGuaVA.getCache("key","T2");
- System.out.println("T2 value=============="+value);
- System.out.println("T2======end========");
- }
- });
- Thread t3=new Thread(new Runnable() {
- @Override
- public void run() {
- System.out.println("T3======start========");
- Object value=TestGuaVA.getCache("key","T3");
- System.out.println("T3 value=============="+value);
- System.out.println("T3======end========");
- }
- });
- t1.start();
- t2.start();
- t3.start();
- }
- }
执行结果,如图所示:
这样就实现了串行,在高并发行时,就不会出现了第二个访问相同业务,肯定是从缓存获取,但是加上Synchronized变成串行,这样在高并发行时性能也下降了。
第三种:
我们为了实现性能和缓存的结果,我们采用Future,因为Future在计算完成时获取,否则会一直阻塞直到任务转入完成状态和ConcurrentHashMap.putIfAbsent方法,代码如下:
- public class TestFutureCahe<K,V> {
- private final ConcurrentHashMap<K, Future<V>> cacheMap=new ConcurrentHashMap<K, Future<V>> ();
- public Object getCache(K keyValue,String ThreadName){
- Future<V> value=null;
- try{
- System.out.println("ThreadName getCache=============="+ThreadName);
- //从缓存获取数据
- value=cacheMap.get(keyValue);
- //如果没有的话,把数据放到缓存
- if(value==null){
- value= putCache(keyValue,ThreadName);
- return value.get();
- }
- return value.get();
- }catch (Exception e) {
- }
- return null;
- }
- public Future<V> putCache(K keyValue,final String ThreadName){
- // //把数据放到缓存
- Future<V> value=null;
- Callable<V> callable=new Callable<V>() {
- @SuppressWarnings("unchecked")
- @Override
- public V call() throws Exception {
- //可以根据业务从数据库获取等取得数据,这边就模拟已经获取数据了
- System.out.println("ThreadName 执行业务数据并返回处理结果的数据(访问数据库等)=============="+ThreadName);
- return (V) "dataValue";
- }
- };
- FutureTask<V> futureTask=new FutureTask<V>(callable);
- value=cacheMap.putIfAbsent(keyValue, futureTask);
- if(value==null){
- value=futureTask;
- futureTask.run();
- }
- return value;
- }
- public static void main(String[] args) {
- final TestFutureCahe<String,String> TestGuaVA=new TestFutureCahe<String,String>();
- Thread t1=new Thread(new Runnable() {
- @Override
- public void run() {
- System.out.println("T1======start========");
- Object value=TestGuaVA.getCache("key","T1");
- System.out.println("T1 value=============="+value);
- System.out.println("T1======end========");
- }
- });
- Thread t2=new Thread(new Runnable() {
- @Override
- public void run() {
- System.out.println("T2======start========");
- Object value=TestGuaVA.getCache("key","T2");
- System.out.println("T2 value=============="+value);
- System.out.println("T2======end========");
- }
- });
- Thread t3=new Thread(new Runnable() {
- @Override
- public void run() {
- System.out.println("T3======start========");
- Object value=TestGuaVA.getCache("key","T3");
- System.out.println("T3 value=============="+value);
- System.out.println("T3======end========");
- }
- });
- t1.start();
- t2.start();
- t3.start();
- }
- }
线程T1或者线程T2访问cacheMap,如果都没有时,这时执行了FutureTask来完成异步任务,假如线程T1执行了FutureTask,并把保存到ConcurrentHashMap中,通过PutIfAbsent方法,因为putIfAbsent方法如果不存在key对应的值,则将value以key加入Map,否则返回key对应的旧值。这时线程T2进来时可以获取Future对象,如果没值没关系,这时是对象的引用,等FutureTask执行完,在通过get返回。
我们问题解决了高并发访问缓存的问题,可以回收元素这些,都没有,容易造成内存溢出,Google Guava Cache在这些问题方面都做得挺好的,接下来我们介绍一下。
三.Google Guava Cache的介绍和应用
http://www.java2s.com/Code/Jar/g/Downloadguava1401jar.htm 下载对应的jar包
Guava Cache与ConcurrentMap很相似,Guava Cache能设置回收,能解决在大数据内存溢出的问题,源代码如下:
public class TestGuaVA<K,V> {
private Cache<K, V> cache= CacheBuilder.newBuilder() .maximumSize(2).expireAfterWrite(10, TimeUnit.MINUTES).build();
public Object getCache(K keyValue,final String ThreadName){
Object value=null;
try {
System.out.println("ThreadName getCache=============="+ThreadName);
//从缓存获取数据
value = cache.get(keyValue, new Callable<V>() {
@SuppressWarnings("unchecked")
public V call() {
System.out.println("ThreadName 执行业务数据并返回处理结果的数据(访问数据库等)=============="+ThreadName);
return (V) "dataValue";
}
});
} catch (ExecutionException e) {
e.printStackTrace();
}
return value;
}
public static void main(String[] args) {
final TestGuaVA<String,String> TestGuaVA=new TestGuaVA<String,String>();
Thread t1=new Thread(new Runnable() {
@Override
public void run() {
System.out.println("T1======start========");
Object value=TestGuaVA.getCache("key","T1");
System.out.println("T1 value=============="+value);
System.out.println("T1======end========");
}
});
Thread t2=new Thread(new Runnable() {
@Override
public void run() {
System.out.println("T2======start========");
Object value=TestGuaVA.getCache("key","T2");
System.out.println("T2 value=============="+value);
System.out.println("T2======end========");
}
});
Thread t3=new Thread(new Runnable() {
@Override
public void run() {
System.out.println("T3======start========");
Object value=TestGuaVA.getCache("key","T3");
System.out.println("T3 value=============="+value);
System.out.println("T3======end========");
}
});
t1.start();
t2.start();
t3.start();
}
}
说明:
CacheBuilder.newBuilder()后面能带一些设置回收的方法:
(1)maximumSize(long):设置容量大小,超过就开始回收。
(2)expireAfterAccess(long, TimeUnit):在这个时间段内没有被读/写访问,就会被回收。
(3)expireAfterWrite(long, TimeUnit):在这个时间段内没有被写访问,就会被回收 。
(4)removalListener(RemovalListener):监听事件,在元素被删除时,进行监听。
执行结果,如图所示: