概述簡介
- 快取是日常開發中經常應用到的一種技術手段,合理的利用快取可以極大的改善應用程式的效能。
- Guava官方對Cache的描述連線
- 快取在各種各樣的用例中非常有用。例如,當計算或檢索值很昂貴時,您應該考慮使用快取,並且不止一次需要它在某個輸入上的值。
- 快取ConcurrentMap要小,但不完全相同。最根本的區別在於一個ConcurrentMap堅持所有新增到它直到他們明確地刪除元素。
- 另一方面,快取一般配置為自動退出的條目,以限制其記憶體佔用。
- 在某些情況下,一個LoadingCache可以即使不驅逐的條目是有用的,因為它的自動快取載入。
適用性
- 你願意花一些記憶體來提高速度。You are willing to spend some memory to improve speed.
- 您希望Key有時會不止一次被查詢。You expect that keys will sometimes get queried more than once.
- 你的快取不需要儲存更多的資料比什麼都適合在。(Guava快取是本地應用程式的一次執行)。Your cache will not need to store more data than what would fit inRAM. (Guava caches are local to a single run of your application.
- 它們不將資料儲存在檔案中,也不儲存在外部伺服器上。如果這樣做不適合您的需要,考慮一個工具像memcached,redis等。
基於引用的回收
(Reference-based Eviction)強(strong)、軟(soft)、弱(weak)、虛(phantom)引用-參考: 通過使用弱引用的鍵、或弱引用的值、或軟引用的值GuavaCache可以把快取設定為允許垃圾回收:
- CacheBuilder.weakKeys():使用弱引用儲存鍵。當鍵沒有其它(強或軟)引用時,快取項可以被垃圾回收。因為垃圾回收僅依賴恆等式(==),使用弱引用鍵的快取用==而不是equals比較鍵。
- CacheBuilder.weakValues():使用弱引用儲存值。當值沒有其它(強或軟)引用時,快取項可以被垃圾回收。因為垃圾回收僅依賴恆等式(==),使用弱引用值的快取用==而不是equals比較值。
- CacheBuilder.softValues():使用軟引用儲存值。軟引用只有在響應記憶體需要時,才按照全域性最近最少使用的順序回收。考慮到使用軟引用的效能影響,我們通常建議使用更有效能預測性的快取大小限定(見上文,基於容量回收)。使用軟引用值的快取同樣用==而不是equals比較值。
快取載入方式
Guava cache 是利用CacheBuilder類用builder模式構造出兩種不同的cache載入方式CacheLoader,Callable,共同邏輯都是根據key是載入value。不同的地方在於CacheLoader的定義比較寬泛,是針對整個cache定義的,可以認為是統一的根據key值load value的方法,而Callable的方式較為靈活,允許你在get的時候指定load方法。看以下程式碼:
Cache<String,Object> cache = CacheBuilder.newBuilder()
.expireAfterWrite(10, TimeUnit.SECONDS).maximumSize(500).build();
cache.get("key", new Callable<Object>() { //Callable 載入
@Override
public Object call() throws Exception {
return "value";
}
});
LoadingCache<String, Object> loadingCache = CacheBuilder.newBuilder()
.expireAfterAccess(30, TimeUnit.SECONDS).maximumSize(5)
.build(new CacheLoader<String, Object>() {
@Override
public Object load(String key) throws Exception {
return "value";
}
});
複製程式碼
快取移除
guava做cache時候資料的移除方式,在guava中資料的移除分為被動移除和主動移除兩種。
- 被動移除資料的方式,guava預設提供了三種方式:
- 基於大小的移除: 看字面意思就知道就是按照快取的大小來移除,如果即將到達指定的大小,那就會把不常用的鍵值對從cache中移除。
定義的方式一般為 CacheBuilder.maximumSize(long),還有一種一種可以算權重的方法,個人認為實際使用中不太用到。就這個常用的來看有幾個注意點,
- 其一,這個size指的是cache中的條目數,不是記憶體大小或是其他;
- 其二,並不是完全到了指定的size系統才開始移除不常用的資料的,而是接近這個size的時候系統就會開始做移除的動作;
- 其三,如果一個鍵值對已經從快取中被移除了,你再次請求訪問的時候,如果cachebuild是使用cacheloader方式的,那依然還是會從cacheloader中再取一次值,如果這樣還沒有,就會丟擲異常
- 基於時間的移除: guava提供了兩個基於時間移除的方法
- expireAfterAccess(long, TimeUnit) 這個方法是根據某個鍵值對最後一次訪問之後多少時間後移除
- expireAfterWrite(long, TimeUnit) 這個方法是根據某個鍵值對被建立或值被替換後多少時間移除
- 基於引用的移除: 這種移除方式主要是基於java的垃圾回收機制,根據鍵或者值的引用關係決定移除
- 主動移除資料方式,主動移除有三種方法:
- 單獨移除用 Cache.invalidate(key)
- 批量移除用 Cache.invalidateAll(keys)
- 移除所有用 Cache.invalidateAll()
如果需要在移除資料的時候有所動作還可以定義Removal Listener,但是有點需要注意的是預設Removal Listener中的行為是和移除動作同步執行的,如果需要改成非同步形式,可以考慮使用RemovalListeners.asynchronous(RemovalListener, Executor)
實戰演練
- maven依賴
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>
複製程式碼
- GuavaAbstractLoadingCache 快取載入方式和基本屬性使用基類(我用的是CacheBuilder)
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Date;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
/**
* @ClassName: GuavaAbstractLoadingCache
* @author LiJing
* @date 2019/07/02 11:09
*
*/
public abstract class GuavaAbstractLoadingCache <K, V> {
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
//用於初始化cache的引數及其預設值
private int maximumSize = 1000; //最大快取條數,子類在構造方法中呼叫setMaximumSize(int size)來更改
private int expireAfterWriteDuration = 60; //資料存在時長,子類在構造方法中呼叫setExpireAfterWriteDuration(int duration)來更改
private TimeUnit timeUnit = TimeUnit.MINUTES; //時間單位(分鐘)
private Date resetTime; //Cache初始化或被重置的時間
private long highestSize=0; //歷史最高記錄數
private Date highestTime; //創造歷史記錄的時間
private LoadingCache<K, V> cache;
/**
* 通過呼叫getCache().get(key)來獲取資料
* @return cache
*/
public LoadingCache<K, V> getCache() {
if(cache == null){ //使用雙重校驗鎖保證只有一個cache例項
synchronized (this) {
if(cache == null){
cache = CacheBuilder.newBuilder().maximumSize(maximumSize) //快取資料的最大條目,也可以使用.maximumWeight(weight)代替
.expireAfterWrite(expireAfterWriteDuration, timeUnit) //資料被建立多久後被移除
.recordStats() //啟用統計
.build(new CacheLoader<K, V>() {
@Override
public V load(K key) throws Exception {
return fetchData(key);
}
});
this.resetTime = new Date();
this.highestTime = new Date();
logger.debug("本地快取{}初始化成功", this.getClass().getSimpleName());
}
}
}
return cache;
}
/**
* 根據key從資料庫或其他資料來源中獲取一個value,並被自動儲存到快取中。
* @param key
* @return value,連同key一起被載入到快取中的。
*/
protected abstract V fetchData(K key) throws Exception;
/**
* 從快取中獲取資料(第一次自動呼叫fetchData從外部獲取資料),並處理異常
* @param key
* @return Value
* @throws ExecutionException
*/
protected V getValue(K key) throws ExecutionException {
V result = getCache().get(key);
if(getCache().size() > highestSize){
highestSize = getCache().size();
highestTime = new Date();
}
return result;
}
public long getHighestSize() {
return highestSize;
}
public Date getHighestTime() {
return highestTime;
}
public Date getResetTime() {
return resetTime;
}
public void setResetTime(Date resetTime) {
this.resetTime = resetTime;
}
public int getMaximumSize() {
return maximumSize;
}
public int getExpireAfterWriteDuration() {
return expireAfterWriteDuration;
}
public TimeUnit getTimeUnit() {
return timeUnit;
}
/**
* 設定最大快取條數
* @param maximumSize
*/
public void setMaximumSize(int maximumSize) {
this.maximumSize = maximumSize;
}
/**
* 設定資料存在時長(分鐘)
* @param expireAfterWriteDuration
*/
public void setExpireAfterWriteDuration(int expireAfterWriteDuration) {
this.expireAfterWriteDuration = expireAfterWriteDuration;
}
}
複製程式碼
- ILocalCache 快取獲取呼叫介面 (用介面方式 類業務操作)
public interface ILocalCache <K, V> {
/**
* 從快取中獲取資料
* @param key
* @return value
*/
public V get(K key);
}
複製程式碼
- 快取獲取的實現方法 快取例項
import com.cn.xxx.xxx.bean.area.Area;
import com.cn.xxx.xxx.mapper.area.AreaMapper;
import com.cn.xxx.xxx.service.area.AreaService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author LiJing
* @ClassName: LCAreaIdToArea
* @date 2019/07/02 11:12
*/
@Component
public class AreaCache extends GuavaAbstractLoadingCache<Long, Area> implements ILocalCache<Long, Area> {
@Autowired
private AreaService areaService;
//由Spring來維持單例模式
private AreaCache() {
setMaximumSize(4000); //最大快取條數
}
@Override
public Area get(Long key) {
try {
Area value = getValue(key);
return value;
} catch (Exception e) {
logger.error("無法根據baseDataKey={}獲取Area,可能是資料庫中無該記錄。", key, e);
return null;
}
}
/**
* 從資料庫中獲取資料
*/
@Override
protected Area fetchData(Long key) {
logger.debug("測試:正在從資料庫中獲取area,area id={}", key);
return areaService.getAreaInfo(key);
}
}
複製程式碼
至此,以上就完成了,簡單快取搭建,就可以使用了. 其原理就是就是先從快取中查詢,沒有就去資料庫中查詢放入快取,再去維護快取,基於你設定的屬性,只需整合快取實現介面就可以擴充套件快取了............上面就是舉個例子
快取管理
- 再來編寫快取管理,進行快取的管理 這裡是統一的快取管理 可以返回到Controller去統一管理
import com.cn.xxx.common.core.page.PageParams;
import com.cn.xxx.common.core.page.PageResult;
import com.cn.xxx.common.core.util.SpringContextUtil;
import com.google.common.cache.CacheStats;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
import java.util.concurrent.ConcurrentMap;
/**
* @ClassName: GuavaCacheManager
* @author LiJing
* @date 2019/07/02 11:17
*
*/
public class GuavaCacheManager {
//儲存一個Map: cacheName -> cache Object,以便根據cacheName獲取Guava cache物件
private static Map<String, ? extends GuavaAbstractLoadingCache<Object, Object>> cacheNameToObjectMap = null;
/**
* 獲取所有GuavaAbstractLoadingCache子類的例項,即所有的Guava Cache物件
* @return
*/
@SuppressWarnings("unchecked")
private static Map<String, ? extends GuavaAbstractLoadingCache<Object, Object>> getCacheMap(){
if(cacheNameToObjectMap==null){
cacheNameToObjectMap = (Map<String, ? extends GuavaAbstractLoadingCache<Object, Object>>) SpringContextUtil.getBeanOfType(GuavaAbstractLoadingCache.class);
}
return cacheNameToObjectMap;
}
/**
* 根據cacheName獲取cache物件
* @param cacheName
* @return
*/
private static GuavaAbstractLoadingCache<Object, Object> getCacheByName(String cacheName){
return (GuavaAbstractLoadingCache<Object, Object>) getCacheMap().get(cacheName);
}
/**
* 獲取所有快取的名字(即快取實現類的名稱)
* @return
*/
public static Set<String> getCacheNames() {
return getCacheMap().keySet();
}
/**
* 返回所有快取的統計資料
* @return List<Map<統計指標,統計資料>>
*/
public static ArrayList<Map<String, Object>> getAllCacheStats() {
Map<String, ? extends Object> cacheMap = getCacheMap();
List<String> cacheNameList = new ArrayList<>(cacheMap.keySet());
Collections.sort(cacheNameList);//按照字母排序
//遍歷所有快取,獲取統計資料
ArrayList<Map<String, Object>> list = new ArrayList<>();
for(String cacheName : cacheNameList){
list.add(getCacheStatsToMap(cacheName));
}
return list;
}
/**
* 返回一個快取的統計資料
* @param cacheName
* @return Map<統計指標,統計資料>
*/
private static Map<String, Object> getCacheStatsToMap(String cacheName) {
Map<String, Object> map = new LinkedHashMap<>();
GuavaAbstractLoadingCache<Object, Object> cache = getCacheByName(cacheName);
CacheStats cs = cache.getCache().stats();
NumberFormat percent = NumberFormat.getPercentInstance(); // 建立百分比格式化用
percent.setMaximumFractionDigits(1); // 百分比小數點後的位數
map.put("cacheName", cacheName);//Cache名稱
map.put("size", cache.getCache().size());//當前資料量
map.put("maximumSize", cache.getMaximumSize());//最大快取條數
map.put("survivalDuration", cache.getExpireAfterWriteDuration());//過期時間
map.put("hitCount", cs.hitCount());//命中次數
map.put("hitRate", percent.format(cs.hitRate()));//命中比例
map.put("missRate", percent.format(cs.missRate()));//讀庫比例
map.put("loadSuccessCount", cs.loadSuccessCount());//成功載入數
map.put("loadExceptionCount", cs.loadExceptionCount());//成功載入數
map.put("totalLoadTime", cs.totalLoadTime()/1000000); //總載入毫秒ms
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
if(cache.getResetTime()!=null){
map.put("resetTime", df.format(cache.getResetTime()));//重置時間
LocalDateTime localDateTime = LocalDateTime.ofInstant(cache.getResetTime().toInstant(), ZoneId.systemDefault()).plusMinutes(cache.getTimeUnit().toMinutes(cache.getExpireAfterWriteDuration()));
map.put("survivalTime", df.format(Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant())));//失效時間
}
map.put("highestSize", cache.getHighestSize());//歷史最高資料量
if(cache.getHighestTime()!=null){
map.put("highestTime", df.format(cache.getHighestTime()));//最高資料量時間
}
return map;
}
/**
* 根據cacheName清空快取資料
* @param cacheName
*/
public static void resetCache(String cacheName){
GuavaAbstractLoadingCache<Object, Object> cache = getCacheByName(cacheName);
cache.getCache().invalidateAll();
cache.setResetTime(new Date());
}
/**
* 分頁獲得快取中的資料
* @param pageParams
* @return
*/
public static PageResult<Object> queryDataByPage(PageParams<Object> pageParams) {
PageResult<Object> data = new PageResult<>(pageParams);
GuavaAbstractLoadingCache<Object, Object> cache = getCacheByName((String) pageParams.getParams().get("cacheName"));
ConcurrentMap<Object, Object> cacheMap = cache.getCache().asMap();
data.setTotalRecord(cacheMap.size());
data.setTotalPage((cacheMap.size()-1)/pageParams.getPageSize()+1);
//遍歷
Iterator<Map.Entry<Object, Object>> entries = cacheMap.entrySet().iterator();
int startPos = pageParams.getStartPos()-1;
int endPos = pageParams.getEndPos()-1;
int i=0;
Map<Object, Object> resultMap = new LinkedHashMap<>();
while (entries.hasNext()) {
Map.Entry<Object, Object> entry = entries.next();
if(i>endPos){
break;
}
if(i>=startPos){
resultMap.put(entry.getKey(), entry.getValue());
}
i++;
}
List<Object> resultList = new ArrayList<>();
resultList.add(resultMap);
data.setResults(resultList);
return data;
}
}
複製程式碼
- 快取service:
import com.alibaba.dubbo.config.annotation.Service;
import com.cn.xxx.xxx.cache.GuavaCacheManager;
import com.cn.xxx.xxx.service.cache.CacheService;
import java.util.ArrayList;
import java.util.Map;
/**
* @ClassName: CacheServiceImpl
* @author lijing
* @date 2019.07.06 下午 5:29
*
*/
@Service(version = "1.0.0")
public class CacheServiceImpl implements CacheService {
@Override
public ArrayList<Map<String, Object>> getAllCacheStats() {
return GuavaCacheManager.getAllCacheStats();
}
@Override
public void resetCache(String cacheName) {
GuavaCacheManager.resetCache(cacheName);
}
}
複製程式碼
import com.alibaba.dubbo.config.annotation.Reference;
import com.cn.xxx.common.core.page.JsonResult;
import com.cn.xxx.xxx.service.cache.CacheService;
import com.github.pagehelper.PageInfo;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
/**
* @ClassName: CacheAdminController
* @author LiJing
* @date 2018/07/06 10:10
*
*/
@Controller
@RequestMapping("/cache")
public class CacheAdminController {
@Reference(version = "1.0.0")
private CacheService cacheService;
@GetMapping("")
@RequiresPermissions("cache:view")
public String index() {
return "admin/system/cache/cacheList";
}
@PostMapping("/findPage")
@ResponseBody
@RequiresPermissions("cache:view")
public PageInfo findPage() {
return new PageInfo<>(cacheService.getAllCacheStats());
}
/**
* 清空快取資料、並返回清空後的統計資訊
* @param cacheName
* @return
*/
@RequestMapping(value = "/reset", method = RequestMethod.POST)
@ResponseBody
@RequiresPermissions("cache:reset")
public JsonResult cacheReset(String cacheName) {
JsonResult jsonResult = new JsonResult();
cacheService.resetCache(cacheName);
jsonResult.setMessage("已經成功重置了" + cacheName + "!");
return jsonResult;
}
/**
* 查詢cache統計資訊
* @param cacheName
* @return cache統計資訊
*/
/*@RequestMapping(value = "/stats", method = RequestMethod.POST)
@ResponseBody
public JsonResult cacheStats(String cacheName) {
JsonResult jsonResult = new JsonResult();
//暫時只支援獲取全部
switch (cacheName) {
case "*":
jsonResult.setData(GuavaCacheManager.getAllCacheStats());
jsonResult.setMessage("成功獲取了所有的cache!");
break;
default:
break;
}
return jsonResult;
}*/
/**
* 返回所有的本地快取統計資訊
* @return
*/
/*@RequestMapping(value = "/stats/all", method = RequestMethod.POST)
@ResponseBody
public JsonResult cacheStatsAll() {
return cacheStats("*");
}*/
/**
* 分頁查詢資料詳情
* @param params
* @return
*/
/*@RequestMapping(value = "/queryDataByPage", method = RequestMethod.POST)
@ResponseBody
public PageResult<Object> queryDataByPage(@RequestParam Map<String, String> params){
int pageSize = Integer.valueOf(params.get("pageSize"));
int pageNo = Integer.valueOf(params.get("pageNo"));
String cacheName = params.get("cacheName");
PageParams<Object> page = new PageParams<>();
page.setPageSize(pageSize);
page.setPageNo(pageNo);
Map<String, Object> param = new HashMap<>();
param.put("cacheName", cacheName);
page.setParams(param);
return GuavaCacheManager.queryDataByPage(page);
}*/
}
複製程式碼
結束語
以上就是gauva快取,在後臺中我們可以重啟和清除快取,管理每一個快取和檢視快取的統計資訊,經常用於快取一些不經常改變的資料 寫的簡陋,歡迎大家抨擊~ 下面是一個後臺頁面展示: