1. введение
В этой статье мы рассмотрим Кофеин — высокопроизводительную библиотеку кэширования для Java .
Одно из фундаментальных различий между кэшем и Картой заключается в том, что кэш удаляет сохраненные элементы.
Политика выселения определяет, какие объекты должны быть удалены в любой момент времени. Эта политика напрямую влияет на частоту попаданий в кэш — важнейшую характеристику библиотек кэширования.
Кофеин использует политику Window TinyLfu выселения, которая обеспечивает почти оптимальную скорость попадания .
2. Зависимость
Нам нужно добавить кофеин зависимость к вашему pom.xml :
com.github.ben-manes.caffeine caffeine 2.5.5
Вы можете найти последнюю версию кофеина на Maven Central .
3. Заполнение Кэша
Давайте сосредоточимся на трех стратегиях кофеина для заполнения кэша: ручная, синхронная загрузка и асинхронная загрузка.
Во-первых, давайте напишем класс для типов значений, которые мы будем хранить в нашем кэше:
class DataObject { private final String data; private static int objectCounter = 0; // standard constructors/getters public static DataObject get(String data) { objectCounter++; return new DataObject(data); } }
3.1. Ручное Заполнение
В этой стратегии мы вручную помещаем значения в кэш и извлекаем их позже.
Давайте инициализируем наш кэш:
Cachecache = Caffeine.newBuilder() .expireAfterWrite(1, TimeUnit.MINUTES) .maximumSize(100) .build();
Теперь мы можем получить некоторое значение из кэша, используя метод getIfPresent /. Этот метод вернет null , если значение отсутствует в кэше:
String key = "A"; DataObject dataObject = cache.getIfPresent(key); assertNull(dataObject);
Мы можем заполнить кэш вручную, используя метод put :
cache.put(key, dataObject); dataObject = cache.getIfPresent(key); assertNotNull(dataObject);
Мы также можем получить значение с помощью метода get , который принимает Функцию вместе с ключом в качестве аргумента. Эта функция будет использоваться для предоставления резервного значения, если ключ отсутствует в кэше, который будет вставлен в кэш после вычисления:
dataObject = cache .get(key, k -> DataObject.get("Data for A")); assertNotNull(dataObject); assertEquals("Data for A", dataObject.getData());
Метод get выполняет вычисление атомарно. Это означает, что вычисление будет выполнено только один раз — даже если несколько потоков запрашивают значение одновременно. Вот почему использование get предпочтительнее getIfPresent .
Иногда нам нужно аннулировать некоторые кэшированные значения вручную:
cache.invalidate(key); dataObject = cache.getIfPresent(key); assertNull(dataObject);
3.2. Синхронная загрузка
Этот метод загрузки кэша принимает функцию , которая используется для инициализации значений, аналогично методу get ручной стратегии. Давайте посмотрим, как мы можем это использовать.
Прежде всего, нам нужно инициализировать наш кэш:
LoadingCachecache = Caffeine.newBuilder() .maximumSize(100) .expireAfterWrite(1, TimeUnit.MINUTES) .build(k -> DataObject.get("Data for " + k));
Теперь мы можем получить значения с помощью метода get :
DataObject dataObject = cache.get(key); assertNotNull(dataObject); assertEquals("Data for " + key, dataObject.getData());
Мы также можем получить набор значений с помощью метода get All :
MapdataObjectMap = cache.getAll(Arrays.asList("A", "B", "C")); assertEquals(3, dataObjectMap.size());
Значения извлекаются из базовой внутренней инициализации Функции , которая была передана методу build . Это позволяет использовать кэш в качестве главного фасада для доступа к значениям.
3.3. Асинхронная загрузка
Эта стратегия работает так же, как и предыдущая, но выполняет операции асинхронно и возвращает CompletableFuture , удерживая фактическое значение:
AsyncLoadingCachecache = Caffeine.newBuilder() .maximumSize(100) .expireAfterWrite(1, TimeUnit.MINUTES) .buildAsync(k -> DataObject.get("Data for " + k));
Мы можем использовать методы get и GetAll | таким же образом, принимая во внимание тот факт, что они возвращают CompletableFuture :
String key = "A"; cache.get(key).thenAccept(dataObject -> { assertNotNull(dataObject); assertEquals("Data for " + key, dataObject.getData()); }); cache.getAll(Arrays.asList("A", "B", "C")) .thenAccept(dataObjectMap -> assertEquals(3, dataObjectMap.size()));
CompletableFuture имеет богатый и полезный API, о котором вы можете подробнее прочитать в этой статье .
4. Выселение ценностей
Кофеин имеет три стратегии выселения ценностей : основанные на размере, основанные на времени и основанные на ссылках.
4.1. Выселение По Размеру
Этот тип выселения предполагает, что выселение происходит при превышении заданного предела размера кэша . Есть два способа получить размер — подсчет объектов в кэше или получение их веса.
Давайте посмотрим, как мы могли бы подсчитать объекты в кэше . При инициализации кэша его размер равен нулю:
LoadingCachecache = Caffeine.newBuilder() .maximumSize(1) .build(k -> DataObject.get("Data for " + k)); assertEquals(0, cache.estimatedSize());
Когда мы добавляем значение, размер, очевидно, увеличивается:
cache.get("A"); assertEquals(1, cache.estimatedSize());
Мы можем добавить второе значение в кэш, что приведет к удалению первого значения:
cache.get("B"); cache.cleanUp(); assertEquals(1, cache.estimatedSize());
Стоит отметить, что мы вызываем метод cleanUp перед получением размера кэша . Это связано с тем, что выселение кэша выполняется асинхронно, и этот метод помогает дождаться завершения выселения .
Мы также можем передать weigher |/Функцию , чтобы получить размер кэша:
LoadingCachecache = Caffeine.newBuilder() .maximumWeight(10) .weigher((k,v) -> 5) .build(k -> DataObject.get("Data for " + k)); assertEquals(0, cache.estimatedSize()); cache.get("A"); assertEquals(1, cache.estimatedSize()); cache.get("B"); assertEquals(2, cache.estimatedSize());
Значения удаляются из кэша, когда вес превышает 10:
cache.get("C"); cache.cleanUp(); assertEquals(2, cache.estimatedSize());
4.2. Временное Выселение
Эта стратегия выселения основана на времени истечения срока действия записи и имеет три типа:
- Истекает после доступа — запись истекает после истечения периода с момента последнего чтения или записи
- Истекает после записи — запись истекает после истечения периода с момента последней записи
- Пользовательская политика — время истечения срока действия рассчитывается для каждой записи индивидуально по реализации Истечения срока действия
Давайте настроим стратегию expireafteraccess с помощью метода expireAfterAccess :
LoadingCachecache = Caffeine.newBuilder() .expireAfterAccess(5, TimeUnit.MINUTES) .build(k -> DataObject.get("Data for " + k));
Для настройки стратегии expireafterwrite мы используем метод expireAfterWrite :
cache = Caffeine.newBuilder() .expireAfterWrite(10, TimeUnit.SECONDS) .weakKeys() .weakValues() .build(k -> DataObject.get("Data for " + k));
Чтобы инициализировать пользовательскую политику, нам необходимо реализовать интерфейс Expiration :
cache = Caffeine.newBuilder().expireAfter(new Expiry() { @Override public long expireAfterCreate( String key, DataObject value, long currentTime) { return value.getData().length() * 1000; } @Override public long expireAfterUpdate( String key, DataObject value, long currentTime, long currentDuration) { return currentDuration; } @Override public long expireAfterRead( String key, DataObject value, long currentTime, long currentDuration) { return currentDuration; } }).build(k -> DataObject.get("Data for " + k));
4.3. Выселение На Основе Ссылок
Мы можем настроить ваш кэш, чтобы разрешить сборку мусора ключей кэша и/или значений . Для этого мы настроим использование Слабой ссылки как для ключей, так и для значений, и мы можем настроить SoftReference только для сбора мусора значений.
Использование Слабой ссылки позволяет собирать мусор объектов, когда нет никаких сильных ссылок на объект. SoftReference позволяет собирать объекты на основе глобальной стратегии JVM, используемой в последнее время. Более подробную информацию о ссылках в Java можно найти здесь .
Мы должны использовать Кофеин.слабые клавиши() , Кофеин.слабые значения(), и Кофеин.мягкие значения() для включения каждой опции:
LoadingCachecache = Caffeine.newBuilder() .expireAfterWrite(10, TimeUnit.SECONDS) .weakKeys() .weakValues() .build(k -> DataObject.get("Data for " + k)); cache = Caffeine.newBuilder() .expireAfterWrite(10, TimeUnit.SECONDS) .softValues() .build(k -> DataObject.get("Data for " + k));
5. Освежающий
Можно настроить кэш для автоматического обновления записей по истечении определенного периода времени. Давайте посмотрим, как это сделать с помощью метода refreshAfterWrite :
Caffeine.newBuilder() .refreshAfterWrite(1, TimeUnit.MINUTES) .build(k -> DataObject.get("Data for " + k));
Здесь мы должны понимать разницу между истекает после и обновить после . Когда запрашивается запись с истекшим сроком действия, выполнение блокируется до тех пор, пока новое значение не будет вычислено функцией build |/.
Но если запись имеет право на обновление, то кэш вернет старое значение и асинхронно перезагрузит значение .
6. Статистика
Кофеин имеет средство записи статистики об использовании кэша :
LoadingCachecache = Caffeine.newBuilder() .maximumSize(100) .recordStats() .build(k -> DataObject.get("Data for " + k)); cache.get("A"); cache.get("A"); assertEquals(1, cache.stats().hitCount()); assertEquals(1, cache.stats().missCount());
Мы также можем перейти в recordStats supplier, который создает реализацию StatsCounter. Этот объект будет выталкиваться при каждом изменении, связанном со статистикой.
7. Заключение
В этой статье мы познакомились с библиотекой кэширования кофеина для Java. Мы увидели, как настроить и заполнить кэш, а также как выбрать соответствующую политику истечения срока действия или обновления в соответствии с нашими потребностями.
Исходный код, показанный здесь, доступен на Github .