在项目的开发中,如果出现一些数据量较大且需要频繁读取而又很少改动场景时,基本上会考虑使用缓存,以降低数据库的压力,毕竟访问到数据库的数据是相当耗费资源。
说到缓存,大家可能更多先想到使用HashMap存储,代码如下:public Map<String, Object> cache = new HashMap<String, Object>();
public Object getData(String key){
Object data = cache.get(key);
if (data == null) {
data = DB.getData(); //从数据库中获取数据(伪代码)
cache.put(key, data);
}
return data;
}
这样就不用每次访问页面都去读数据库的数据,提高了性能。但是代码有缺陷:HashMap不是线程安全,所以多线程并发的时候会出现问题,所以需要加锁同步,代码如下:
public Map<String, Object> cache = new HashMap<String, Object>();
public synchronized Object getData(String key){
Object data = cache.get(key);
if (data == null) {
data = DB.getData(); //从数据库中获取数据(伪代码)
cache.put(key, data);
}
return data;
}
这样就解决了线程安全的问题。但是加了锁之后,每次只能进去一个线程,尽管是不同缓存数据,还是要等待上一个线程执行完才能执行,性能出现问题。
我们可以尝试使用ConcurrentHashMap来代替HashMap,由于ConcurrentHashMap是线程安全,所以代码不需要加同步锁,并且使用了分段的原理,比HashMap有更好的解决并发操作。
public Map<String, Object> cache = new ConcurrentHashMap<String, Object>();
public Object getData(String key){
Object data = cache.get(key);
if (data == null) {
data = DB.getData(); //从数据库中获取数据(伪代码)
cache.put(key, data);
}
return data;
}
这样的代码还是有问题,就是第一次初始化缓存是如果需要经历开销很大的计算(例如需要耗费10s),那么在这10s内访问同一个缓存的线程都需要进行重新初始化缓存的操作,这样既浪费时间又浪费资源。 我们可以使用一下Callable接口、Future接口、FutureTask类来实现,Callable是一个线程任务接口,Future是有关任务处理的接口,Future.get会获取任务的状态,如果任务已经完成,则能返回结果,否则get将阻塞直到任务 进入完成状态,然后返回结果或抛出异常,FutureTask是Future的实现类。
public Map<String, Future<Object>> cache = new ConcurrentHashMap<String, Future<Object>>();
public Object getData(String key) throws InterruptedException {
Future<Object> future = cache.get(key);
if (future == null) {
Callable<Object> call = new Callable<Object>() {
@Override
public Object call() throws Exception {
return DB.getData();
}
};
FutureTask<Object> task = new FutureTask<Object>(call);
future = task;
cache.put(key, future);
task.run(); //这里再从数据库获取数据
}
try {
return future.get();
} catch (ExecutionException e) {
//处理执行任务时出现异常
}
}
这样就能解决多次初始化同一个缓存的问题,如果是任务还未执行完,下一个请求就阻塞,直到任务执行完再返回数据,有效节省资源。但是代码还是存在问题,例如在初始化Future任务时的并发问题,并且执行到一半时任务出现中断的情况。
改进代码如下:
public ConcurrentHashMap<String, Future<Object>> cache = new ConcurrentHashMap<String, Future<Object>>();
public Object getData(String key) throws InterruptedException {
while (true) {
Future<Object> future = cache.get(key);
if (future == null) {
Callable<Object> call = new Callable<Object>(){
@Override
public Object call() throws Exception {
return DB.getData();
}
};
FutureTask<Object> task = new FutureTask<Object>(call);
cache.putIfAbsent(key, task);
if (future == null) {
future = task;
task.run();
}
}
try {
return future.get();
} catch (CancellationException e) {
cache.remove(key, future); //任务出现取消时的处理
} catch (ExecutionException e) {
//处理执行任务时出现异常
}
}
}
这样代码就解决了初始化Future任务并发的问题,还解决了任务出现取消操作时候的处理,putIfAbsent有效的解决将相同的任务重复加到Map中(注意这是ConcurrentHashMap的方法,需要使用ConcurrentHashMap去定义对象),避免了上一个版本的漏洞。
这样,缓存最终实现。
大家看完还有什么不懂或是觉得代码哪里有问题,欢迎留言交流。