«1. Обзор

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

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

2. Как использовать Guava Cache

Давайте начнем с простого примера – давайте кэшируем экземпляры 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.