简介
本文的主体内容,大概是从一个简单的需求
然后分析对应的并发问题,高效可伸缩,缓存污染问题,最后得到一个让我们满意的Map作为对应的实现.
最简单的实现
首先,先放一个最简单的实现.我相信,大部分的人(包括我)在内,在大部分情况就是这么实现的..
package current; import java.util.HashMap; public class Foo { private HashMap<String,String> dataCache = new HashMap<String, String>(); public String getData(String key){ String data = dataCache.get(key); if(data == null){ String newData = fetchData(key); dataCache.put(key,data); return newData; } else { return data; } } /** * 获取对应的数据 */ private String fetchData(String key){ return key + ":data"; } }
其实,这种实现基本上能满足没有高并发的大部分情况了..当然,作为码农,是绝对不会到此为止的..相信大部分的人也都发现了一个问题
解决的方法有两个,
2 把HashMap换成 ConcurrentHashMap .
一般来说,相信大家都会选择第二种,使用JDK自带的同步容器ConcurrentHashMap.
下面是改进以后的代码
package current; import java.util.concurrent.ConcurrentHashMap; public class Foo { private ConcurrentHashMap<String,String> dataCache = new ConcurrentHashMap<String, String>(); public String getData(String key){ String data = dataCache.get(key); if(data == null){ String newData = fetchData(key); dataCache.put(key,data); return newData; } else { return data; } } /** * 获取对应的数据 */ private String fetchData(String key){ return key + ":data"; } }
到此为止,难道结束了嘛..我们继续挑毛病,如果在高并发的情况下,可能对同一个key初始化两次(针对某一些缓存对象只能被初始化一次的情况,这种漏洞将会带来很大的安全风险). 针对这个问题,那么,可以引入一个闭锁实现 FutureTask
使用FutureTask来解决初始化两次的问题.
对应的代码如下
package current; import java.util.concurrent.*; public class Foo { private ConcurrentHashMap<String,FutureTask<String>> dataCache = new ConcurrentHashMap<String, FutureTask<String>>(); public String getData(final String key){ FutureTask<String> future = dataCache.get(key); if(future == null){ future = new FutureTask<String>(new Callable<String>() { @Override public String call() throws Exception { return fetchData(key); } }); dataCache.put(key,future); future.run(); } try { return future.get(); } catch (InterruptedException e) { //这里注意一下,引入futuretask造成了缓存污染,如果在future.run失败,需要在缓存中删除对应的记录 dataCache.remove(key); } catch (ExecutionException e) {} return null; } /** * 获取对应的数据 */ private String fetchData(String key){ return key + ":data"; } }
这个代码,看起来已经非常完美.如果其他线程看到前面的请求正在fetchData,那么也会等待run结束.但是,这还没结束.有一个地方没有做到同步
FutureTask<String> future = dataCache.get(key); if(future == null){ future = new FutureTask<String>(new Callable<String>() { @Override public String call() throws Exception { return fetchData(key); } }); dataCache.put(key,future);
高并发下 ,会出现,同一个key的future被创建两次,然后分别put覆盖到cache中.注意,这里的不同步和之前
String data = dataCache.get(key); if(data == null){ String newData = fetchData(key); dataCache.put(key,data);
这个代码的不同步是有非常本质的区别的.后者是等fetchData完成以后再put,而前者只是先往cache中注册一个future.然后马上就put了.两者的时间消耗完全不同.
为了解决上面的复合操作(其实就是put if absent),解决的方式就非常简单了,就是使用ConcurrentHashMap自带的putIfAbsent方法.
最终版本
package current;
import java.util.concurrent.*;
public class Foo {
private ConcurrentHashMap<String,FutureTask<String>> dataCache = new ConcurrentHashMap<String, FutureTask<String>>();
public String getData(final String key){
FutureTask<String> future = dataCache.get(key);
if(future == null){
future = new FutureTask<String>(new Callable<String>() {
@Override
public String call() throws Exception {
return fetchData(key);
}
});
FutureTask old = dataCache.putIfAbsent(key,future);
if(old == null){
future.run();
} else {
future = old;
}
}
try {
return future.get();
} catch (InterruptedException e) {
//这里注意一下,引入futuretask造成了缓存污染,如果在future.run失败,需要在缓存中删除对应的记录
dataCache.remove(key);
} catch (ExecutionException e) {}
return null;
}
/**
* 获取对应的数据
*/
private String fetchData(String key){
return key + ":data";
}
}
貌似有些地方,还会在getData方法中加一下while(true)循环.如果fetchData出错,那就继续获取.这个就太霸气了点.我觉得这样已经差不多了..已经能满足99.99%的需求了
总结
其实,大部分情况,只是缓存一下数据,估计就做到ConcurrentHashMap那一层就差不多了.会引入future的非常少.这个就看自己看自己系统的情况吧.