«1. Обзор

Apache Geode — это распределенная сетка данных в памяти, поддерживающая кэширование и вычисление данных.

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

2. Настройка

Во-первых, нам нужно загрузить и установить Apache Geode и установить среду gfsh. Для этого мы можем следовать инструкциям в официальном руководстве Geode.

И, во-вторых, этот туториал создаст некоторые артефакты файловой системы. Таким образом, мы можем изолировать их, создав временный каталог и запуская оттуда.

2.1. Установка и настройка

Из нашего временного каталога нам нужно запустить экземпляр Locator:

gfsh> start locator --name=locator --bind-address=localhost

Локаторы отвечают за координацию между различными членами кластера Geode, которым мы можем в дальнейшем управлять через JMX.

Далее, давайте запустим экземпляр сервера для размещения одного или нескольких регионов данных:

gfsh> start server --name=server1 --server-port=0

Мы устанавливаем для параметра —server-port значение 0, чтобы Geode выбирал любой доступный порт. Хотя, если мы опустим его, сервер будет использовать порт по умолчанию 40404. Сервер — это настраиваемый член кластера, который работает как долгоживущий процесс и отвечает за управление областями данных.

И, наконец, нам нужен регион:

gfsh> create region --name=baeldung --type=REPLICATE

Регион — это то место, где мы будем хранить наши данные.

2.2. Проверка

Давайте удостоверимся, что все работает, прежде чем двигаться дальше.

Во-первых, давайте проверим, есть ли у нас наш Сервер и наш Локатор:

gfsh> list members
 Name   | Id
------- | ----------------------------------------------------------
server1 | 192.168.0.105(server1:6119)<v1>:1024
locator | 127.0.0.1(locator:5996:locator)<ec><v0>:1024 [Coordinator]

И затем, что у нас есть наш Регион:

gfsh> describe region --name=baeldung
..........................................................
Name            : baeldung
Data Policy     : replicate
Hosting Members : server1

Non-Default Attributes Shared By Hosting Members  

 Type  |    Name     | Value
------ | ----------- | ---------------
Region | data-policy | REPLICATE
       | size        | 0
       | scope       | distributed-ack

Также у нас должны быть некоторые каталоги в файловой системе под нашим временный каталог с именем «locator» и «server1».

С этим выводом мы знаем, что готовы двигаться дальше.

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

Теперь, когда у нас есть запущенный Geode, давайте начнем смотреть клиентский код.

Чтобы работать с Geode в нашем Java-коде, нам нужно добавить клиентскую библиотеку Apache Geode Java в наш pom:

<dependency>
     <groupId>org.apache.geode</groupId>
     <artifactId>geode-core</artifactId>
     <version>1.6.0</version>
</dependency>

Давайте начнем с простого сохранения и извлечения некоторых данных в нескольких регионах. .

4. Простое хранение и извлечение

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

Чтобы начать хранить данные в нашем регионе «baeldung», подключимся к нему с помощью локатора:

@Before
public void connect() {
    this.cache = new ClientCacheFactory()
      .addPoolLocator("localhost", 10334)
        .create();
    this.region = cache.<String, String> 
      createClientRegionFactory(ClientRegionShortcut.CACHING_PROXY)
        .create("baeldung");
}

4.1. Сохранение отдельных значений

Теперь мы можем просто сохранять и извлекать данные в нашем регионе:

@Test
public void whenSendMessageToRegion_thenMessageSavedSuccessfully() {

    this.region.put("A", "Hello");
    this.region.put("B", "Baeldung");

    assertEquals("Hello", region.get("A"));
    assertEquals("Baeldung", region.get("B"));
}

4.2. Сохранение нескольких значений одновременно

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

@Test
public void whenPutMultipleValuesAtOnce_thenValuesSavedSuccessfully() {

    Supplier<Stream<String>> keys = () -> Stream.of("A", "B", "C", "D", "E");
    Map<String, String> values = keys.get()
        .collect(Collectors.toMap(Function.identity(), String::toLowerCase));

    this.region.putAll(values);

    keys.get()
        .forEach(k -> assertEquals(k.toLowerCase(), this.region.get(k)));
}

4.3. Сохранение пользовательских объектов

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

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

public class CustomerKey implements Serializable {
    private long id;
    private String country;
    
