«1. Обзор

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

2. Что такое карта хроник?

Согласно документации, «Chronicle Map — это сверхбыстрое, хранящееся в памяти, неблокирующее хранилище ключей и значений, предназначенное для приложений с малой задержкой и/или многопроцессорных приложений».

В двух словах, это хранилище ключей-значений вне кучи. Карта не требует большого объема оперативной памяти для корректной работы. Он может увеличиваться в зависимости от доступной емкости диска. Кроме того, он поддерживает репликацию данных в конфигурации с несколькими главными серверами.

Давайте теперь посмотрим, как мы можем настроить и работать с ним.

3. Зависимость Maven

Для начала нам нужно добавить в наш проект зависимость Chronicle-Map:

<dependency>
    <groupId>net.openhft</groupId>
    <artifactId>chronicle-map</artifactId>
    <version>3.17.2</version>
</dependency>

4. Типы карт Chronicle

Мы можем создать карту в двумя способами: либо как карта в памяти, либо как сохраняемая карта.

Давайте посмотрим на оба из них в деталях.

4.1. In-Memory Map

In-memory Chronicle Map — это хранилище карт, созданное в физической памяти сервера. Это означает, что он доступен только в процессе JVM, в котором создано хранилище карт.

Давайте рассмотрим краткий пример:

ChronicleMap<LongValue, CharSequence> inMemoryCountryMap = ChronicleMap
  .of(LongValue.class, CharSequence.class)
  .name("country-map")
  .entries(50)
  .averageValue("America")
  .create();

Для простоты мы создаем карту, которая хранит идентификаторы 50 стран и их названия. Как видно из фрагмента кода, создание довольно простое, за исключением конфигурации mediumValue(). Это говорит карте настроить среднее количество байтов, занимаемых значениями записи карты.

Другими словами, при создании карты Chronicle Map определяет среднее количество байтов, занимаемых сериализованной формой значений. Он делает это путем сериализации заданного среднего значения с помощью настроенных маршалеров значений. Затем он выделяет определенное количество байтов для значения каждой записи карты.

Одна вещь, которую мы должны отметить, когда дело доходит до карты в памяти, заключается в том, что данные доступны только тогда, когда процесс JVM активен. Библиотека очистит данные, когда процесс завершится.

4.2. Сохраняемая карта

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

ChronicleMap<LongValue, CharSequence> persistedCountryMap = ChronicleMap
  .of(LongValue.class, CharSequence.class)
  .name("country-map")
  .entries(50)
  .averageValue("America")
  .createPersistedTo(new File(System.getProperty("user.home") + "/country-details.dat"));

Это создаст файл с именем country-details.dat в указанной папке. Если этот файл уже доступен по указанному пути, то реализация построителя откроет ссылку на существующее хранилище данных из этого процесса JVM.

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

    выжила после процесса создания; например, для поддержки горячего повторного развертывания приложения сделайте его глобальным на сервере; например, для поддержки одновременного доступа к нескольким процессам действуйте как хранилище данных, которое мы будем сохранять на диск

5. Конфигурация размера

Обязательно настройте среднее значение и средний ключ при создании карты хроники, за исключением случай, когда наш тип ключ/значение является либо упакованным примитивом, либо интерфейсом значения. В нашем примере мы не настраиваем средний ключ, поскольку тип ключа LongValue является интерфейсом значения.

Теперь давайте посмотрим, какие есть параметры для настройки среднего количества байтов ключа/значения:

    AverageValue() — значение, из которого среднее количество байтов должно быть выделено для значения записи карты. определяется mediumValueSize() — среднее количество байтов, которое должно быть выделено для значения записи карты. ConstantValueSizeBySample() — количество байтов, которое будет выделено для значения записи карты, когда размер значения всегда тот же среднийKey() — ключ, из которого определяется среднее количество байтов, выделяемых для ключа записи карты. mediumKeySize() — среднее количество байтов, выделяемых для ключа карты. entry ConstantKeySizeBySample() — количество байтов, выделяемых для ключа записи карты, когда размер ключа всегда один и тот же

6. Типы ключей и значений

«Существуют определенные стандарты, которым мы должны следовать при создании карты хроники, особенно при определении ключа и значения. Карта работает лучше всего, когда мы создаем ключ и значение, используя рекомендуемые типы.

