转载

手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可以把缓存设置为允许垃圾回收:

  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 学不会你输一包辣条给我

原文  https://juejin.im/post/5d3561bf518825165444687f
正文到此结束
Loading...