1. Обзор
В этом уроке мы увидим, как мы можем использовать карту хроник для хранения пар ключ-значение. Мы также создадим короткие примеры, чтобы продемонстрировать его поведение и использование.
2. Что такое карта хроники?
Следуя документации, «Chronicle Map — это сверхбыстрое, хранящееся в памяти, неблокирующее хранилище ключей и значений, предназначенное для приложений с малой задержкой и/или многопроцессорных приложений».
В двух словах, это хранилище ключей-значений вне кучи. Карта не требует большого объема оперативной памяти для корректной работы. Он может увеличиваться в зависимости от доступной емкости диска . Кроме того, он поддерживает репликацию данных в конфигурации с несколькими главными серверами.
Давайте теперь посмотрим, как мы можем настроить и работать с ним.
3. Зависимость от Maven
Для начала нам нужно добавить в наш проект зависимость Chronicle- Map:
<dependency>
<groupId>net.openhft</groupId>
<artifactId>chronicle-map</artifactId>
<version>3.17.2</version>
</dependency>
4. Типы карт хроник
Мы можем создать карту двумя способами: либо как карту в памяти, либо как постоянную карту.
Давайте посмотрим на оба из них в деталях.
4.1. Карта в памяти
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()
. Это говорит карте настроить среднее количество байтов, занимаемых значениями записи карты.
Другими словами, при создании карты Карта хроник определяет среднее количество байтов, занимаемых сериализованной формой значений. Он делает это путем сериализации заданного среднего значения с помощью настроенных маршалеров значений. Затем он выделяет определенное количество байтов для значения каждой записи карты.
Одна вещь, которую мы должны отметить, когда дело доходит до карты в памяти, заключается в том, что данные доступны только тогда, когда процесс 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()
— значение, из которого определяется среднее количество байтов, выделяемых для значения записи карты.AverageValueSize()
— среднее количество байтов, выделяемых для значения записи карты.ConstantValueSizeBySample()
— количество байтов, которое будет выделено для значения записи карты, когда размер значения всегда одинаков.AverageKey()
— ключ, из которого определяется среднее количество байтов, выделяемых для ключа записи карты.AverageKeySize()
— среднее количество байтов, выделяемых для ключа записи карты.ConstantKeySizeBySample()
— количество байтов, которое будет выделено для ключа записи карты, когда размер ключа всегда одинаков.
6. Типы ключей и значений
Существуют определенные стандарты, которым мы должны следовать при создании карты хроники, особенно при определении ключа и значения. Карта работает лучше всего, когда мы создаем ключ и значение, используя рекомендуемые типы.
Вот некоторые из рекомендуемых типов:
- `
Интерфейсы
значений` - Любой класс, реализующий интерфейс
Byteable
из Chronicle Bytes - Любой класс, реализующий интерфейс
BytesMarshallable
из Chronicle Bytes; класс реализации должен иметь общедоступный конструктор без аргументов байт[]
ибайтбуфер
CharSequence
,String
иStringBuilder
Целое
,длинное
идвойное
- Любой класс, реализующий
java.io.Externalizable
; класс реализации должен иметь общедоступный конструктор без аргументов - Любой тип, реализующий
java.io.Serializable
, включая примитивные типы в штучной упаковке (кроме перечисленных выше) и типы массивов. - Любой другой тип, если предоставлены пользовательские сериализаторы.
7. Запрос карты хроники
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 .