Skip to main content
 首页 » 编程设计

Guava 缓存教程

2022年07月19日124freeliver54

Guava 缓存

本文我们看下Guava Cache的实现,包括基本使用,驱逐策略,刷新缓存以及一些有趣的批处理操作。最后,我们再看看缓存发出的删除通知功能。

如何使用Guava Cache

先来看一个简单示例,缓存字符串实例的大小形式。首先,我们创建ChcheLoader,用于计算存储在缓存中的值,然后我们便捷的CacheBuilder类依照规范构建缓存:

@Test 
public void whenCacheMiss_thenValueIsComputed() { 
    CacheLoader<String, String> loader; 
    loader = new CacheLoader<String, String>() { 
        @Override 
        public String load(String key) { 
            return key.toUpperCase(); 
        } 
    }; 
 
    LoadingCache<String, String> cache; 
    cache = CacheBuilder.newBuilder().build(loader); 
 
    assertEquals(0, cache.size()); 
    assertEquals("HELLO", cache.getUnchecked("hello")); 
    assertEquals(1, cache.size()); 
}

因为“hello” 键对应值在缓存中没有,所以值被计算并缓存。注意,我们使用getUnchecked() 方法,如果对应值不存在,则计算并缓存值到缓存中。

驱逐策略

每个缓存需要在必要时删除内容,让我们讨论下从缓存中清除值机制——使用不同的标准。

按大小驱逐

我们能通过 maximumSize()方法限制缓存大小,如果缓存达到上限,最老的项将被驱逐。
下面示例代码中,我们限制缓存大小为3条记录:

@Test 
public void whenCacheReachMaxSize_thenEviction() { 
    CacheLoader<String, String> loader; 
    loader = new CacheLoader<String, String>() { 
        @Override 
        public String load(String key) { 
            return key.toUpperCase(); 
        } 
    }; 
    LoadingCache<String, String> cache; 
    cache = CacheBuilder.newBuilder().maximumSize(3).build(loader); 
 
    cache.getUnchecked("first"); 
    cache.getUnchecked("second"); 
    cache.getUnchecked("third"); 
    cache.getUnchecked("forth"); 
    assertEquals(3, cache.size()); 
    assertNull(cache.getIfPresent("first")); 
    assertEquals("FORTH", cache.getIfPresent("forth")); 
}

按权重驱逐

我们也可以按照自定义权重功能限制缓存大小,下面代码,使用length作为我们自定义权重函数:

@Test 
public void whenCacheReachMaxWeight_thenEviction() { 
    CacheLoader<String, String> loader; 
    loader = new CacheLoader<String, String>() { 
        @Override 
        public String load(String key) { 
            return key.toUpperCase(); 
        } 
    }; 
 
    Weigher<String, String> weighByLength; 
    weighByLength = new Weigher<String, String>() { 
        @Override 
        public int weigh(String key, String value) { 
            return value.length(); 
        } 
    }; 
 
    LoadingCache<String, String> cache; 
    cache = CacheBuilder.newBuilder() 
      .maximumWeight(16) 
      .weigher(weighByLength) 
      .build(loader); 
 
    cache.getUnchecked("first"); 
    cache.getUnchecked("second"); 
    cache.getUnchecked("third"); 
    cache.getUnchecked("last"); 
    assertEquals(3, cache.size()); 
    assertNull(cache.getIfPresent("first")); 
    assertEquals("LAST", cache.getIfPresent("last")); 
}

注意,缓存可能删除多条记录,为较长记录预留空间。

按时间驱逐

除了按大小驱逐较早记录,我们还可以采用时间方式。下面示例我们自定义缓存删除已经闲置2ms的记录:

@Test 
public void whenEntryIdle_thenEviction() 
  throws InterruptedException { 
    CacheLoader<String, String> loader; 
    loader = new CacheLoader<String, String>() { 
        @Override 
        public String load(String key) { 
            return key.toUpperCase(); 
        } 
    }; 
 
    LoadingCache<String, String> cache; 
    cache = CacheBuilder.newBuilder() 
      .expireAfterAccess(2,TimeUnit.MILLISECONDS) 
      .build(loader); 
 
    cache.getUnchecked("hello"); 
    assertEquals(1, cache.size()); 
 
    cache.getUnchecked("hello"); 
    Thread.sleep(300); 
 
    cache.getUnchecked("test"); 
    assertEquals(1, cache.size()); 
    assertNull(cache.getIfPresent("hello")); 
}

我们也可以基于整个生命时间来驱逐,下面示例中,被存储2ms后记录被清除缓存。

@Test 
public void whenEntryLiveTimeExpire_thenEviction() 
  throws InterruptedException { 
    CacheLoader<String, String> loader; 
    loader = new CacheLoader<String, String>() { 
        @Override 
        public String load(String key) { 
            return key.toUpperCase(); 
        } 
    }; 
 
    LoadingCache<String, String> cache; 
    cache = CacheBuilder.newBuilder() 
      .expireAfterWrite(2,TimeUnit.MILLISECONDS) 
      .build(loader); 
 
    cache.getUnchecked("hello"); 
    assertEquals(1, cache.size()); 
    Thread.sleep(300); 
    cache.getUnchecked("test"); 
    assertEquals(1, cache.size()); 
    assertNull(cache.getIfPresent("hello")); 
}

弱引用键(Weak key)

