Guava cache本地缓存简介、刷新机制简单示例及封装

简介

Guava Cache与ConcurrentMap很相似,但也不完全一样。最基本的区别是ConcurrentMap会一直保存所有添加的元素,直到显式地移除。相对地,Guava Cache为了限制内存占用,通常都设定为自动回收元素。在某些场景下,尽管LoadingCache 不回收元素,它也是很有用的,因为它会自动加载缓存。

通常来说,Guava Cache适用于:

  • 你愿意消耗一些内存空间来提升速度。
  • 你预料到某些键会被查询一次以上。
  • 缓存中存放的数据总量不会超出内存容量。(Guava Cache是单个应用运行时的本地缓存。它不把数据存放到文件或外部服务器。如果这不符合你的需求,请尝试Memcached这类工具)

如果你的场景符合上述的每一条,Guava Cache就适合你。

如同范例代码展示的一样,Cache实例通过CacheBuilder生成器模式获取,但是自定义你的缓存才是最有趣的部分。

:如果你不需要Cache中的特性,使用ConcurrentHashMap有更好的内存效率——但Cache的大多数特性都很难基于旧有的ConcurrentMap复制,甚至根本不可能做到。

Guava Cache的使用示例

使用缓存时,最常遇到的场景需要就是:

"获取缓存-如果没有-则计算"[get-if-absent-compute]的原子语义.

具体含义:

  1. 从缓存中取。
  2. 缓存中存在该数据,直接返回;
  3. 缓存中不存在该数据,从数据源中取。
  4. 数据源中存在该数据,放入缓存,并返回;
  5. 数据源中不存在该数据,返回空。

刷新机制

三种基于时间的清理或刷新缓存数据的方式:

expireAfterAccess: 当缓存项在指定的时间段内没有被读或写就会被回收。

expireAfterWrite:当缓存项在指定的时间段内没有更新就会被回收。

refreshAfterWrite:当缓存项上一次更新操作之后的多久会被刷新。

一、定时过期

LoadingCache<String, Object> caches = CacheBuilder.newBuilder()
                .maximumSize(100)
                .expireAfterWrite(30, 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方法,进行缓存值的计算。 

二、定时刷新

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方法的调用,所以如果一直没有用户尝试获取该缓存值,则该缓存也并不会刷新。

三、异步刷新

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();
}

当缓存的key很多时,高并发条件下大量线程同时获取不同key对应的缓存,此时依然会造成大量线程阻塞,并且给数据库带来很大压力。这个问题的解决办法就是将刷新缓存值的任务交给后台线程,所有的用户请求线程均返回旧的缓存值,这样就不会有用户线程被阻塞了。

{

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

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

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

}

封装

封装后的的缓存抽象类实现了刷新时间、时间单位、定时刷新及初始化缓存值等方法。实现类只需要设置自己的缓存时间等信息,实现preload()方法加载缓存数据,实现getCacheValue()方法将缓存数据添加到缓存中。

抽象类代码如下:

package com.lenchy.lms.util.cache;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListenableFutureTask;
import com.homolo.datamodel.manager.EntityManager;
import com.homolo.framework.setup.Initializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.task.TaskExecutor;

import javax.validation.constraints.NotNull;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * 数据缓存抽象类
 *
 * @param <T> 缓存值的类型
 */
public abstract class BaseDataCache<T> extends Initializer {
	private static final Logger LOGGER = LoggerFactory.getLogger(BaseDataCache.class);
	private static final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(10);
	@Autowired
	private TaskExecutor taskExecutor;
	@Autowired
	protected EntityManager entityManager;

	private LoadingCache<String, T> cache;

	/**
	 * @return 时间
	 */
	protected abstract long duration();

	/**
	 * @return 时间单位
	 */
	protected abstract TimeUnit unit();

	/**
	 * 根据key生成缓存值的方法
	 *
	 * @param key key
	 * @return 缓存值
	 */
	protected abstract T getCacheValue(String key);

	/**
	 * 缓存到期是否自动刷新
	 */
	protected boolean refresh() {
		return false;
	}

