предисловие
Недавно я просматривал исходный код Mybatis, и я только что видел кеш Mybatis предоставляет кеш первого уровня и кеш второго уровня, кеш первого уровня относительно прост, а кеш второго уровня относительно полный, что в основном удовлетворяет потребности кэша.Некоторые функции;конечно, если его сравнивать со специальным фреймворком кэширования, таким как ehcache, может быть небольшой пробел, в этой статье мы разберем, что следует учитывать при реализация локального кэша.
Учитывайте баллы
Основными моментами рассмотрения являются то, как хранить данные, сколько данных можно хранить, как обращаться с избыточными данными и т. д. Давайте подробно представим каждый пункт рассмотрения и способы его реализации;
1. Структура данных
Первое соображение заключается в том, как хранить данные и какую структуру данных использовать для их хранения.Проще всего использовать Map для непосредственного хранения данных или для сложных типов данных, таких как Redis, он предоставляет хэши, списки, наборы, упорядоченные наборы, и т. д. Нижний уровень использует структуры данных, такие как двусторонние связанные списки, сжатые списки, наборы и списки переходов;
2. Верхний предел объекта
Поскольку это локальный кеш, а объем памяти имеет верхний предел, обычно указывается количество кэшируемых объектов, например 1024. При достижении определенного верхнего предела требуется определенная стратегия для удаления избыточных данных;
3. Клиринговая стратегия
Как упоминалось выше, при достижении верхнего предела объекта требуется стратегия очистки, такая как LRU (наименее недавно использовавшаяся), FIFO (первым поступил – первым обслужен), LFU (наименее часто использовавшийся в последнее время), SOFT (мягкая ссылка). ), WEAK (слабая ссылка) и др. стратегия;
4. Срок годности
В дополнение к использованию стратегии очистки локальный кеш также будет иметь настройку времени истечения срока действия.Например, Redis может установить время истечения срока действия для каждого ключа, так что, когда время истечения срока действия будет достигнуто, он будет удален напрямую, а очистка стратегия + двойная гарантия срока экспирации;
5. Безопасность потоков
Например, Redis использует однопоточную обработку напрямую, поэтому нет проблем с безопасностью потоков; а локальный кэш, который мы сейчас предоставляем, часто может быть доступен нескольким потокам одновременно, поэтому безопасность потоков — это проблема, которую нельзя игнорировать; и вопросы безопасности потоков не должны быть брошены пользователю, чтобы гарантировать;
6. Лаконичный интерфейс
Необходимо предусмотреть дурацкий внешний интерфейс, для пользователей использование этого кеша не в тягость, а в удовольствие, достаточно предоставить общеупотребительные методы get, put, remove, clear, getSize;
7. Он стойкий?
Это на самом деле не обязательно.Необходимо ли сохранять кэшированные данные, зависит от требований, локальные кеши, такие как ehcache, поддерживают сохранение, в то время как guava не имеет сохранения, распределенные кеши, такие как redis, имеют сохранение, а memcached - нет.Persistence function;
8. Механизм блокировки
При просмотре исходного кода Mybatis кеш второго уровня предоставляет флаг блокировки, что означает, что, когда элемент не найден в кеше, он устанавливает блокировку ключа кеша, поэтому другие потоки будут ждать, пока этот элемент будет найден. заполняется вместо попадания в базу данных; Фактически, цель использования кеша заключается в том, что требуется время для генерации кэшированных данных, таких как вызов внешнего интерфейса, запрос к базе данных, вычисление большого количества результатов и т. д. В это время , если несколько потоков вызывают метод get одновременно, все полученные результаты равны нулю, каждый поток выполняет трудоемкие вычисления, что на самом деле является пустой тратой ресурсов; лучший способ — иметь для выполнения только один поток, и другие потоки для ожидания, и одного расчета достаточно, но эта функция в основном передается пользователю Для обработки, несколько локальных кешей имеют эту функцию;
Как добиться
Вышеприведенное примерно представляет, что нам нужно учитывать при реализации локального кеша.Конечно, могут быть и другие моменты, которые мы не рассмотрели, давайте продолжим смотреть, как должен быть реализован каждый пункт, сосредоточившись на идеях;
1. Структура данных
Наиболее распространенный локальный кеш — использовать Map для прямого хранения.Например, guava использует ConcurrentHashMap, ehcache также использует ConcurrentHashMap, а вторичный кеш Mybatis использует HashMap для хранения:
Map<Object, Object> cache = new ConcurrentHashMap<Object, Object>()
Использование Mybatis HashMap само по себе не является потокобезопасным, поэтому можно увидеть, что SynchronizedCache используется внутри для упаковки, чтобы обеспечить потокобезопасность;
Конечно, помимо использования Map для хранения, для хранения могут использоваться и другие структуры данных.Например, Redis использует двусторонние связанные списки, сжатые списки, целочисленные наборы, таблицы переходов и словари; конечно, это в основном потому, что Redis предоставляет богатый интерфейс для внешнего мира.Помимо хеширования, существуют также такие функции, как списки, наборы и упорядоченные наборы;
2. Верхний предел объекта
Общий атрибут локального кеша, как правило, кеш будет иметь значение по умолчанию, такое как 1024, которое указывается по умолчанию, если пользователь не указывает его; когда кэшированные данные достигают указанного максимального значения, требуется соответствующая стратегия для очистки лишние данные из кеша, что включает в себя стратегию удаления, которая будет описана ниже;
3. Клиринговая стратегия
Стратегии очистки сцены, используемые после верхнего предела объекта, следующие: LRU (наименее недавно использованный), FIFO (первым пришел первый обслужен), LFU (наименее часто используемый в последнее время), SOFT (мягкая ссылка), WEAK (слабый ссылка);
LRU: Аббревиатура наименее недавно использованных используется наименее недавно и удаляет объекты, которые не использовались в течение длительного времени; это обычно реализуется с использованием LinkedHashMap, который также является стратегией по умолчанию, используемой многими локальными кэшами;
FIFO: первый пришел, первый ушел, удаление объектов в том порядке, в котором они попали в кеш; обычно это реализуется с помощью очереди Queue;
LFU: Аббревиатура Least Frequently Used, вероятно, также означает наименее использовавшийся в последнее время, что немного похоже на LRU; разница в том, что правило исключения LRU основано на времени доступа, а LFU основано на количестве посещений; достигается с помощью HashMap и записи количества посещений;
SOFT: Мягкие ссылки удаляют объекты на основе состояния сборщика мусора и правил мягких ссылок, для достижения которых обычно используется SoftReference;
WEAK: слабые ссылки удаляют объекты более агрессивно в зависимости от состояния сборщика мусора и правил слабых ссылок, обычно реализуемых с помощью WeakReference;
4. Срок годности
Установите время истечения срока действия, чтобы кэшированные данные автоматически удалялись по истечении указанного времени, существуют две общие стратегии удаления данных с истекшим сроком действия: пассивное удаление и активное удаление;
пассивное удаление: каждый раз, когда вы выполняете операцию get/put, она проверяет, истек ли срок действия текущего ключа, и, если срок его действия истек, удаляет его, как в следующем коде:
if (System.currentTimeMillis() - lastClear > clearInterval) {
clear();
}
Активно удалить: в фоновом режиме выполняется специальное задание, регулярно проверяющее, истек ли срок действия данных, и удаляющее их, если оно истекает, что может эффективно обрабатывать холодные данные;
5. Безопасность потоков
Попробуйте использовать потокобезопасные классы для хранения данных, например, используя ConcurrentHashMap вместо HashMap, или предоставьте соответствующие классы обработки синхронизации, например Mybatis предоставляет SynchronizedCache:
public synchronized void putObject(Object key, Object object) {
...省略...
}
@Override
public synchronized Object getObject(Object key) {
...省略...
}
6. Лаконичный интерфейс
Предоставьте часто используемые методы get, put, remove, clear, getSize, такие как интерфейс Cache Mybatis:
public interface Cache {
String getId();
void putObject(Object key, Object value);
Object getObject(Object key);
Object removeObject(Object key);
void clear();
int getSize();
ReadWriteLock getReadWriteLock();
}
Давайте взглянем на интерфейс Cache, предоставляемый guava, который относительно лаконичен:
public interface Cache<K, V> {
V getIfPresent(@CompatibleWith("K") Object key);
V get(K key, Callable<? extends V> loader) throws ExecutionException;
ImmutableMap<K, V> getAllPresent(Iterable<?> keys);
void put(K key, V value);
void putAll(Map<? extends K, ? extends V> m);
void invalidate(@CompatibleWith("K") Object key);
void invalidateAll(Iterable<?> keys);
void invalidateAll();
long size();
CacheStats stats();
ConcurrentMap<K, V> asMap();
void cleanUp();
}
7. Он стойкий?
Преимущество сохранения заключается в том, что данные в файле могут быть загружены снова после перезапуска, что аналогично горячей загрузке; например, ehcache предоставляет функцию сохранения кэша диска и сохранения кэшированных данных в файле . файл данных;
diskPersistent="false" //是否持久化磁盘缓存
Redis доводит функцию постоянства до крайности, и постепенно она немного напоминает базу данных, она предоставляет два метода постоянства, AOF и RDB, конечно, во многих случаях эти два метода можно использовать вместе;
8. Механизм блокировки
В дополнение к тому, что я увидел BlockingCache в Mybatis для реализации этой функции, когда я наблюдал **>**, был реализован идеальный кеш. Примерный код выглядит следующим образом:
public class Memoizerl<A, V> implements Computable<A, V> {
private final Map<A, Future<V>> cache = new ConcurrentHashMap<A, Future<V>>();
private final Computable<A, V> c;
public Memoizerl(Computable<A, V> c) {
this.c = c;
}
@Override
public V compute(A arg) throws InterruptedException, ExecutionException {
while (true) {
Future<V> f = cache.get(arg);
if (f == null) {
Callable<V> eval = new Callable<V>() {
@Override
public V call() throws Exception {
return c.compute(arg);
}
};
FutureTask<V> ft = new FutureTask<V>(eval);
f = cache.putIfAbsent(arg, ft);
if (f == null) {
f = ft;
ft.run();
}
try {
return f.get();
} catch (CancellationException e) {
cache.remove(arg, f);
}
}
}
}
}
вычисления — это очень трудоемкий метод, поэтому результат вычисления здесь кэшируется, но есть проблема, что если два потока одновременно входят в этот метод, как сделать так, чтобы вычисление выполнялось только один раз. здесь используется метод putIfAbsent ConcurrentHashMap, и одновременно будет записана только одна FutureTask;
Суммировать
В этой статье в общих чертах рассказывается, какие моменты необходимо учитывать при проектировании локального кеша: структура данных, верхний предел объекта, стратегия очистки, время истечения срока действия, безопасность потоков, механизм блокировки, практический интерфейс, является ли он постоянным; конечно, должны быть и другие соображения, добро пожаловать добавить .