    // getters and setters
    // equals and hashcode
}

И следующий тип значения:

public class Customer implements Serializable {
    private CustomerKey key;
    private String firstName;
    private String lastName;
    private Integer age;
    
    // getters and setters 
}

Есть пара дополнительных шагов, чтобы иметь возможность сохраните их:

Во-первых, они должны реализовать Serializable. Хотя это не является строгим требованием, сделав их сериализуемыми, Geode может хранить их более надежно.

Во-вторых, они должны находиться в пути к классам нашего приложения, а также в пути к классам нашего сервера Geode.

Чтобы поместить их в путь к классам сервера, давайте упакуем их, скажем, с помощью mvn clean package.

И затем мы можем сослаться на полученный jar в новой команде запуска сервера:

gfsh> stop server --name=server1
gfsh> start server --name=server1 --classpath=../lib/apache-geode-1.0-SNAPSHOT.jar --server-port=0

Опять же, мы должны запускать эти команды из временного каталога.

Наконец, давайте создадим новый регион с именем «baeldung-customers» на сервере, используя ту же команду, которую мы использовали для создания региона «baeldung»:

gfsh> create region --name=baeldung-customers --type=REPLICATE

В коде мы протянем к локатору, как и раньше, указав пользовательский тип:

@Before
public void connect() {
    // ... connect through the locator
    this.customerRegion = this.cache.<CustomerKey, Customer> 
      createClientRegionFactory(ClientRegionShortcut.CACHING_PROXY)
        .create("baeldung-customers");
}

И затем мы можем сохранить нашего клиента, как и раньше:

@Test
public void whenPutCustomKey_thenValuesSavedSuccessfully() {
    CustomerKey key = new CustomerKey(123);
    Customer customer = new Customer(key, "William", "Russell", 35);

    this.customerRegion.put(key, customer);

    Customer storedCustomer = this.customerRegion.get(key);
    assertEquals("William", storedCustomer.getFirstName());
    assertEquals("Russell", storedCustomer.getLastName());
}

5. Типы регионов

Для большинства сред у нас будет более одной копии или более одного раздела нашего региона, в зависимости от наших требований к пропускной способности чтения и записи.

До сих пор мы использовали реплицированные регионы в памяти. Давайте посмотрим поближе.

5.1. Реплицированный регион

Как следует из названия, реплицированный регион поддерживает копии своих данных более чем на одном сервере. Давайте проверим это.

«Из консоли gfsh в рабочем каталоге давайте добавим в кластер еще один сервер с именем server2:

gfsh> start server --name=server2 --classpath=../lib/apache-geode-1.0-SNAPSHOT.jar --server-port=0

Помните, что когда мы создавали «baeldung», мы использовали «type=REPLICATE». Из-за этого Geode автоматически реплицирует наши данные на новый сервер.

Давайте проверим это, остановив server1:

gfsh> stop server --name=server1

Затем давайте выполним быстрый запрос к региону «baeldung».

Если данные были реплицированы успешно, мы получим результат:

gfsh> query --query='select e.key from /baeldung.entries e'
Result : true
Limit  : 100
Rows   : 5

Result
------
C
B
A 
E
D

Итак, похоже, репликация прошла успешно!

Добавление реплики в наш регион повышает доступность данных. А поскольку на запросы может отвечать несколько серверов, мы также получим более высокую пропускную способность при чтении.

Но что, если они оба разобьются? Поскольку это области в памяти, данные будут потеряны. Вместо этого мы можем использовать —type=REPLICATE_PERSISTENT, который также сохраняет данные на диске во время репликации.

5.2. Разделенный регион

С большими наборами данных мы можем лучше масштабировать систему, настроив Geode для разделения региона на отдельные разделы или сегменты.

Давайте создадим один разделенный регион с именем «baeldung-partitioned»:

gfsh> create region --name=baeldung-partitioned --type=PARTITION

Добавим некоторые данные:

gfsh> put --region=baeldung-partitioned --key="1" --value="one"
gfsh> put --region=baeldung-partitioned --key="2" --value="two"
gfsh> put --region=baeldung-partitioned --key="3" --value="three"

И быстро проверим:

gfsh> query --query='select e.key, e.value from /baeldung-partitioned.entries e'
Result : true
Limit  : 100
Rows   : 3