	/**
	 * 预加载缓存数据,系统启动后执行
	 *
	 * @return Runnable 返回 null 表示不执行预加载
	 */
	protected Runnable preload() {
		return null;
	}

	/**
	 * 获取缓存值,请在初始化完成之后调用
	 *
	 * @param key key
	 * @return 缓存值,异常时返回 getCacheValue()
	 */
	public T get(String key) {
		try {
			return cache.get(key);
		} catch (ExecutionException e) {
			LOGGER.error("get cache error", e);
			return getCacheValue(key);
		}
	}

	@Override
	public void initialize() {
		long duration = duration();
		TimeUnit unit = unit();
		cache = CacheBuilder.newBuilder().refreshAfterWrite(duration(), unit())
				.build(new CacheLoader<String, T>() {
					@Override
					public T load(@NotNull String key) {
						return getCacheValue(key);
					}

					@Override
					public ListenableFuture<T> reload(final String key, T oldValue) throws Exception {
						ListenableFutureTask<T> task = ListenableFutureTask.create(new Callable<T>() {
							public T call() {
								return getCacheValue(key);
							}
						});
						taskExecutor.execute(task);
						return task;
					}
				});
		Runnable preload = preload();
		if (preload != null) {
			LOGGER.info("preload {}", this.getClass().getSimpleName());
			taskExecutor.execute(preload());
		}
		if (refresh()) {
			executorService.scheduleAtFixedRate(new Runnable() {
				@Override
				public void run() {
					for (String key : cache.asMap().keySet()) {
						cache.refresh(key);
					}
				}
			}, duration, duration, unit);
		}
	}

	@Override
	public int getPhase() {
		return 20000;
	}
}

简单实现类

@Component
public class Statistic4JusticeCache extends BaseDataCache<Statistic4JusticeCache.JusticeCache> {
	@Autowired
	private JusticeBureauManager justiceBureauManager;
	@Autowired
	protected NationalDataCountUtil nationalDataStatisticsUtil;
	@Autowired
	private JusticeBureauFilter justiceBureauFilter;

	@Override
	protected long duration() {
		return 1;
	}

	@Override
	protected TimeUnit unit() {
		return TimeUnit.DAYS;
	}

	@Override
	protected boolean refresh() {
		return true;
	}

	@Override
	protected Runnable preload() {
		return new Runnable() {
			@Override
			public void run() {
				List<String> cacheKwys = new ArrayList<>();
                cacheKwys.add( "first" ); 
                cacheKwys.add( "second" ); 
                cacheKwys.add( "third" ); 
				for (String key : cacheKwys) {
					get(key);
				}
			}
		};
	}

	@Override
	protected Statistic4JusticeCache.JusticeCache getCacheValue(String key) {
		JusticeCache cache = new JusticeCache();
		cache.setFirstList(managerGetList(key));
		cache.setSecondList(getZoneList(key));
		cache.setThirdMap(getLawyerModifymap(key));
		return cache;
	}

    public static class JusticeCache {
		private List firstList;
		private List secondList;
		private Map thirdMap;

		public List getFirstList() {
			return firstList;
		}

		public void setFirstList(List firstList) {
			this.firstList = firstList;
		}

		public List getSecondlist() {
			return secondList;
		}

		public void setSecondList(List secondList) {
			this.secondList = secondList;
		}

		public Map getThirdMap() {
			return thirdMap;
		}

		public void setThirdMap(Map thirdMap) {
			this.thirdMap = thirdMap;
		}
	}

    public List<String> getFirstList(){
        List first = new ArrayList<>();
        first.add("first1");
        first.add("first2");
        return first;
    }

    public List<String> getSecondlist(){
        List second = new ArrayList<>();
        first.add("second1");
        first.add("second2");
        return first;
    }

    public Map<String,String> getThirdMap(){
        Map<String, String> third = new HashMap<String, String>();
        third.put( "third1" , "third1" ); 
        third.put( "third2" , "third2" );
        return third; 
    }


}

有不足的地方希望大家指出。。。。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章