如何实现缓存

37 阅读2分钟

如何实现缓存

 几乎所有的服务器应用都会使用缓存。重用之前的结果可以降低延迟,提高吞吐量

那么,代价是什么呢? 消耗更多的内存!

和许多“重复发明的轮子”一样,实现一个缓存看上去十分简单,其实不然。设计不佳的简单缓存不仅可能达不到足够的性能提升,甚至可能返回错误的结果。

public interface Computable<A,V> {
    V compute(A arg) throws InterruptedException;
}

public class ExpensiveFunction implements Computable<String,BigInteger>{

    @Override
    public BigInteger compute(String arg) throws InterruptedException {
        // 很长时间的计算后
        return new BigInteger(arg);
    }
}

现有以上任务,以下将给出几个错误的示例

示例1:并发错误

public class Memorizer<A,V> implements Computable<A,V> {
    private final Map<A,V> cache = new ConcurrentHashMap<>();
    private final Computable<A,V> c;

    public Memorizer(Computable<A, V> c) {
        this.c = c;
    }

    @Override
    public V compute(A arg) throws InterruptedException {
        V result = cache.get(arg);
        //以下是一个先检查后执行操作
        if (result == null) {
            result = c.compute(arg);
            cache.put(arg, result);
        }
        return result;
    }
}result;
    }
}

我们采用一个ConcurrentHashMap来储存计算结果,并发类的ConcurrentHashMap保证了单个cache对象内部不会出现并发问题,再通过一个装饰器模式将Memorizer的compute操作转发到c.compute上,可谓是十分的优雅!

然而,注释中已经指出,这是一个十分典型的先检查后执行操作。如果这个操作是一个很费时间的操作,那么触发这种并发错误将很十分常见。这种并发错误会导致程序做重复的计算任务,还有优化空间!

这个先检查后执行操作肯定是不能加锁的,那怎么办呢

示例2:完美的缓存实现

public class Memorizer2<A,V> implements Computable<A,V> {
    private final Map<A, Future<V>> cache = new ConcurrentHashMap<>();
    private final Computable<A,V> c;

    public Memorizer2(Computable<A, V> c) {
        this.c = c;
    }

    @Override
    public V compute(A arg) throws InterruptedException {
        while (true) {
            Future<V> f = cache.get(arg);
            if (f == null) {
                FutureTask<V> ft = new FutureTask<>(() -> c.compute(arg));
                f = cache.putIfAbsent(arg, ft);
                if (f == null) {
                   f = ft;
                   ft.run();
               }
            }
            try {
                return f.get();
            } catch (CancellationException e) {
                //计算失败,移除缓存,防止缓存污染
                cache.remove(arg, f);
            } catch (ExecutionException e) {
                throw new RuntimeException(e);
            }
       }
    }
}

我们更改了ConcurrentHashMap的值类型,从值本身变成了Future,核心思想在于将任务注册到ConcurrentHashMap中。这样一来,其他线程能以最快速度得知“已经有别人在做这个任务了”。并通过putIfAbsent方法防止重复提交。

至此,缓存代码已经足够健壮。当然可以在此基础上实现其他功能,如缓存逾期,缓存清理等等。