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

本文由 简悦 SimpRead 转码, 原文地址 blog.csdn.net

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

  • CacheLoader
  • Callable callback

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

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

1. 简单使用:定时过期

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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();
}

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

如果用其他的缓存方式,如 redis,我们知道上面这种 “如果有缓存则返回;否则运算、缓存、然后返回” 的缓存模式是有很大弊端的。当高并发条件下同时进行 get 操作,而此时缓存值已过期时,会导致大量线程都调用生成缓存值的方法,比如从数据库读取。这时候就容易造成大量请求同时查询数据库中该条记录,也就是 “缓存击穿”。(之前说成“穿透” 和“雪崩”,属于概念理解错误,感谢评论区指正) 而 Guava cache 则对此种情况有一定控制。当大量线程用相同的 key 获取缓存值时,只会有一个线程进入 load 方法,而其他线程则等待,直到缓存值被生成。这样也就避免了缓存击穿的危险。

2. 进阶使用:定时刷新

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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();
}

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

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

3. 进阶使用:异步刷新

如 2 中的使用方法,解决了同一个 key 的缓存过期时会让多个线程阻塞的问题,只会让用来执行刷新缓存操作的一个用户线程会被阻塞。由此可以想到另一个问题,当缓存的 key 很多时,高并发条件下大量线程同时获取不同 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
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();
}

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

TIPS

  • 可以看到防缓存击穿和防用户线程阻塞都是依靠返回旧值来完成的。所以如果没有旧值,同样会全部阻塞,因此应视情况尽量在系统启动时将缓存内容加载到内存中。
  • 在刷新缓存时,如果 generateValueByKey 方法出现异常或者返回了 null,此时旧值不会更新。
  • 题外话:在使用内存缓存时,切记拿到缓存值之后不要在业务代码中对缓存直接做修改,因为此时拿到的对象引用是指向缓存真正的内容的。如果需要直接在该对象上进行修改,则在获取到缓存值后拷贝一份副本,然后传递该副本,进行修改操作。(我曾经就犯过这个低级错误 - -!)

4. 简单抽象封装

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

  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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
/**
 * @description: 利用guava实现的内存缓存。缓存加载之后永不过期,后台线程定时刷新缓存值。刷新失败时将继续返回旧缓存。
 * 					在调用getValue之前,需要设置 refreshDuration, refreshTimeunit, maxSize 三个参数
 * 					后台刷新线程池为该系统中所有子类共享,大小为20.
 * @author: luozhuo
 * @date: 2017年6月21日 上午10:03:45 
 * @version: V1.0.0
 * @param <K>
 * @param <V>
 */
public abstract class BaseGuavaCache <K, V> {
    
    private Logger logger = LoggerFactory.getLogger(getClass()); 
	
	// 缓存自动刷新周期
	protected int refreshDuration = 10;
	// 缓存刷新周期时间格式
	protected TimeUnit refreshTimeunit = TimeUnit.MINUTES;
	// 缓存过期时间(可选择)
	protected int expireDuration = -1;
	// 缓存刷新周期时间格式
	protected TimeUnit expireTimeunit = TimeUnit.HOURS;
	// 缓存最大容量
	protected int maxSize = 4;
	// 数据刷新线程池
	protected static ListeningExecutorService refreshPool = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(20));
	
	private LoadingCache<K, V> cache = null;
	
	/**
	 * 用于初始化缓存值(某些场景下使用,例如系统启动检测缓存加载是否征程)
	 */
	public abstract void loadValueWhenStarted();
	
	/**
	 * @description: 定义缓存值的计算方法
	 * @description: 新值计算失败时抛出异常,get操作时将继续返回旧的缓存
	 * @param key
	 * @author: luozhuo
	 * @throws Exception 
	 * @date: 2017年6月14日 下午7:11:10
	 */
	protected abstract V getValueWhenExpired(K key) throws Exception;
	
	/**
     * @description: 从cache中拿出数据操作
     * @param key
     * @author: luozhuo
	 * @throws Exception 
     * @date: 2017年6月13日 下午5:07:11
     */
    public V getValue(K key) throws Exception {
    	try {
			return getCache().get(key);
		} catch (Exception e) {
			logger.error("从内存缓存中获取内容时发生异常,key: " + key, e);
			throw e;
		}
    }
	
    public V getValueOrDefault(K key, V defaultValue) {
    	try {
			return getCache().get(key);
		} catch (Exception e) {
			logger.error("从内存缓存中获取内容时发生异常,key: " + key, e);
			return defaultValue;
		}
    }
    
	/**
     * 设置基本属性
     */
    public BaseGuavaCache<K, V> setRefreshDuration( int refreshDuration ){
        this.refreshDuration = refreshDuration;
        return this;
    }
    
    public BaseGuavaCache<K, V> setRefreshTimeUnit(TimeUnit refreshTimeunit){
        this.refreshTimeunit = refreshTimeunit;
        return this;
    }
    
    public BaseGuavaCache<K, V> setExpireDuration( int expireDuration ){
        this.expireDuration = expireDuration;
        return this;
    }
    
    public BaseGuavaCache<K, V> setExpireTimeUnit(TimeUnit expireTimeunit){
        this.expireTimeunit = expireTimeunit;
        return this;
    }
    
    public BaseGuavaCache<K, V> setMaxSize( int maxSize ){
        this.maxSize = maxSize;
        return this;
    }

    public void clearAll(){
    	this.getCache().invalidateAll();
	}
	
	/**
     * @description: 获取cache实例
     * @author: luozhuo
     * @date: 2017年6月13日 下午2:50:11
     */
    private LoadingCache<K, V> getCache() {
        if(cache == null){
            synchronized (this) {
                if(cache == null){
                	CacheBuilder<Object, Object> cacheBuilder = CacheBuilder.newBuilder()
                            .maximumSize(maxSize);
                            
                    if(refreshDuration > 0) {
                    	cacheBuilder = cacheBuilder.refreshAfterWrite(refreshDuration, refreshTimeunit);
                    }
                	if(expireDuration > 0) {
                		cacheBuilder = cacheBuilder.expireAfterWrite(expireDuration, expireTimeunit);
                	}
                	
                    cache = cacheBuilder.build(new CacheLoader<K, V>() {
                            @Override
                            public V load(K key) throws Exception {
                                return getValueWhenExpired(key);
                            }
                            
                            @Override
                            public ListenableFuture<V> reload(final K key,
                                    V oldValue) throws Exception {
                                return refreshPool.submit(new Callable<V>() {
                                    public V call() throws Exception {
                                        return getValueWhenExpired(key);
                                    }
                                });
                            }
                        } );
                }
            }
        }
        return cache;
    }

    @Override
    public String toString() {
        return "GuavaCache";
    }
}