Guava Cache内存缓存使用实践-定时异步刷新及简单抽象封装

https://www.cnblogs.com/boothsun/p/5848143.html

版权声明:本文为博主原创文章,未经博主允许不得转载。 http://blog.csdn.net/u012859681/article/details/75220605

目录(?)[-]

  1. 简单使用定时过期
  2. 进阶使用定时刷新
  3. 进阶使用异步刷新
  4. TIPS
  5. 简单抽象封装

缓存在应用中是必不可少的,经常用的如redis、memcache以及内存缓存等。Guava是Google出的一个工具包,它里面的cache即是对本地内存缓存的一种实现,支持多种缓存过期策略。 
Guava cache的缓存加载方式有两种:

  • CacheLoader
  • Callable callback

具体两种方式的介绍看官方文档:http://ifeve.com/google-guava-cachesexplained/

接下来看看常见的一些使用方法。 
后面的示例实践都是以CacheLoader方式加载缓存值。

1.简单使用:定时过期

LoadingCache<String, Object> caches = CacheBuilder.newBuilder()
                .maximumSize(100)
                .expireAfterWrite(10, TimeUnit.MINUTES)
                .build(new CacheLoader<String, Object>() {
                    @Override
                    public Object load(String key) throws Exception {
                        return generateValueByKey(key);
                    }
                });