key | value
--- | -----
2   | two
1   | one
3   | three

Затем, чтобы убедиться, что данные получены разделены, давайте снова остановим server1 и повторим запрос:

gfsh> stop server --name=server1
gfsh> query --query='select e.key, e.value from /baeldung-partitioned.entries e'
Result : true
Limit  : 100
Rows   : 1

key | value
--- | -----
2   | two

На этот раз мы получили только некоторые записи данных, потому что на этом сервере есть только один раздел данных, поэтому, когда server1 упал, его данные были потеряны.

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

    PARTITION_REDUNDANT разделяет и реплицирует наши данные между различными членами кластера PARTITION_PERSISTENT разделяет данные, как и PARTITION, но на диск, а PARTITION_REDUNDANT_PERSISTENT дает нам все три варианта поведения.

6. Язык объектных запросов

Geode также поддерживает язык объектных запросов или OQL, который может быть более мощным, чем простой поиск по ключу. Это немного похоже на SQL.

Для этого примера воспользуемся созданным ранее регионом «baeldung-customer».

Если мы добавим еще пару клиентов:

Map<CustomerKey, Customer> data = new HashMap<>();
data.put(new CustomerKey(1), new Customer("Gheorge", "Manuc", 36));
data.put(new CustomerKey(2), new Customer("Allan", "McDowell", 43));
this.customerRegion.putAll(data);

Тогда мы сможем использовать QueryService для поиска клиентов с именем «Аллан»:

QueryService queryService = this.cache.getQueryService();
String query = 
  "select * from /baeldung-customers c where c.firstName = 'Allan'";
SelectResults<Customer> results =
  (SelectResults<Customer>) queryService.newQuery(query).execute();
assertEquals(1, results.size());

7. Функция

Одна из самых Мощным представлением о сетках данных в памяти является идея «переноса вычислений в данные».

Проще говоря, поскольку Geode — это чистая Java, нам легко не только отправлять данные, но и выполнять логику над этими данными.

Это может напомнить нам об идее расширений SQL, таких как PL-SQL или Transact-SQL.

7.1. Определение функции

Чтобы определить единицу работы для Geode, мы реализуем интерфейс функции Geode.

Например, давайте представим, что нам нужно изменить все имена клиентов на верхний регистр.

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

public class UpperCaseNames implements Function<Boolean> {
    @Override
    public void execute(FunctionContext<Boolean> context) {
        RegionFunctionContext regionContext = (RegionFunctionContext) context;
        Region<CustomerKey, Customer> region = regionContext.getDataSet();

        for ( Map.Entry<CustomerKey, Customer> entry : region.entrySet() ) {
            Customer customer = entry.getValue();
            customer.setFirstName(customer.getFirstName().toUpperCase());
        }
        context.getResultSender().lastResult(true);   
    }

    @Override
    public String getId() {
        return getClass().getName();
    }
}

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

FunctionContext содержит все данные нашего региона, поэтому мы можем сделать из него более сложный запрос или, как мы сделали здесь, изменить его.

А у Function гораздо больше возможностей, поэтому ознакомьтесь с официальным руководством, особенно с методом getResultSender.

7.2. Развертывание функции

Нам нужно сообщить Geode о нашей функции, чтобы иметь возможность ее запускать. Как и в случае с нашими пользовательскими типами данных, мы упакуем банку.

Но на этот раз мы можем просто использовать команду deploy:

gfsh> deploy --jar=./lib/apache-geode-1.0-SNAPSHOT.jar

7.3. Выполнение функции

Теперь мы можем выполнить функцию из приложения с помощью FunctionService:

@Test
public void whenExecuteUppercaseNames_thenCustomerNamesAreUppercased() {
    Execution execution = FunctionService.onRegion(this.customerRegion);
    execution.execute(UpperCaseNames.class.getName());
    Customer customer = this.customerRegion.get(new CustomerKey(1));
    assertEquals("GHEORGE", customer.getFirstName());
}

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

В этой статье мы изучили основные концепции экосистемы Apache Geode. Мы рассмотрели простые операции get и put со стандартными и пользовательскими типами, реплицированные и секционированные регионы, а также поддержку oql и функций.

И, как всегда, все эти примеры доступны на GitHub.