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

錦成同學發表於2019-07-25

概述簡介

  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可以把快取設定為允許垃圾回收:

  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方法。看以下程式碼:

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預設提供了三種方式:
  1. 基於大小的移除: 看字面意思就知道就是按照快取的大小來移除,如果即將到達指定的大小,那就會把不常用的鍵值對從cache中移除。

定義的方式一般為 CacheBuilder.maximumSize(long),還有一種一種可以算權重的方法,個人認為實際使用中不太用到。就這個常用的來看有幾個注意點,

  • 其一,這個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)

實戰演練

  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);
    }
}
複製程式碼

至此,以上就完成了,簡單快取搭建,就可以使用了. 其原理就是就是先從快取中查詢,沒有就去資料庫中查詢放入快取,再去維護快取,基於你設定的屬性,只需整合快取實現介面就可以擴充套件快取了............上面就是舉個例子

快取管理

  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快取,在後臺中我們可以重啟和清除快取,管理每一個快取和檢視快取的統計資訊,經常用於快取一些不經常改變的資料 寫的簡陋,歡迎大家抨擊~ 下面是一個後臺頁面展示:

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

  

相關文章