try {
    System.out.println(caches.get("key-zorro"));
} catch (ExecutionException e) {
    e.printStackTrace();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

如代码所示新建了名为caches的一个缓存对象,maximumSize定义了缓存的容量大小,当缓存数量即将到达容量上线时,则会进行缓存回收,回收最近没有使用或总体上很少使用的缓存项。需要注意的是在接近这个容量上限时就会发生,所以在定义这个值的时候需要视情况适量地增大一点。 
另外通过expireAfterWrite这个方法定义了缓存的过期时间,写入十分钟之后过期。 
在build方法里,传入了一个CacheLoader对象,重写了其中的load方法。当获取的缓存值不存在或已过期时,则会调用此load方法,进行缓存值的计算。 
这就是最简单也是我们平常最常用的一种使用方法。定义了缓存大小、过期时间及缓存值生成方法。

如果用其他的缓存方式,如redis,我们知道上面这种“如果有缓存则返回;否则运算、缓存、然后返回”的缓存模式是有很大弊端的。当高并发条件下同时进行get操作,而此时缓存值已过期时,会导致大量线程都调用生成缓存值的方法,比如从数据库读取。这时候就容易造成数据库雪崩。这也就是我们常说的“缓存穿透”。 
而Guava cache则对此种情况有一定控制。当大量线程用相同的key获取缓存值时,只会有一个线程进入load方法,而其他线程则等待,直到缓存值被生成。这样也就避免了缓存穿透的危险。

2.进阶使用:定时刷新

如上的使用方法,虽然不会有缓存穿透的情况,但是每当某个缓存值过期时,老是会导致大量的请求线程被阻塞。而Guava则提供了另一种缓存策略,缓存值定时刷新:更新线程调用load方法更新该缓存,其他请求线程返回该缓存的旧值。这样对于某个key的缓存来说,只会有一个线程被阻塞,用来生成缓存值,而其他的线程都返回旧的缓存值,不会被阻塞。 
这里就需要用到Guava cache的refreshAfterWrite方法。如下所示:

LoadingCache<String, Object> caches = CacheBuilder.newBuilder()
                .maximumSize(100)
                .refreshAfterWrite(10, TimeUnit.MINUTES)
                .build(new CacheLoader<String, Object>() {
                    @Override
                    public Object load(String key) throws Exception {
                        return generateValueByKey(key);
                    }
                });
try {
    System.out.println(caches.get("key-zorro"));
} catch (ExecutionException e) {
    e.printStackTrace();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

如代码所示,每隔十分钟缓存值则会被刷新。

此外需要注意一个点,这里的定时并不是真正意义上的定时。Guava cache的刷新需要依靠用户请求线程,让该线程去进行load方法的调用,所以如果一直没有用户尝试获取该缓存值,则该缓存也并不会刷新。

3.进阶使用:异步刷新

如2中的使用方法,解决了同一个key的缓存过期时会让多个线程阻塞的问题,只会让用来执行刷新缓存操作的一个用户线程会被阻塞。由此可以想到另一个问题,当缓存的key很多时,高并发条件下大量线程同时获取不同key对应的缓存,此时依然会造成大量线程阻塞,并且给数据库带来很大压力。这个问题的解决办法就是将刷新缓存值的任务交给后台线程,所有的用户请求线程均返回旧的缓存值,这样就不会有用户线程被阻塞了。 
详细做法如下:

ListeningExecutorService backgroundRefreshPools = 
                MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(20));
        LoadingCache<String, Object> caches = CacheBuilder.newBuilder()
                .maximumSize(100)
                .refreshAfterWrite(10, TimeUnit.MINUTES)
                .build(new CacheLoader<String, Object>() {
                    @Override
                    public Object load(String key) throws Exception {
                        return generateValueByKey(key);
                    }

                    @Override
                    public ListenableFuture<Object> reload(String key,
                            Object oldValue) throws Exception {
                        return backgroundRefreshPools.submit(new Callable<Object>() {

                            @Override
                            public Object call() throws Exception {
                                return generateValueByKey(key);
                            }
                        });
                    }
                });
try {
    System.out.println(caches.get("key-zorro"));
} catch (ExecutionException e) {
    e.printStackTrace();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

在上面的代码中,我们新建了一个线程池,用来执行缓存刷新任务。并且重写了CacheLoader的reload方法,在该方法中建立缓存刷新的任务并提交到线程池。 
注意此时缓存的刷新依然需要靠用户线程来驱动,只不过和2不同之处在于该用户线程触发刷新操作之后,会立马返回旧的缓存值。

TIPS

  • 可以看到防缓存穿透和防用户线程阻塞都是依靠返回旧值来完成的。所以如果没有旧值,同样会全部阻塞,因此应视情况尽量在系统启动时将缓存内容加载到内存中。

  • 在刷新缓存时,如果generateValueByKey方法出现异常或者返回了null,此时旧值不会更新。

  • 题外话:在使用内存缓存时,切记拿到缓存值之后不要在业务代码中对缓存直接做修改,因为此时拿到的对象引用是指向缓存真正的内容的。如果需要直接在该对象上进行修改,则在获取到缓存值后拷贝一份副本,然后传递该副本,进行修改操作。(我曾经就犯过这个低级错误 - -!)

4.简单抽象封装

如下为基于Guava cache抽象出来的一个缓存工具类。(抽象得不好,勉强能用 - -!)。 
有改进意见麻烦多多指教。

/**
 * @description: 利用guava实现的内存缓存。缓存加载之后永不过期,后台线程定时刷新缓存值。刷新失败时将继续返回旧缓存。
 *                  需要在子类中初始化refreshDuration、refreshTimeunitType、cacheMaximumSize三个参数
 *                  后台刷新线程池为该系统中所有子类共享,大小为20.
 * @author: luozhuo
 * @date: 2017年6月21日 上午10:03:45 
 * @version: V1.0.0
 * @param <K>
 * @param <V>
 */
public abstract class ZorroGuavaCache <K, V> {

    /**
     * 缓存自动刷新周期
     */
    protected int refreshDuration;

    /**
     * 缓存刷新周期时间格式
     */
    protected TimeUnit refreshTimeunitType;

    /**
     * 缓存最大容量
     */
    protected int cacheMaximumSize;

    private LoadingCache<K, V> cache;
    private ListeningExecutorService backgroundRefreshPools = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(20));

    /**
     * @description: 初始化所有protected字段:
     * refreshDuration、refreshTimeunitType、cacheMaximumSize
     * @author: luozhuo
     * @date: 2017年6月13日 下午2:49:19
     */
    protected abstract void initCacheFields();

    /**
     * @description: 定义缓存值的计算方法
     * @description: 新值计算失败时抛出异常,get操作时将继续返回旧的缓存
     * @param key
     * @return
     * @author: luozhuo
     * @throws Exception 
     * @date: 2017年6月14日 下午7:11:10
     */
    protected abstract V getValueWhenExpire(K key) throws Exception;

    /**
     * @description: 提供给外部使用的获取缓存方法,由实现类进行异常处理
     * @param key
     * @return
     * @author: luozhuo
     * @date: 2017年6月15日 下午12:00:57
     */
    public abstract V getValue(K key);

    /**
     * @description: 获取cache实例
     * @return
     * @author: luozhuo
     * @date: 2017年6月13日 下午2:50:11
     */
    private LoadingCache<K, V> getCache() {
        if(cache == null){
            synchronized (this) {
                if(cache == null){
                    initCacheFields();

                    cache = CacheBuilder.newBuilder()
                            .maximumSize(cacheMaximumSize)
                            .refreshAfterWrite(refreshDuration, refreshTimeunitType)
                            .build(new CacheLoader<K, V>() {
                                @Override
                                public V load(K key) throws Exception {
                                    return getValueWhenExpire(key);
                                }

                                @Override
                                public ListenableFuture<V> reload(final K key,
                                        V oldValue) throws Exception {
                                    return backgroundRefreshPools.submit(new Callable<V>() {
                                        public V call() throws Exception {
                                            return getValueWhenExpire(key);
                                        }
                                    });
                                }
                            });
                }
            }
        }
        return cache;
    }


    /**
     * @description: 从cache中拿出数据的操作
     * @param key
     * @return
     * @throws ExecutionException
     * @author: luozhuo
     * @date: 2017年6月13日 下午5:07:11
     */
    protected V fetchDataFromCache(K key) throws ExecutionException {
        return getCache().get(key);
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110

猜你喜欢

转载自my.oschina.net/newchaos/blog/1641741
今日推荐