[手mo手]-Springboot集成Guava cache 學不會你輸一包辣條給我

概述簡介

  1. 緩存是平常開發中常常應用到的一種技術手段,合理的利用緩存能夠極大的改善應用程序的性能。
  2. Guava官方對Cache的描述鏈接
  3. 緩存在各類各樣的用例中很是有用。例如,當計算或檢索值很昂貴時,您應該考慮使用緩存,而且不止一次須要它在某個輸入上的值。
  4. 緩存ConcurrentMap要小,但不徹底相同。最根本的區別在於一個ConcurrentMap堅持全部添加到它直到他們明確地刪除元素。
  5. 另外一方面,緩存通常配置爲自動退出的條目,以限制其內存佔用。
  6. 在某些狀況下,一個LoadingCache能夠即便不驅逐的條目是有用的,由於它的自動緩存加載。

適用性

  1. 你願意花一些內存來提升速度。You are willing to spend some memory to improve speed.
  2. 您但願Key有時會不止一次被查詢。You expect that keys will sometimes get queried more than once.
  3. 你的緩存不須要存儲更多的數據比什麼都適合在。(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.
  4. 它們不將數據存儲在文件中,也不存儲在外部服務器上。若是這樣作不適合您的須要,考慮一個工具像memcached,redis等。

基於引用的回收

(Reference-based Eviction)強(strong)、軟(soft)、弱(weak)、虛(phantom)引用-參考: 經過使用弱引用的鍵、或弱引用的值、或軟引用的值GuavaCache能夠把緩存設置爲容許垃圾回收:java

  1. CacheBuilder.weakKeys():使用弱引用存儲鍵。當鍵沒有其它(強或軟)引用時,緩存項能夠被垃圾回收。由於垃圾回收僅依賴恆等式(==),使用弱引用鍵的緩存用==而不是equals比較鍵。
  2. CacheBuilder.weakValues():使用弱引用存儲值。當值沒有其它(強或軟)引用時,緩存項能夠被垃圾回收。由於垃圾回收僅依賴恆等式(==),使用弱引用值的緩存用==而不是equals比較值。
  3. CacheBuilder.softValues():使用軟引用存儲值。軟引用只有在響應內存須要時,才按照全局最近最少使用的順序回收。考慮到使用軟引用的性能影響,咱們一般建議使用更有性能預測性的緩存大小限定(見上文,基於容量回收)。使用軟引用值的緩存一樣用==而不是equals比較值。

緩存加載方式

Guava cache 是利用CacheBuilder類用builder模式構造出兩種不一樣的cache加載方式CacheLoader,Callable,共同邏輯都是根據key是加載value。不一樣的地方在於CacheLoader的定義比較寬泛,是針對整個cache定義的,能夠認爲是統一的根據key值load value的方法,而Callable的方式較爲靈活,容許你在get的時候指定load方法。看如下代碼:git

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中數據的移除分爲被動移除和主動移除兩種。github

  • 被動移除數據的方式,guava默認提供了三種方式:
  1. 基於大小的移除: 看字面意思就知道就是按照緩存的大小來移除,若是即將到達指定的大小,那就會把不經常使用的鍵值對從cache中移除。

定義的方式通常爲 CacheBuilder.maximumSize(long),還有一種一種能夠算權重的方法,我的認爲實際使用中不太用到。就這個經常使用的來看有幾個注意點,web

  • 其一,這個size指的是cache中的條目數,不是內存大小或是其餘;
  • 其二,並非徹底到了指定的size系統纔開始移除不經常使用的數據的,而是接近這個size的時候系統就會開始作移除的動做;
  • 其三,若是一個鍵值對已經從緩存中被移除了,你再次請求訪問的時候,若是cachebuild是使用cacheloader方式的,那依然仍是會從cacheloader中再取一次值,若是這樣尚未,就會拋出異常
  1. 基於時間的移除: guava提供了兩個基於時間移除的方法
  • expireAfterAccess(long, TimeUnit) 這個方法是根據某個鍵值對最後一次訪問以後多少時間後移除
  • expireAfterWrite(long, TimeUnit) 這個方法是根據某個鍵值對被建立或值被替換後多少時間移除
  1. 基於引用的移除:   這種移除方式主要是基於java的垃圾回收機制,根據鍵或者值的引用關係決定移除
  • 主動移除數據方式,主動移除有三種方法:
  • 單獨移除用 Cache.invalidate(key)
  • 批量移除用 Cache.invalidateAll(keys)
  • 移除全部用 Cache.invalidateAll()

若是須要在移除數據的時候有所動做還能夠定義Removal Listener,可是有點須要注意的是默認Removal Listener中的行爲是和移除動做同步執行的,若是須要改爲異步形式,能夠考慮使用RemovalListeners.asynchronous(RemovalListener, Executor)redis

實戰演練

  1. maven依賴
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>23.0</version>
 </dependency>
複製代碼
  1. 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;
   }
}
複製代碼
  1. ILocalCache 緩存獲取調用接口 (用接口方式 類業務操做)
public interface ILocalCache <K, V> {

    /**
     * 從緩存中獲取數據
     * @param key
     * @return value
     */
    public V get(K key);
}
複製代碼
  1. 緩存獲取的實現方法 緩存實例
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);
    }
}
複製代碼

至此,以上就完成了,簡單緩存搭建,就可使用了. 其原理就是就是先從緩存中查詢,沒有就去數據庫中查詢放入緩存,再去維護緩存,基於你設置的屬性,只需集成緩存實現接口就能夠擴展緩存了............上面就是舉個栗子spring

緩存管理

  1. 再來編寫緩存管理,進行緩存的管理 這裏是統一的緩存管理 能夠返回到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;
    }
}
複製代碼
  1. 緩存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緩存,在後臺中咱們能夠重啓和清除緩存,管理每個緩存和查看緩存的統計信息,常常用於緩存一些不常常改變的數據 寫的簡陋,歡迎你們抨擊~ 下面是一個後臺頁面展現:數據庫

相關文章
相關標籤/搜索