Перейти к основному содержимому

Кэш гуавы

· 7 мин. чтения

Задача: Наибольшая подстрока без повторений

Для заданной строки s, найдите длину наибольшей подстроки без повторяющихся символов. Подстрока — это непрерывная непустая последовательность символов внутри строки...

ANDROMEDA 42

1. Обзор

В этом руководстве мы рассмотрим реализацию Guava Cache — базовое использование, политики исключения, обновление кеша и некоторые интересные массовые операции.

Наконец, мы рассмотрим использование уведомлений об удалении, которые кэш может отправлять.

2. Как использовать кеш гуавы

Давайте начнем с простого примера — давайте кэшируем заглавную форму экземпляров String .

Во-первых, мы создадим CacheLoader , который будет использоваться для вычисления значения, хранящегося в кеше. Исходя из этого, мы будем использовать удобный 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() — она вычисляет и загружает значение в кеш, если оно еще не существует.

3. Политика выселения

Каждый кеш должен в какой-то момент удалить значения. Давайте обсудим механизм вытеснения значений из кеша — по разным критериям.

3.1. Выселение по размеру

Мы можем ограничить размер нашего кеша, используя maxSize() . Если кеш достигает предела, самые старые элементы будут удалены.

В следующем коде мы ограничиваем размер кеша тремя записями:

@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"));
}

3.2. Выселение по весу

Мы также можем ограничить размер кеша, используя пользовательскую функцию веса. В следующем коде мы используем длину в качестве нашей пользовательской функции веса:

@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"));
}

Примечание. Кэш может удалить более одной записи, чтобы освободить место для новой большой записи.

3.3. Выселение по времени

Помимо использования размера для удаления старых записей, мы можем использовать время. В следующем примере мы настраиваем наш кеш для удаления записей, которые простаивали в течение 2 мс :

@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"));
}

Мы также можем исключать записи на основе их общего времени жизни . В следующем примере кеш удалит записи через 2 мс после их сохранения:

@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"));
}

4. Слабые ключи

Далее давайте посмотрим, как сделать так, чтобы наши ключи кеша имели слабые ссылки, позволяя сборщику мусора собирать ключи кеша, на которые нет ссылок где-либо еще.

По умолчанию ключи и значения кеша имеют сильные ссылки, но мы можем заставить наш кеш хранить ключи, используя слабые ссылки, используя 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);
}

5. Мягкие значения

Мы можем позволить сборщику мусора собирать наши кэшированные значения, используя 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);
}

Примечание. Многие программные ссылки могут повлиять на производительность системы — предпочтительно использовать maxSize() .

6. Обработка нулевых значений

Теперь давайте посмотрим, как обрабатывать нулевые значения кеша. По умолчанию Guava Cache будет генерировать исключения, если вы попытаетесь загрузить нулевое значение, поскольку нет никакого смысла кэшировать нулевое значение .

Но если нулевое значение что-то означает в вашем коде, вы можете эффективно использовать класс 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);
}

7. Обновить кеш

Далее давайте посмотрим, как обновить наши значения кеша.

7.1. Обновление вручную

Мы можем обновить один ключ вручную с помощью LoadingCache.refresh(key).

String value = loadingCache.get("key");
loadingCache.refresh("key");

Это заставит CacheLoader загрузить новое значение для ключа.

Пока новое значение не будет успешно загружено, get(key) вернет предыдущее значение ключа . ``

7.2. Автоматическое обновление

Мы можем использовать CacheBuilder.refreshAfterWrite(duration) для автоматического обновления кэшированных значений.

@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);
}

Важно понимать, что refreshAfterWrite(duration) делает ключ пригодным для обновления только после указанной продолжительности . Значение будет фактически обновлено только тогда, когда соответствующая запись будет запрошена с помощью get(key).

8. Предварительно загрузите кэш

Мы можем вставить несколько записей в наш кеш, используя метод putAll() . В следующем примере мы добавляем несколько записей в наш кеш с помощью карты :

@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());
}

9. Уведомление об удалении

Иногда вам нужно предпринять какие-то действия, когда запись удаляется из кеша; Итак, давайте обсудим RemovalNotification .

Мы можем зарегистрировать RemovalListener , чтобы получать уведомления об удалении записи. У нас также есть доступ к причине удаления — через метод getCause() .

В следующем примере RemovalNotification получен, когда четвертый элемент в кеше из-за его размера:

@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());
}

10. Примечания

Наконец, вот несколько дополнительных быстрых заметок о реализации кэша Guava:

  • это потокобезопасно
  • вы можете вручную вставлять значения в кеш, используя put(key,value)
  • вы можете измерить производительность кэша с помощью CacheStats ( hitRate() , missRate() , ..)

11. Заключение

В этом руководстве мы рассмотрели множество вариантов использования кеша Guava — от простого использования до вытеснения элементов, обновления и предварительной загрузки кеша и уведомлений об удалении.

Как обычно, все примеры можно найти на GitHub .