如何亲自够造一个高效可伸缩的缓存

缓存可以降低系统延迟,负载,提高吞吐量。我们一般用到的第三方缓存框架OSCache,ehcache,memcache,redis等。当然一 个缓存框架是很复杂的,要考虑key的生成,索引生成,文件结构的生成,序列化还有反序列化,还有淘汰算法,过期检查,还有持久化媒介等。

    当然我们不会去实现这么一个复杂的缓存系统。我们下面讲一个用java简单高效可伸缩的内存缓存系统。

    背景:

    我们现在有一个比较耗时的方法,我们希望把这个耗时的方法计算的结果缓存起来,同样的参数请求过来就不需要进行昂贵的计算过程了。

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

 方案一:

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

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

    public synchronized V compute(A arg) {
        V result = cache.get(arg);
        if (result == null) {
            result = c.compute(arg);
            cache.put(arg, result);
        }

        return result;
    }
}

上面我们用HashMap实现了一个简单的缓存,它把我们计算的结果缓存了起来,并且通过sychronized保证了多个客户端访问情况下的线程安全。但是这个缓存效果可能达不到我们预期。这种同步策略,只能保证每次
只有一个线程访问(通过this来保证线程同步)。假如有一个线程进行了一个十分耗时的计算,那么其他线程都会被阻塞。这恐怕是不能接受的。


方案二:

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

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

    public V compute(A arg) {
        V result = cache.get(arg);
        if (result == null) {
            result = c.compute(arg);
            cache.put(arg, result);
        }

        return result;
    }
}

为了避免上诉HashMap结合sychronized导致的线程长时间独占锁阻塞引起的伸缩性性能问题。我们采用了ConcurrentHashMap来代替HashMap。这里我们为什么不用HashTable呢,因为HashTable是通过锁定整个
hash表来保证线程安全性的,get,put,remove等操作都会独占锁。但是ConcurrentHashMap采用了段锁,它把hash表分成了16段,每个操作只锁其操作的那一段hash表,这样就提高了它的性能。
  用ConcurrentHashMap的方法,我们避免了方法一引起的性能问题。它拥有更好的性能,更好的伸缩性。但是上诉方案可能会引入了另一个问题:会计算相同的值。当一个线程正在进行一次长时间计算,但是其他线程又
不知道,就可能会引起上边的问题。
  多次计算相同的值,会对性能带来影响。

 方案三:

扫描二维码关注公众号,回复: 496447 查看本文章
public class CHMWithFutureCache<A, V> implements Computable<A, V> {
    private final Map<A, Future<V>> cache = new ConcurrentHashMap<A, Future<V>>();
    private final Computable<A, V> c;

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

    public V compute(final A arg) {
        Future<V> f = cache.get(arg);
        if (f == null) {
            Callable<V> eval = new Callable<V>() {
                public V call() throws Exception {
                    return c.compute(arg);
                }
            };

            FutureTask<V> ft = new FutureTask<V>(eval);

            f = ft;
            cache.put(arg, ft);
            ft.run();
        }

        try {
            return f.get();
        } catch (Exception e) {
            //此处处理异常方式不好
            return null;
        }
    }
}
方案二的问题是,可能会导致重复计算。我们要达到的目的是什么呢:如果请求过来发现缓存里边没值的化,不是马上进行计算,而是去检查一下是否已经有计算在进行中了。如果有,那么阻塞等待计算返回。有没有可以
实现提交一个任务,然后等待它计算完成之后再取结果的组件呢?有,FutureTask!我们往缓存里边放的不是最终的结果,而是一个FutureTask。这样我们就能避免因为上诉问题产生的重复计算的问题。

 方案四:

     上面方案三还有没有什么问题呢?看到if判断语句没?先检查后执行,这个操作不是原子性的,还是会导致重复提交任务。

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

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

    public V compute(final A arg) {
        Future<V> f = cache.get(arg);
        if (f == null) {
            Callable<V> eval = new Callable<V>() {
                public V call() throws Exception {
                    return c.compute(arg);
                }
            };

            FutureTask<V> ft = new FutureTask<V>(eval);

            f = cache.putIfAbsent(arg, ft);
            if (f == null) {
                f = ft;
                ft.run();
            }
        }

        try {
            return f.get();
        } catch (Exception e) {
            return null;
        }
    }

}

我们采用了putIfabsent来避免上诉情况引起的重复计算的问题。如果已经提交过任务了,那么返回的是null,就不再进行提交计算了。

 方案五:解决缓存污染问题。

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

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

    public V compute(final A arg) {
        Future<V> f = cache.get(arg);
        while (true) {
            if (f == null) {
                Callable<V> eval = new Callable<V>() {
                    public V call() throws Exception {
                        return c.compute(arg);
                    }
                };

                FutureTask<V> ft = new FutureTask<V>(eval);

                f = cache.putIfAbsent(arg, ft);
                if (f == null) {
                    f = ft;
                    ft.run();
                }
            }

            try {
                return f.get();
            } catch (Exception e) {
                cache.remove(arg);
            }
        }
    }

}

假如提交的任务因为某些原因执行失败了,那么如果不做任何处理的话就会造成缓存污染,后续相同参数近来的请求都将会失败,所以需要在任务执行失败的时候将相关的FutureTask remove掉。

上诉示例摘自Java并发编程实战。

猜你喜欢

转载自study-a-j8.iteye.com/blog/2365018