Константин Боенко
Ведущий разработчик ГК Юзтех
В последнее время всё большую популярность набирает Tarantool — платформа in-memory вычислений с гибкой схемой данных, включающая в себя NoSQL-базу данных и сервер приложений. В этой статье я хочу рассказать об одной из его реализаций — Tarantool Data Grid (TDG).
Что такое Tarantool Data Grid?
Tarantool Data Grid (TDG) — это, по сути, три компонента в одной коробке:
-
Сам Tarantool
-
Cartridge
-
UI
Прежде чем работать c TDG, необходимо его настроить и сконфигурировать. Настройка осуществляется в UI и включает в себя настройку кластера Tarantool (собственно, это именно то, что делает Cartridge). Затем нужно загрузить в TDG набор файлов конфигурации, включающий в себя:
1. Модель (файл model.avsc) – обязательно;
2. Собственно файл конфигурации TDG (файл config.yml) – обязательно;
3. Пользовательские lua-скрипты – при необходимости.
Основой для работы с TDG является модель — местный эквивалент схемы данных реляционных БД. Модель TDG представляет собой avro-схему примерно следующего вида:
[ { "name": "User", "type": "record", "fields": [ { "name": "user_id", "type": "long" }, { "name": "name", "type": "string" }, { "name": "age", "type": [ "null", "long" ] }, { "name": "sex", "type": [ "null", "boolean" ] } ], "indexes": [ "user_guid" ] }, { "name": "Address", "type": "record", "fields": [ { "name": "address_guid", "type": "string" }, { "name": "city", "type": "string" }, { "name": "street", "type": "string" }, { "name": "house", "type": "string" }, { "name": "apartment", "type": [ "null", "long" ] } ], "indexes": [ "address_guid" ] } ]
Как видно, модель представляет собой avro-описание спейсов Tarantool, в которых будут храниться данные соответствующих типов. Спейсы будут созданы автоматически при загрузке модели в TDG. В нашем случае это спейсы User и Address.
У каждого спейса заданы индексы (в данном примере это guid). Но по желанию или необходимости индексы могут быть и составными. Для этого можно в модели написать конструкцию вида:
"indexes": [ { "name": "primary", "parts": [ “field_1”, "field_2", … ] } ]
Но модель — это далеко не всё. Обязательным элементом конфигурации TDG является корректно заполненный файл config.yml, который должен быть «отдан» TDG вместе с моделью. Заполнение config.yml — отдельная тема, всестороннее рассмотрение которой заслуживает отдельной статьи. Здесь мы ограничимся лишь отдельным примером, о котором я расскажу немного позже.
Для взаимодействия с TDG из приложений, разработанных на различных языках, существуют т.н. коннекторы. Есть коннектор и для Java + Spring, представляющий собой реализацию спецификации JPA — spring-data-tarantool. Для его подключения в файле pom.xml напишем его зависимость:
<dependency> <groupId>io.tarantool</groupId> <artifactId>spring-data-tarantool</artifactId> <version>0.5.2</version> </dependency>
Пользоваться библиотекой довольно просто (особенно, если вы уже имели дело со Spring Data JPA).
Как обычно, описываем классы сущностей (сейчас ограничимся сущностью User из нашей модели):
@Tuple(“user”) public class User { @Id @Field("user_id") String id; String name; Integer age; @Field("sex") Boolean gender; }
Как видите, аннотация @Field повешена не на все поля: в соответствии с принципами JPA маппинг этих полей будет производиться на основе полей класса сущности — а они совпадают с соответствующими полями модели. Там же, где имена полей не совпадают, ставим аннотацию.
Пишем репозиторий:
public interface UserRepository extends CrudRepository<User, Long> { }
И всё, теперь мы можем обращаться к Tarantool так, как обычно и делаем, используя Spring Data JPA и стандартные методы jpa-репозитория. Например:
@Service public class MyService { private final UserRepository repository; @Autowired public MyService(UserRepository repository) { this.repository = repository; } public void myMethod() { User user = new User(); user.setName("Alexey Petrov"); user.setAge(28); user.setGender(true) User saved = repository.save(user); List<User> userList = repository.findAll(); } }
Поставка TDG включает в себя коробочный модуль repository, в котором имеются реализации стандартных CRUD-методов. Методы JPA обращаются именно к ним. Мы также имеем возможность обратиться к методам модуля repository непосредственно. Для этого будем использовать аннотацию @Query. Например, для записи в спейс User мы можем использовать следующий метод:
@Query(function = "repository.put") List<User> put(String typeName, User entity, Map<String, ?> options, List<?> context, Map<?, ?> credentials);
И тогда для записи очередного пользователя мы вызовем метод примерно так:
User myUser = …. // вызов метода создания юзера List<User> saved = repository.put(“user”, myUser, new HashMap<>(), new HashMap<>())
Но что, если нам этого мало, и требуется какая-то дополнительная кастомная логика? И при этом желательно чтобы всё происходило в рамках одного обращения к TDG, а не нескольких? Здесь нам помогут функции, написанные на языке lua (именно этот язык выступает в качестве основного языка запросов Tarantool) и хранимые в TDG. Как уже говорилось выше, такие lua-скрипты загружаются в TDG вместе с конфигурационными файлами.
Например, мы написали некую функцию (на всякий случай уточню: в lua это называется именно «функции», а не «методы») do_something, и поместили её в файл-модуль my_module.lua:
local log = require('log') local function do_something(m) log.info(‘Function do_something is called! M = ’ + m) end return { do_something = do_something }
Теперь мы хотим вызывать её в нашем Java-сервисе. Как это сделать?
Для этого нужно произвести дополнительное конфигурирование TDG. Чтобы дать возможность вызывать нашу функцию извне, в файле config.yml напишем следующее:
services: do_something: doc: "test function" function: my_module.do_something return_type: any args: m: string
Тем самым мы указываем, что снаружи можно вызвать из TDG сервис do_something. При его вызове будет вызвана функция do_something из файла my_module.lua, и эта функция принимает на вход строку, а возвращает что угодно (в нашем случае — ничего).
За вызов сервисов в TDG отвечает встроенный метод call_service. Воспользуемся им. В Java-сервисе в репозиторий добавим вот такой метод:
@Query(function = "call_service") List<T> callService(String name, Map<String, ?> args, Map<?, ?> options);
И теперь в нужном месте мы можем вызвать наш сервис следующим образом:
public void callTDGService(String str) { Map<String, String> args = new HashMap<>(); args.put(“m”, “hello, TDG!”) service.callService("do_something", args, new HashMap<>()); }
Но и это еще не всё. TDG позволяет осуществить последовательный «конвейерный» вызов, и, таким образом, одномоментно выполнять такие действия, как приём, обработка, сохранение и репликация входных данных. Для этого в TDG существует функция tarantool_protocol_process. Вызывается она точно так же, как и любая другая функция TDG. Пишем в репозитории метод:
@Query(function = "tarantool_protocol_process") String process(RawUser user, Map<?, ?> options);
Предположим, что мы хотим отправлять в TDG инстанс некоего класса RawUser, там в соответствии с какой-то логикой преобразовывать его в уже знакомый нам класс User, сохранять его в соответствующий спейс, после чего реплицировать эти данные в имеющееся у нас резервное хранилище (например, БД PostgreSQL). Здесь-то нам и поможет функция tarantool_protocol_process. Она принимает наш объект и помещает его в конвейерную очередь, где он будет обрабатываться согласно тому, как мы эту очередь сконфигурируем. А значит, приступим к конфигурации. Снова открываем наш файл config.yml и напишем там следующее:
connector: input: - name: tarantool_protocol type: tarantool_protocol routing_key: user output: - name: user type: dummy input_processor: handlers: - key: user function: my_module.create_user_from_raw_user storage: - key: user type: User output_processor: user: handlers: - function: pg_replicator.save_user outputs: - user odbc: - dsn: DRIVER=/usr/pgsql-13/lib/psqlodbc.so;SERVER=<url>;PORT=<port>;DATABASE=<db-name>;UID=<user>;PWD=<password>; name: postgres
Сначала мы настроили коннекторы для получения (input) и для возвращения (output) данных. Здесь есть очень важный момент – routing_key. Именно по нему очередь будет отличать ваши данные от всех остальных. Этот ключ можно задать самим первым действием, когда объект попадет в очередь. Но мы ограничимся тем, который будет ему назначен по умолчанию (в нашем случае это user).
Далее конфигурируем input_processor, т.е. действия, которые будут производиться с объектом на этапе получения. В секции handlers описано, что при получении данных с ключом user будет вызвана функция create_user_from_raw_user в модуле my_module.lua. Представим, что мы написали такую функцию, и она возвращает объект типа User (в соответствии с загруженной в TGD моделью).
Далее попадаем в секцию storage. В ней говорится, что объект с ключом user будет сохранен в спейc User. При этом, если такой записи в спейсе нет, она будет добавлена. Если она уже есть, то она будет обновлена.
Далее следует конфигурирование output_processor – действий, которые будут совершены с объектом перед удалением из очереди. В нашем случает это репликация во внешнюю систему – БД PostgreSQL. Мы описываем, что при попадании в output_processor данный с ключом user будет вызвана функция save_user в модуле pg_replicator.lua. А чтобы она отработала корректно, мы должны сконфигурировать подключение к PostrgeSQL в секции odbc.
Листинг файла pg_replicator.lua local odbc = require(‘odbc’) local function save_user(user) local res, err = odbc.execute(‘postgres’, “insert into my_scheme.user(name, age, sex) values (?, ?, ?)”, {user.name, user.age, user.sex}) return res end
Вот так всё просто.
ссылка на оригинал статьи https://habr.com/ru/company/usetech/blog/713592/
Добавить комментарий