• Guava Cache相关


    官方:http://ifeve.com/google-guava-cachesexplained/

    理解:https://segmentfault.com/a/1190000007300118

    项目中用到cache的例子:

     1 public class TokenCache {
     2 
     3     //打印日志
     4     private static Logger logger = LoggerFactory.getLogger(TokenCache.class);
     5 
     6     public static final String TOKEN_PREFIX = "token_";
     7 
     8     //LRU算法
     9     //initialCapacity(1000)设置cache的初始大小为1000
    10     //maximumSize(10000)设置缓存个数为10000,当个数超过10000会利用LRU算法删除部分缓存
    11     //expireAfterAccess(12, TimeUnit.HOURS)设置cache中的数据在写入之后的存活时间为12小时,TimeUnit.HOURS表示12的单位是小时
    12     //key和value都是String类型
    13     private static LoadingCache<String, String> localCache = CacheBuilder.newBuilder().initialCapacity(1000).maximumSize(10000).expireAfterAccess(12, TimeUnit.HOURS).build(
    14             new CacheLoader<String, String>() {
    15                 //默认的数据加载实现,当调用get取值的时候,如果key没有对应的值,就调用这个方法进行加载
    16                 //当本地缓存命没有中时,调用load方法获取结果并将结果缓存
    17                 @Override
    18                 public String load(String s) throws Exception {
    19                     //因为key和value都是String类型,在下面调用getKey时返回的value值也是String类型,虽然有可能是null值但是也是string类型,所以这里返回"null"而不是null
    20                     return "null";
    21                 }
    22             }
    23     );
    24 
    25     public static void setKey(String key, String value) {
    26         localCache.put(key, value);
    27     }
    28 
    29     public static String getKey(String key) {
    30         String value = null;
    31         try {
    32             value = localCache.get(key);
    33             //这里用"null"字符串来进行判空
    34             if("null".equals(value)) {
    35                 return null;
    36             }
    37             return value;
    38         } catch (Exception e) {
    39             logger.error("localCache get error", e);
    40         }
    41         return null;
    42     }
    43 }
    View Code

    存入cache:

     //把token放入本地cache中,然后设置其有效期
                TokenCache.setKey(TokenCache.TOKEN_PREFIX + username, forgetToken);

    读出cache:

            //从cache中获取token,根据key拿到value值
            String token = TokenCache.getKey(TokenCache.TOKEN_PREFIX + username);
            //校验cache中的token是否有效
            if(StringUtils.isBlank(token)) {
                return ServerResponse.createByErrorMessage("token无效或者过期");
            }    

    使用解释:

    1 final static Cache<Integer, String> cache = CacheBuilder.newBuilder()  
    2         //设置cache的初始大小为10,要合理设置该值  
    3         .initialCapacity(10)  
    4         //设置并发数为5,即同一时间最多只能有5个线程往cache执行写入操作  
    5         .concurrencyLevel(5)  
    6         //设置cache中的数据在写入之后的存活时间为10秒  
    7         .expireAfterWrite(10, TimeUnit.SECONDS)  
    8         //构建cache实例  
    9         .build();  
    View Code

    据说GuavaCache的实现是基于ConcurrentHashMap的,因此上面的构造过程所调用的方法,通过查看其官方文档也能看到一些类似的原理。比如通过initialCapacity(5)定义初始值大小,要是定义太大就好浪费内存空间,要是太小,需要扩容的时候就会像map一样需要resize,这个过程会产生大量需要gc的对象,还有比如通过concurrencyLevel(5)来限制写入操作的并发数,这和ConcurrentHashMap的锁机制也是类似的(ConcurrentHashMap读不需要加锁,写入需要加锁,每个segment都有一个锁)。

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

      Guava Cache适用场景:

    • 你愿意消耗一部分内存来提升速度;
    • 你已经预料某些值会被多次调用;
    • 缓存数据不会超过内存总量;

     Guava Cache是一个全内存的本地缓存实现,它提供了线程安全的实现机制。

    接下来看看Cache提供哪些方法(只列了部分常用的):

     1 /** 
     2  * 该接口的实现被认为是线程安全的,即可在多线程中调用 
     3  * 通过被定义单例使用 
     4  */  
     5 public interface Cache<K, V> {  
     6   
     7   /** 
     8    * 通过key获取缓存中的value,若不存在直接返回null 
     9    */  
    10   V getIfPresent(Object key);  
    11   
    12   /** 
    13    * 通过key获取缓存中的value,若不存在就通过valueLoader来加载该value 
    14    * 整个过程为 "if cached, return; otherwise create, cache and return" 
    15    * 注意valueLoader要么返回非null值,要么抛出异常,绝对不能返回null 
    16    */  
    17   V get(K key, Callable<? extends V> valueLoader) throws ExecutionException;  
    18   
    19   /** 
    20    * 添加缓存,若key存在,就覆盖旧值 
    21    */  
    22   void put(K key, V value);  
    23   
    24   /** 
    25    * 删除该key关联的缓存 
    26    */  
    27   void invalidate(Object key);  
    28   
    29   /** 
    30    * 删除所有缓存 
    31    */  
    32   void invalidateAll();  
    33   
    34   /** 
    35    * 执行一些维护操作,包括清理缓存 
    36    */  
    37   void cleanUp();  
    38 }  
    View Code

    清除缓存的策略

    任何Cache的容量都是有限的,而缓存清除策略就是决定数据在什么时候应该被清理掉。GuavaCache提了以下几种清除策略:
     1.基于存活时间的清除(Timed Eviction)
    这应该是最常用的清除策略,在构建Cache实例的时候,CacheBuilder提供两种基于存活时间的构建方法:
    (1)expireAfterAccess(long, TimeUnit):缓存项在创建后,在给定时间内没有被读/写访问,则清除。
    (2)expireAfterWrite(long, TimeUnit):缓存项在创建后,在给定时间内没有被写访问(创建或覆盖),则清除。
    expireAfterWrite()方法有些类似于redis中的expire命令,但显然它只能设置所有缓存都具有相同的存活时间。若遇到一些缓存数据的存活时间为1分钟,一些为5分钟,那只能构建两个Cache实例了。
    2.基于容量的清除(size-based eviction)
    在构建Cache实例的时候,通过CacheBuilder.maximumSize(long)方法可以设置Cache的最大容量数,当缓存数量达到或接近该最大值时,Cache将清除掉那些最近最少使用的缓存。
    以上是这种方式是以缓存的“数量”作为容量的计算方式,还有另外一种基于“权重”的计算方式。比如每一项缓存所占据的内存空间大小都不一样,可以看作它们有不同的“权重”(weights)。你可以使用CacheBuilder.weigher(Weigher)指定一个权重函数,并且用CacheBuilder.maximumWeight(long)指定最大总重。
    3.显式清除
    任何时候,你都可以显式地清除缓存项,而不是等到它被回收,Cache接口提供了如下API:
    (1)个别清除:Cache.invalidate(key)
    (2)批量清除:Cache.invalidateAll(keys)
    (3)清除所有缓存项:Cache.invalidateAll()

    4.基于引用的清除(Reference-based Eviction)

    在构建Cache实例过程中,通过设置使用弱引用的键、或弱引用的值、或软引用的值,从而使JVM在GC时顺带实现缓存的清除,不过一般不轻易使用这个特性。
    (1)CacheBuilder.weakKeys():使用弱引用存储键
    (2)CacheBuilder.weakValues():使用弱引用存储值
    (3)CacheBuilder.softValues():使用软引用存储值

    清除什么时候发生?

    也许这个问题有点奇怪,如果设置的存活时间为一分钟,难道不是一分钟后这个key就会立即清除掉吗?我们来分析一下如果要实现这个功能,那Cache中就必须存在线程来进行周期性地检查、清除等工作,很多cache如redis、ehcache都是这样实现的。
    但在GuavaCache中,并不存在任何线程!它实现机制是在写操作时顺带做少量的维护工作(如清除),偶尔在读操作时做(如果写操作实在太少的话),也就是说在使用的是调用线程,参考如下示例:
     1 public class CacheService {  
     2     static Cache<Integer, String> cache = CacheBuilder.newBuilder()  
     3             .expireAfterWrite(5, TimeUnit.SECONDS)  
     4             .build();  
     5       
     6     public static void main(String[] args) throws Exception {  
     7         new Thread() { //monitor  
     8             public void run() {  
     9                 while(true) {  
    10                     SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");  
    11                     System.out.println(sdf.format(new Date()) +" size: "+cache.size());  
    12                     try {  
    13                         Thread.sleep(2000);  
    14                     } catch (InterruptedException e) {  
    15                     }  
    16                 }  
    17             };  
    18         }.start();  
    19         SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");  
    20         cache.put(1, "Hi");  
    21         System.out.println("write key:1 ,value:"+cache.getIfPresent(1));  
    22         Thread.sleep(10000);  
    23         // when write ,key:1 clear  
    24         cache.put(2, "bbb");  
    25         System.out.println("write key:2 ,value:"+cache.getIfPresent(2));  
    26         Thread.sleep(10000);  
    27         // when read other key ,key:2 do not clear  
    28         System.out.println(sdf.format(new Date())  
    29                 +" after write, key:1 ,value:"+cache.getIfPresent(1));  
    30         Thread.sleep(2000);  
    31         // when read same key ,key:2 clear  
    32         System.out.println(sdf.format(new Date())  
    33                 +" final, key:2 ,value:"+cache.getIfPresent(2));  
    34     }  
    35 }  
    View Code

    控制台输出:

     1 00:34:17 size: 0  
     2 write key:1 ,value:Hi  
     3 00:34:19 size: 1  
     4 00:34:21 size: 1  
     5 00:34:23 size: 1  
     6 00:34:25 size: 1  
     7 write key:2 ,value:bbb  
     8 00:34:27 size: 1  
     9 00:34:29 size: 1  
    10 00:34:31 size: 1  
    11 00:34:33 size: 1  
    12 00:34:35 size: 1  
    13 00:34:37 after write, key:1 ,value:null  
    14 00:34:37 size: 1  
    15 00:34:39 final, key:2 ,value:null  
    16 00:34:39 size: 0  
    View Code
    通过分析发现:
    (1)缓存项<1,"Hi">的存活时间是5秒,但经过5秒后并没有被清除,因为还是size=1
    (2)发生写操作cache.put(2, "bbb")后,缓存项<1,"Hi">被清除,因为size=1,而不是size=2
    (3)发生读操作cache.getIfPresent(1)后,缓存项<2,"bbb">没有被清除,因为还是size=1,看来读操作确实不一定会发生清除
    (4)发生读操作cache.getIfPresent(2)后,缓存项<2,"bbb">被清除,因为读的key就是2

    这在GuavaCache被称为“延迟删除”,即删除总是发生得比较“晚”,这也是GuavaCache不同于其他Cache的地方!这种实现方式的问题:缓存会可能会存活比较长的时间,一直占用着内存。如果使用了复杂的清除策略如基于容量的清除,还可能会占用着线程而导致响应时间变长。但优点也是显而易见的,没有启动线程,不管是实现,还是使用起来都让人觉得简单(轻量)。
    如果你还是希望尽可能的降低延迟,可以创建自己的维护线程,以固定的时间间隔调用Cache.cleanUp(),ScheduledExecutorService可以帮助你很好地实现这样的定时调度。不过这种方式依然没办法百分百的确定一定是自己的维护线程“命中”了维护的工作。

    总结

    请一定要记住GuavaCache的实现代码中没有启动任何线程!!Cache中的所有维护操作,包括清除缓存、写入缓存等,都是通过调用线程来操作的。这在需要低延迟服务场景中使用时尤其需要关注,可能会在某个调用的响应时间突然变大。
    GuavaCache毕竟是一款面向本地缓存的,轻量级的Cache,适合缓存少量数据。如果你想缓存上千万数据,可以为每个key设置不同的存活时间,并且高性能,那并不适合使用GuavaCache。
  • 相关阅读:
    Unity3D GUI图形用户界面系统
    Unity3D 自动寻路入门指南
    Unity3D 导航网格自动寻路(Navigation Mesh)
    拓展通用的冒泡排序方法
    DoTween 应用设置
    DoTween 教程
    Unity3D 脚本手册
    unity3d中获得物体的size
    Unity自动寻路Navmesh之高级
    C# 代码页获取input的值
  • 原文地址:https://www.cnblogs.com/cing/p/7818622.html
Copyright © 2020-2023  润新知