Вот некоторые из рекомендуемых типов:

    Интерфейсы значений Любой класс, реализующий интерфейс Byteable из Chronicle Bytes Любой класс, реализующий интерфейс BytesMarshallable из Chronicle Bytes; класс реализации должен иметь общедоступный конструктор без аргументов byte[] и ByteBuffer CharSequence, String и StringBuilder Integer, Long и Double Любой класс, реализующий java.io.Externalizable; класс реализации должен иметь общедоступный конструктор без аргументов Любой тип, реализующий java.io.Serializable, включая упакованные примитивные типы (кроме перечисленных выше) и типы массивов Любой другой тип, если предоставлены пользовательские сериализаторы

7. Запросы к хронике Map

Chronicle Map поддерживает как запросы с одним ключом, так и запросы с несколькими ключами.

7.1. Запросы с одним ключом

Запросы с одним ключом — это операции, которые имеют дело с одним ключом. ChronicleMap поддерживает все операции интерфейса Java Map и интерфейса ConcurrentMap:

LongValue qatarKey = Values.newHeapInstance(LongValue.class);
qatarKey.setValue(1);
inMemoryCountryMap.put(qatarKey, "Qatar");

//...

CharSequence country = inMemoryCountryMap.get(key);

В дополнение к обычным операциям get и put, ChronicleMap добавляет специальную операцию getUsing(), которая уменьшает объем памяти при извлечении и обработке данных. Вход. Давайте посмотрим на это в действии:

LongValue key = Values.newHeapInstance(LongValue.class);
StringBuilder country = new StringBuilder();
key.setValue(1);
persistedCountryMap.getUsing(key, country);
assertThat(country.toString(), is(equalTo("Romania")));

key.setValue(2);
persistedCountryMap.getUsing(key, country);
assertThat(country.toString(), is(equalTo("India")));

Здесь мы использовали один и тот же объект StringBuilder для получения значений разных ключей, передав его методу getUsing(). В основном он повторно использует один и тот же объект для получения разных записей. В нашем случае метод getUsing() эквивалентен:

country.setLength(0);
country.append(persistedCountryMap.get(key));

7.2. Запросы с несколькими ключами

Могут быть случаи, когда нам нужно иметь дело с несколькими ключами одновременно. Для этого мы можем использовать функцию queryContext(). Метод queryContext() создаст контекст для работы с записью карты.

Давайте сначала создадим мультикарту и добавим в нее некоторые значения:

Set<Integer> averageValue = IntStream.of(1, 2).boxed().collect(Collectors.toSet());
ChronicleMap<Integer, Set<Integer>> multiMap = ChronicleMap
  .of(Integer.class, (Class<Set<Integer>>) (Class) Set.class)
  .name("multi-map")
  .entries(50)
  .averageValue(averageValue)
  .create();

Set<Integer> set1 = new HashSet<>();
set1.add(1);
set1.add(2);
multiMap.put(1, set1);

Set<Integer> set2 = new HashSet<>();
set2.add(3);
multiMap.put(2, set2);

Для работы с несколькими записями мы должны заблокировать эти записи, чтобы предотвратить несогласованность, которая может возникнуть из-за одновременного обновления:

try (ExternalMapQueryContext<Integer, Set<Integer>, ?> fistContext = multiMap.queryContext(1)) {
    try (ExternalMapQueryContext<Integer, Set<Integer>, ?> secondContext = multiMap.queryContext(2)) {
        fistContext.updateLock().lock();
        secondContext.updateLock().lock();

        MapEntry<Integer, Set<Integer>> firstEntry = fistContext.entry();
        Set<Integer> firstSet = firstEntry.value().get();
        firstSet.remove(2);

        MapEntry<Integer, Set<Integer>> secondEntry = secondContext.entry();
        Set<Integer> secondSet = secondEntry.value().get();
        secondSet.add(4);

        firstEntry.doReplaceValue(fistContext.wrapValueAsData(firstSet));
        secondEntry.doReplaceValue(secondContext.wrapValueAsData(secondSet));
    }
} finally {
    assertThat(multiMap.get(1).size(), is(equalTo(1)));
    assertThat(multiMap.get(2).size(), is(equalTo(2)));
}

~~ ~ 8. Закрытие карты хроники

Теперь, когда мы закончили работу с нашими картами, давайте вызовем метод close() на наших объектах карты, чтобы освободить память вне кучи и связанные с ней ресурсы:

persistedCountryMap.close();
inMemoryCountryMap.close();
multiMap.close();

~ ~~ Здесь следует иметь в виду, что все операции с картой должны быть завершены до закрытия карты. В противном случае JVM может неожиданно выйти из строя.

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

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

Все рассмотренные здесь примеры можно найти в проекте Github.