Резюме: В этой статье сначала рассказывается, почему используется Guava Cache, затем объясняется, как использовать Guava Cache, как его очищать и какие ошибки появляются во время использования, затем объясняется базовая структура данных и анализируются причины его отличной производительности, направление. оптимизации.
Зачем использовать локальный кеш
В многопоточных сценариях и сценариях с высокой степенью параллелизма кэш часто неотделим. Различные кэши необходимо выбирать в соответствии с различными сценариями приложений. Например, распределенные кэши, такие какRedis, memcached и локальные (внутрипроцессные) кэши, такие как ehcache,Guava Cache. По сравнению с операциями ввода-вывода кэш работает быстрее и эффективнее, по сравнению с Redis, Guava,Приложение и кеш находятся в одном процессе, кеш запросов работает очень быстро, нет чрезмерных сетевых накладных расходов.. Преимущество Redis в том, что это самостоятельная программа, и несколько приложений могут совместно использовать кеш. Мы должны решить, какой тип кеша следует использовать в соответствии с типом данных и бизнес-сценарием, чтобы уменьшить объем вычислений и повысить скорость отклика.
Применимые сценарии для локального кэширования (должны быть удовлетворены)
-
Готов потреблять немного памяти для скорости
-
Ожидается, что некоторые ключи будут запрашиваться несколько раз.
-
Общий объем данных, хранящихся в кеше, не будет превышать объем памяти
-
Для более быстрого отклика кеш не требует сетевого ввода-вывода (централизованному кешу требуется дополнительный сетевой ввод-вывод)
Как использовать кэш Гуавы
Guava Cache — это инструмент кэширования в Guava, библиотеке набора инструментов повторного использования Java с открытым исходным кодом от Google.Ниже описаны шаги для доступа к кешу guava и ямам, наступившим во время использования.
Демонстрация доступа к Guava Cache
- 1. Импорт ссылок Maven
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>19.0</version>
</dependency>
- 2. Инициализация кеша, используя режим проектирования Builder, вы можете самостоятельно задавать различные параметры
private final LoadingCache<Long, TestDemo> demo = CacheBuilder.newBuilder()
//设置 cache 的初始大小为10,要合理设置该值
.initialCapacity(10)
//设置并发数为5,即同一时间最多只能有5个线程往 cache 执行写入操作
.concurrencyLevel(5)
//最大 key 个数
.maximumSize(100)
//移除监听器
.removalListener(removalListener)
//设置 cache 中的数据在写入之后的存活时间为10秒
.expireAfterWrite(10, TimeUnit.SECONDS)
//构建 cache 实例
.build(new CacheLoader<Long, TestDemo>() {
@Override
public TestDemo load(Long id) throws Exception {
// 读取 db 数据
}
});
RemovalListener<String, String> removalListener = new RemovalListener<String, String>() {
public void onRemoval(RemovalNotification<id, TestDemo> removal) {
System.out.println("[" + removal.getKey() + ":" + removal.getValue() + removal.getCause() + "] is evicted!");
}
};
- 3. Если во время загрузки кеша возникает исключение, будет ли в следующий раз исключение кеша или обычные данные?
public class TestDemoImpl implements TestDemo {
int time;
private final LoadingCache<Long, TestDemo> demo;
this.demo1 = CacheBuilder.newBuilder()
.expireAfterWrite(duration, TimeUnit.HOURS)
.build(new CacheLoader<Long, TestDemo>() {
@Override
public TestDemo load(Long id) throws Exception {
// 查询 db
if (time++==0) {
throw new RunTimeException();
}
return result;
}
});
}
public static void main(String[] args) {
try {
try {
TestDemo testDemo = demoCache.getUnchecked(21L);
System.out.println("第一次查询:" + testDemo);
} catch (Exception e) {
}
TestDemo testDemo1 = testDemo.getUnchecked(21L);
System.out.println("第二次查询:" + testDemo1);
} catch (Exception e) {
e.printStackTrace();
}
}
-
Вывод: второй раз возвращает нормальные данные
Использование гуавы, чтобы наступить на яму
- 1,Избегайте слабых ключей при использовании кэша гуавы. Правило попадания ключа для слабых ключей ==.Если используется небазовый тип, кеш не может попасть, потому что оценка ключа не равна.
- 2,Просто кэшируйте сами метаданные, не кэшируйте их взаимосвязь, иначе это приведет к декартову произведению. Например, кешированные данные состоят из измерений трех таблиц, A, B и C, а отношение кэша приведет к объему данных AXBX C. Если метаданные кэшируются, объем кэшированных данных составляет только A+B. +С;
- 3.Перед использованием кеша необходимо оценить размер кэшируемых данных и установить количество или размер кеша. Если вы не установите срок действия, то не установите размер, данные кеша не могут быть восстановлены, это вызовет OOM
Как очистить кэш гуавы
В процессе ежедневной разработки наиболее распространенной проблемой является очистка кэша Guava Ниже описаны два метода очистки.
Ручная очистка
Вызывается вручную через остальные бэкдоры и т. д.
// 获取最新 db 数据更新缓存
testDemoCache.refresh(key);
// 清理 guava cache 缓存
estDemoCache.invalidateAll();
Автоматически очищать кеш
Ручная очистка кеша требует больших трудозатрат, сейчас есть решение по автоматической очистке кеша на всех машинах через MQ трансляцию после добавления/изменения данных. Или очистите кеш, прослушивая изменения базы данных через промежуточное ПО binlog, такое как canal.
Анализ принципа кэширования Guava
Кэш Guava наследует идею ConcurrentHashMap и использует мелкозернистые блокировки в нескольких сегментах для поддержки сценариев с высокой степенью параллелизма, обеспечивая при этом безопасность потоков. Ниже представлена его базовая реализация и проанализированы причины его отличной производительности.
несколько важных компонентов
1. CacheBuilder Построитель кеша. Создайте запись кэша, укажите параметры конфигурации кэша и инициализируйте локальный кэш. Использование шаблона проектирования Builder предоставляет объект кэша с различными наборами параметров. 2. Структура данных LocalCache. Структура данных класса ядра кэша LocalCache очень похожа на ConcurrentHashMap.Он состоит из нескольких сегментов, причем каждый сегмент относительно независим и не влияет друг на друга, поэтому может поддерживать параллельные операции.Каждый сегмент состоит из таблицы и нескольких очередей. . Кэшированные данные хранятся в таблице типа AtomicReferenceArray.
На приведенном выше рисунке показана базовая структура данных кэша Guava, а в следующей таблице объясняется его структура.
| серийный номер | структура данных | Функции | Детальное объяснение |
|---|---|---|---|
| 1 | Segment<K, V>[] segments | Сегмент наследуется от ReetrantLock для уменьшения детализации блокировок и повышения эффективности параллелизма. | |
| 2 | AtomicReferenceArray<ReferenceEntry<K, V>> table | Подобно таблице в HasmMap, она эквивалентна контейнеру записи. | |
| 3 | ReferenceEntry<K, V> referenceEntry | Запись на основе ссылок, ее класс реализации имеет запись со слабой ссылкой, запись со строгой ссылкой и т. д. | Кэш состоит из нескольких сегментов, и каждый сегмент содержит массив ReferenceEntry, каждый элемент массива ReferenceEntry представляет собой цепочку ReferenceEntry, а ReferenceEntry содержит поля key, hash, valueReference, next. В дополнение к цепочкам, сформированным в элементах массива ReferenceEntry, в Segment все ReferenceEntry также образуют цепочку доступа (accessQueue) и цепочку записи (writeQueue). ReferenceEntry может быть ключом строгого ссылочного типа или ключом типа WeakReference. Чтобы уменьшить использование памяти, вы также можете решить, нужна ли вам цепочка записи и цепочка доступа в зависимости от того, настроены ли expireAfterWrite, expireAfterAccess и maxSize для определения конкретного Создаваемая ссылка: StrongEntry, StrongWriteEntry, StrongAccessEntry, StrongWriteAccessEntry и т. д. |
| 4 | ReferenceQueue keyReferenceQueue | Очередь ссылок ключей, которая была проверена сборщиком мусора и нуждается в внутренней очистке. | |
| 5 | ReferenceQueue valueReferenceQueue | Очереди значений, включенные в сборщик мусора и нуждающиеся во внутренней очистке. | Поскольку Cache поддерживает значения со строгой ссылкой, значение SoftReference и значение WeakReference, он соответствует трем классам реализации: StrongValueReference, SoftValueReference и WeakValueReference. Для поддержки механизма динамической загрузки он также имеет LoadingValueReference. Когда значение ключа необходимо загрузить динамически, значение сначала инкапсулируется в LoadingValueReference, чтобы показать, что значение, соответствующее ключу, уже загружается. другие потоки также хотят запросить значение ключа Значение, соответствующее ключу, может получить ссылку и дождаться завершения загруженного значения, чтобы гарантировать, что значение загружается только один раз.После загрузки значения замените LoadingValueReference другим типом ValueReference. Ссылка на ReferenceEntry сохраняется в объекте ValueReference, потому что, когда Value перерабатывается из-за WeakReference и SoftReference, его ключ необходимо использовать для удаления соответствующего элемента из таблицы Segment. |
| 6 | Queue<ReferenceEntry<K, V>> recencyQueue | Запишите записи при обновлении списка доступных.Когда сегмент достигает критического значения или происходит операция записи, очередь будет очищена | |
| 7 | Queue<ReferenceEntry<K, V>> writeQueue | Очередь элементов, отсортированных по времени записи, когда элемент будет записан, он будет добавлен в конец очереди | Чтобы реализовать наименее использовавшийся алгоритм, Guava Cache добавляет в сегмент две цепочки: цепочку записи (writeQueue) и цепочку доступа (accessQueue), обе из которых представляют собой двусвязный список. форма очереди. WriteQueue и AccessQueue настраивают логику предложения, добавляют (вызывают предложение напрямую), удаляют, опрашивают и т. д. Для операции предложения (добавления), если это вновь добавленный узел, он будет добавлен непосредственно в конец цепочки, если это Для существующего узла, хвост цепочки узла связывается; для операции удаления узел удаляется непосредственно из цепочки; для операции опроса следующий узел головного узла удаляется и возвращается |
| 8 | Queue<ReferenceEntry<K, V>> accessQueue | Очередь элементов, отсортированных по времени доступа, при обращении к элементу (в том числе при записи) он добавляется в конец очереди |
Общий интерфейс Гуавы
/**
* 该接口的实现被认为是线程安全的,即可在多线程中调用
* 通过被定义单例使用
*/
public interface Cache<K, V> {
/**
* 通过key获取缓存中的value,若不存在直接返回null
*/
V getIfPresent(Object key);
На приведенном выше рисунке показана блок-схема выполнения метода getIfPresent.
/**
* 通过 key 获取缓存中的 value,若不存在就通过 valueLoader 来加载该 value
* 整个过程为 "if cached, return; otherwise create, cache and return"
* 注意 valueLoader 要么返回非 null 值,要么抛出异常,绝对不能返回 null
*/
V get(K key, Callable<? extends V> valueLoader) throws ExecutionException;
На приведенном выше рисунке показана блок-схема выполнения метода get.
/**
* 添加缓存,若key存在,就覆盖旧值
*/
void put(K key, V value);
На приведенном выше рисунке показана блок-схема выполнения метода put.
/**
* 删除该key关联的缓存
*/
void invalidate(Object key);
/**
* 删除所有缓存
*/
void invalidateAll();
/**
* 执行一些维护操作,包括清理缓存
*/
void cleanUp();
}
На картинке выше показана блок-схема удаления кеша.
восстановление кеша
Guava Cache предоставляет три основных метода перезапуска кэша: перезапуск на основе емкости, перезапуск по времени и перезапуск на основе ссылок.Внутренняя реализация метода на основе емкости использует алгоритм LRU, в котором эффективно используется механизм сборки мусора виртуальной машины Java на основе восстановления ссылок.
1. Выселение по размеру
Если вы хотите указатьколичество кэшированных элементовне более фиксированного значения, просто используйтеCacheBuilder.maximumSize(long). Кэш попытается восстановить кэшированные элементы, которые не использовались в последнее время или редко используются в целом. Прежде чем количество элементов кеша достигнет предела, кеш может быть высвобожден, вообще говоря, это происходит, когда количество элементов кеша приближается к пределу.
2, время восстановления (Timed Eviction)
CacheBuilder предоставляет два метода временного перезапуска:expireAfterAccess(long, TimeUnit): элемент кэша восстанавливается, если к нему не обращались для чтения/записи в течение заданного времени. Обратите внимание, что этот кеш освобождается в том же порядке, что и освобождение на основе емкости.expireAfterWrite(long, TimeUnit): элемент кэша не был доступен для записи (создан или перезаписан) в течение заданного времени, а затем восстановлен.Если вы считаете, что кэшированные данные всегда устаревают по истечении фиксированного периода времени, этот метод утилизации желателен.
Сбор по времени периодически выполняется при операциях записи и иногда при операциях чтения..
3. Выселение на основе ссылок
Используя ключи со слабыми ссылками, значения со слабыми ссылками или значения с мягкими ссылками, Guava Cache может настроить кеш для разрешения сборки мусора.CacheBuilder.weakKeys(): хранить ключи, используя слабые ссылки. Элементы кэша могут быть удалены сборщиком мусора, если у ключа нет других (строгих или программных) ссылок. Поскольку сборка мусора опирается только на идентификаторы (==), кэши, использующие ключи со слабыми ссылками, используют == вместо равенства для сравнения ключей.CacheBuilder.weakValues(): Используйте слабые ссылки для хранения значений. Элементы кэша могут быть удалены сборщиком мусора, если значение не имеет других (сильных или программных) ссылок. Поскольку сборка мусора опирается только на идентификаторы (==), кэши, использующие значения со слабыми ссылками, сравнивают значения с == вместо равных.CacheBuilder.softValues(): Используйте мягкие ссылки для хранения значений. Мягкие ссылки восстанавливаются в глобальном порядке наименее недавно использовавшихся только при необходимости в ответ на нехватку памяти. Учитывая влияние мягких ссылок на производительность, мы обычно рекомендуем использовать предельный размер кэша, более прогнозирующий производительность (см. выше, на основе высвобождения емкости). Кэши, использующие мягкие эталонные значения, также используют == вместо equals для сравнения значений.
4. Явно ясно
В любое время вы можете явно очистить элемент кеша, не дожидаясь его восстановления:индивидуальный клиринг: Cache.invalidate(ключ)Массовая очистка: Cache.invalidateAll(ключи)Очистить все кэшированные элементы:Cache.invalidateAll()
5. Удалить слушателя
С помощью CacheBuilder.removalListener (RemovalListener) вы можете объявить прослушиватель для выполнения некоторых дополнительных действий при удалении кэшированного элемента. Когда кешированный элемент удаляется, RemovalListener получает уведомление об удалении [RemovalNotification], которое содержит причину удаления [RemovalCause], ключ и значение.
6. СтатистикаCacheBuilder.recordStats(): используется для включения функции статистики Guava Cache. Когда статистика включена, метод Cache.stats() возвращает объект CacheStats для предоставления следующей статистики:
- hitRate(): частота попаданий в кеш;
- mediumLoadPenalty(): среднее время загрузки новых значений в наносекундах;
- evictionCount(): общее количество возвращаемых кэшированных элементов, исключая явные очистки.
Кроме того, есть много другой статистики. Эти статистические данные имеют решающее значение для настройки параметров кэша, и мы рекомендуем обратить на них пристальное внимание в приложениях с высокими требованиями к производительности.
Когда происходит уборка?
Кэши, созданные с помощью CacheBuilder, не выполняют «автоматически» очистку и освобождение, а также не очищаются, как только срок действия элемента кэша истекает, и такого механизма очистки нет. Напротив,Он будет выполнять небольшое обслуживание случайно при записи или иногда при чтении.(если записей слишком мало). Причина этого:Если вы хотите автоматически и постоянно очищать кеш, у вас должен быть поток, который конкурирует с пользовательскими операциями за общую блокировку.. Кроме того, создание потоков в некоторых средах может быть ограничено, поэтому CacheBuilder недоступен. Вместо этого мы передали вариант вам. Если ваш кеш имеет высокую пропускную способность, вам не нужно беспокоиться об обслуживании и очистке кеша. Если ваш кеш только изредка пишет, вы не хотите, чтобы работа по очистке мешала операции чтения, тогда вы можете создать свой собственный поток обслуживания и вызывать cache.cleanup() через фиксированный интервал времени. Scheduledexecutorvice может помочь вам достичь таких временных графиков.
Направление оптимизации
- 1. Для удобства использования SpringBoot интегрирует Guava Cache для реализации локального кэширования.
- 2. Оптимизируйте производительность, используйте кеш Caffeine вместо кеша Guava.
Caffeine — это новое поколение инструментов кэширования, основанное на Java8, и производительность кэширования близка к теоретическому оптимуму. Его можно рассматривать как расширенную версию Guava Cache. API похож на два. Основное отличие состоит в том, чтомеханизм устранения памяти; Потому что при ограничениях существующих алгоритмов частота попаданий кэшированных данных будет более или менее повреждена, а частота попаданий является важным показателем кэша. Caffeine использует стратегию переработки Window TinyLfu, которая обеспечивает почти оптимальную частоту попаданий.
Механизм реализации кофеина
-
Тинилфу поддерживаетсяИнформация о частоте недавних посещений, как фильтр, когда приходят новые записи, в кеш могут быть вставлены только те записи, которые соответствуют требованиям TinyLFU. Ему необходимо решить две проблемы: как избежать больших накладных расходов, связанных с хранением информации о частоте, и как отразить шаблоны доступа, которые меняются со временем.
-
В Count-Min Sketch, если размер вашего кеша равен 100, он сгенерирует длинный массив, размер которого равен ближайшей степени от 2 до 100, что равно 128. И этот массив будет записывать нашу частоту обращения. Максимальная частота, указанная в кофеине, составляет 15, 15 двоичных битов — 1111, всего 4 бита, а тип Long — 64 бита. Caffeine использует четыре алгоритма хеширования, каждый тип Long делится на четыре сегмента, и каждый сегмент хранит частоты четырех алгоритмов. Преимущество этого заключается в том, что конфликты хэшей могут быть дополнительно уменьшены.Первоначальный размер хэша 128 становится 128X4.
Детали алгоритма показаны на следующем рисунке:
1. Производительность чтения и записи
- В кэше гуавы операции чтения и записи смешиваются с обработкой времени истечения срока действия, то есть вы также можете выполнять операции исключения в операции размещения, поэтому его производительность чтения и записи будет в определенной степени затронута. обработка этих событий Операция выполняется через асинхронную операцию, которая отправляет событие в очередь.Структура данных очереди здесь RingBuffer, а затем она будет передавать ForkJoinPool.commonPool() по умолчанию или настроить пул потоков самостоятельно , выполните операцию извлечения из очереди, а затем выполните последующее удаление. , операция с истекшим сроком действия. Также существуют разные очереди для чтения и записи.В caffeine считается, что кэшированных операций чтения намного больше, чем записей, поэтому для операций записи все выполнения имеют общий кольцевой буфер.
2. Стратегия удаления данных
-
Все данные в кофеине находятся в ConcurrentHashMap, который отличается от кеша гуавы, кеш гуавы реализует структуру, аналогичную ConcurrentHashMap сам по себе. Есть три очереди LRU, на которые ссылаются записи в кофеине:
-
Очередь Эдема: В кофеине оговорено, что она может составлять только 1% от емкости кеша, если size = 100, то эффективный размер этой очереди равен 1. Вновь поступившие данные записываются в эту очередь, чтобы предотвратить устранение всплесков трафика из-за отсутствия частоты доступа ранее. На самом деле, в начале новой сборки нет частоты доступа, чтобы предотвратить ее устранение другими кэшами после выхода в сеть и присоединения к этой области. Иденский район, самый благоустроенный и благоустроенный район, здесь трудно исключить другими данными.
-
Очередь на пробацию: она называется очередью на пробацию.В этой очереди это означает, что ваши данные относительно холодные и скоро будут удалены. Эффективный размер равен размеру минус eden минус защищенный.
-
Защищенная очередь: в этой очереди вы не будете исключены на данный момент.Если в очереди пробации нет данных или защищенные данные заполнены, вы также столкнетесь с неловкой ситуацией исключения. Конечно, если вы хотите стать этой очередью, вам нужно один раз получить доступ к испытательному сроку, а затем она будет переведена в Защищенную очередь. Этот эффективный размер равен (размер минус eden) X 80%. Если размер = 100, будет 79.
1. Все новые данные будут поступать в Eden. 2. Иден полон, устранен и находится на испытательном сроке. 3. Если к одному из данных будет осуществлен доступ в испытательном сроке, данные будут обновлены до уровня «Защищено». 4. Если Защищено заполнено, его статус по-прежнему будет понижен до Испытательного срока. 5. Когда произойдет удаление данных, они будут исключены из испытательного срока. Голова очереди данных в этой очереди будет называться жертвой.Этот глава очереди должен войти первым.По алгоритму LRU очереди он должен быть фактически устранен,но здесь его можно назвать только Жертва Эта очередь Это очередь на условный срок, и представитель собирается казнить его. Здесь хвост очереди называется кандидатом, также называемым злоумышленником. Здесь жертва будет играть с нападавшим, чтобы решить, кого следует устранить.
Следующие суждения сделаны на основе частотных данных, записанных в нашем эскизе Count-Min:
1. Если нападающий больше жертвы, то жертва уничтожается напрямую. 2. Если атакующий
3. Другие оптимизацииПолитика истечения срока действия: В Caffeine есть метод scheduleDrainBuffers, который используется для планирования наших просроченных задач, которые будут вызываться после чтения и записи:обновить стратегию: создать CacheLoader для переупорядочивания, которое выполняется синхронно и может быть построено асинхронно с помощью метода buildAsync, а автоматическое обновление существует только после операции чтения;
Суммировать
Кэш Guava основан на отличном дизайне ConcurrentHashMap и имеет соответствующие стратегии улучшения в поддержке сценариев с высоким параллелизмом и безопасности потоков.Использование справочной команды Reference повышает скорость доступа к данным в условиях высокого параллелизма и поддерживает возможность повторного использования GC, эффективно экономя место; В то же время время, дизайн цепочки записи и цепочки доступа может более гибко и эффективно реализовывать различные типы стратегий очистки кеша, включая очистку на основе тома, очистку на основе времени, очистку на основе ссылок и т. д. Программное управление генератором сборки, позволяет пользователи имеют больше степеней свободы и могут устанавливать соответствующие режимы в соответствии с различными сценариями.
В коде реализации GuavaCache не запускается ни один поток.Все операции обслуживания в Cache, включая очистку кеша, запись кеша и т. д., требуют для реализации внешних вызовов. Это требует внимания при использовании в сценариях, требующих услуг с малой задержкой, и время ответа на вызов может внезапно увеличиться. В конце концов, GuavaCache — это облегченный Cache, ориентированный на локальный кеш, подходящий для кэширования небольшого объема данных.Если вы хотите кэшировать десятки миллионов данных, вы можете установить разное время выживания для каждого ключа, а высокая производительность не подходит для использования GuavaCache..