接下来,看看如何使缓存键为弱引用,即允许垃圾收集器收集在其他地方没有引用的缓存键。缺省缓存键和值都为强引用,但我们能使用weakKeys()方法使缓存键为弱引用,示例如下:

@Test 
public void whenWeakKeyHasNoRef_thenRemoveFromCache() { 
    CacheLoader<String, String> loader; 
    loader = new CacheLoader<String, String>() { 
        @Override 
        public String load(String key) { 
            return key.toUpperCase(); 
        } 
    }; 
 
    LoadingCache<String, String> cache; 
    cache = CacheBuilder.newBuilder().weakKeys().build(loader); 
}

缓存值软引用(soft valuel)

我们可以使用softValues()方法让垃圾回收器收集缓存值,防止内存溢出:

@Test 
public void whenSoftValue_thenRemoveFromCache() { 
    CacheLoader<String, String> loader; 
    loader = new CacheLoader<String, String>() { 
        @Override 
        public String load(String key) { 
            return key.toUpperCase(); 
        } 
    }; 
 
    LoadingCache<String, String> cache; 
    cache = CacheBuilder.newBuilder().softValues().build(loader); 
}

需要注意的时,软引用太多会影响系统性能,最好使用maximumSize()方法。

处理null值

现在,让我们看看缓存如何null值。默认情况下,Guava Cache如果加载null值会抛出异常,因为缓存null值没有意义。
但如果null值在你代码中有意义,那你可以使用Optional类实现:

@Test 
public void whenNullValue_thenOptional() { 
    CacheLoader<String, Optional<String>> loader; 
    loader = new CacheLoader<String, Optional<String>>() { 
        @Override 
        public Optional<String> load(String key) { 
            return Optional.fromNullable(getSuffix(key)); 
        } 
    }; 
 
    LoadingCache<String, Optional<String>> cache; 
    cache = CacheBuilder.newBuilder().build(loader); 
 
    assertEquals("txt", cache.getUnchecked("text.txt").get()); 
    assertFalse(cache.getUnchecked("hello").isPresent()); 
} 
private String getSuffix(final String str) { 
    int lastIndex = str.lastIndexOf('.'); 
    if (lastIndex == -1) { 
        return null; 
    } 
    return str.substring(lastIndex + 1); 
}

刷新缓存

下面看看如何刷新缓存值,可以使用refreshAfterWrite()方法自动刷新。下面示例中,每分钟自动刷新:

@Test 
public void whenLiveTimeEnd_thenRefresh() { 
    CacheLoader<String, String> loader; 
    loader = new CacheLoader<String, String>() { 
        @Override 
        public String load(String key) { 
            return key.toUpperCase(); 
        } 
    }; 
 
    LoadingCache<String, String> cache; 
    cache = CacheBuilder.newBuilder() 
      .refreshAfterWrite(1,TimeUnit.MINUTES) 
      .build(loader); 
}

也可以手动通过refresh(key)方法刷新缓存中指定记录。

预缓存

我们可以通过putAll()方法在缓存中插入多条记录。下面示例中,通过Map往缓存中增加多条记录:

@Test 
public void whenPreloadCache_thenUsePutAll() { 
    CacheLoader<String, String> loader; 
    loader = new CacheLoader<String, String>() { 
        @Override 
        public String load(String key) { 
            return key.toUpperCase(); 
        } 
    }; 
 
    LoadingCache<String, String> cache; 
    cache = CacheBuilder.newBuilder().build(loader); 
 
    Map<String, String> map = new HashMap<String, String>(); 
    map.put("first", "FIRST"); 
    map.put("second", "SECOND"); 
    cache.putAll(map); 
 
    assertEquals(2, cache.size()); 
}

删除通知

有时当缓存记录删除时,需要处理一些业务,下面讨论下RemovalNotification。
我们可以注册RemovalListener 监听器获得删除通知,还可以通过getCause()方法访问删除原因。
下面示例中,缓存中第四个元素删除时会收到通知:

@Test 
public void whenEntryRemovedFromCache_thenNotify() { 
    CacheLoader<String, String> loader; 
    loader = new CacheLoader<String, String>() { 
        @Override 
        public String load(final String key) { 
            return key.toUpperCase(); 
        } 
    }; 
 
    RemovalListener<String, String> listener; 
    listener = new RemovalListener<String, String>() { 
        @Override 
        public void onRemoval(RemovalNotification<String, String> n){ 
            if (n.wasEvicted()) { 
                String cause = n.getCause().name(); 
                assertEquals(RemovalCause.SIZE.toString(),cause); 
            } 
        } 
    }; 
 
    LoadingCache<String, String> cache; 
    cache = CacheBuilder.newBuilder() 
      .maximumSize(3) 
      .removalListener(listener) 
      .build(loader); 
 
    cache.getUnchecked("first"); 
    cache.getUnchecked("second"); 
    cache.getUnchecked("third"); 
    cache.getUnchecked("last"); 
    assertEquals(3, cache.size()); 
}

补充说明

最后,有一些关于Guava 缓存实现的补充说明:

  • 是线程安全的

  • put(key,value)方法可以手动插入记录

  • CacheStats ( hitRate(), missRate(), ..)等方法可以监控缓存性能

总结

文本通过示例说明Guava缓存主要功能及应用,从简单使用到元素驱逐,刷新缓存,预缓存以及删除通知等。


本文参考链接:https://blog.csdn.net/neweastsun/article/details/79904098
阅读延展