LSF系列-把计算结果放到一个Map中作为缓存

       简介

         本文的主体内容,大概是从一个简单的需求

写道
把计算结果放到一个Map中作为缓存

 然后分析对应的并发问题,高效可伸缩,缓存污染问题,最后得到一个让我们满意的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";
    }
}

 其实,这种实现基本上能满足没有高并发的大部分情况了..当然,作为码农,是绝对不会到此为止的..相信大部分的人也都发现了一个问题

写道
HashMap并不是线程安全的.

 解决的方法有两个,

写道
1 getData方法做同步 .加上 synchronized关键字
2 把HashMap换成 ConcurrentHashMap .

 一般来说,相信大家都会选择第二种,使用JDK自带的同步容器ConcurrentHashMap.

写道
这里必须明确一下,ConcurrentHashMap并没有实现独占的访问加锁.所以,如果你有这方面的需要的话,使用synchronized是你唯一的选择.

 

下面是改进以后的代码

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的非常少.这个就看自己看自己系统的情况吧.

你可能感兴趣的:(map)