
VK Tech открывает исходный код коннектора Tarantool Java EE и переименовывает его в Tarantool Java SDK. Дистрибутив уже доступен в Maven Central, что упростит интеграцию с Java-приложениями в корпоративных средах.
Меня зовут Артём Дубинин, я старший программист в VK Tech. Я разрабатываю коннекторы к Tarantool, а также участвую в разработке различных продуктов в VK Tech. Однажды я понял, что материала про совместную работу Java и Tarantool очень мало и из-за этого может казаться, что совмещать их сложно. Но на самом деле это не так — и иногда альтернативы Tarantool могут быть даже сложнее во взаимодействии. Поэтому я и решил написать эту статью.
Статья рассчитана больше на практический подход. В ней присутствуют две практики:
В первой практике мы будем работать с единичным хранилищем Tarantool и сохранять разные структуры данных Java в Tarantool с использованием низкоуровневого API.
Во второй практике у нас уже будет кластерный Tarantool. А на стороне Java мы будем показывать, как легко перейти от использования Redis Spring в использование Tarantool Spring.
Цель статьи — показать через код, что Tarantool реально совместить с Java без больших полотен кода, а итеративно с небольшими сниппетами (не считая java импортов и xml ?). Мы будем использовать такие, чтобы каждый мог попробовать провести у себя на компьютере эти эксперименты и понять, что Tarantool легок в использовании.
Что такое Tarantool
Tarantool — это многопротокольная in-memory база данных. На ее основе можно создавать высоконагруженные отказоустойчивые хранилища и серверы приложений, чтобы логика лежала рядом с данными.
Второе главное достоинство Tarantool, кроме способности обрабатывать высокую нагрузку, — универсальность применения. Про это у нас есть отдельная статья на Хабре.

С той статьи поменялось то, что теперь Tarantool можно применять для обработки гибридной нагрузки между OLTP и OLAP.
Основные возможности Tarantool:
Хранение данных в оперативной памяти.
Гарантированная сохранность данных.
Оптимизация хранения с помощью сжатия.
Поддержка разных моделей данных: key-value, документарная БД, классические таблицы.
Поддержка вторичных индексов.
Гибкая настройка и управление схемой данных в кластерном хранилище данных.
Поддержка CRUD-операций для работы с объектами и данными в кластерном хранилище.
API для чтения и записи данных из бизнес-приложений на Java, Python, Go и C.
Управление временем жизни данных.
Синхронная и асинхронная репликация.
Сегментирование (шардирование) данных.
Поддержка справочников в кластере.
Возможно, вы спросите: «Зачем использовать Tarantool, если есть Redis?» Мы провели подробное сравнение этих инструментов. Короткий ответ: Redis в некоторых вещах может быть сложнее или у него может отсутствовать нужная функциональность.
Запустим для дальнейшей практики одиночный экземпляр Tarantool в Docker прямо на своем компьютере.
# docker
docker run -p 3301:3301 -it tarantool/tarantool
Вместо Docker возможно сделать нативный запуск в Linux или на MacOS:
# or native on macos:
brew install tarantool && tarantool -i
# or native on debian like:
curl -L https://tarantool.io/release/3/installer.sh | bash
apt-get -y install tarantool && tarantool -i
Для разнообразия во второй практике мы не будем использовать Doсker и запустим Tarantool напрямую в операционной системе.
Tarantool (single instance) + Java
Tarantool — это NoSQL, то есть not only SQL. Поэтому механизм подключения из Java к Tarantool ближе к Redis и Mongo, чем к традиционным СУБД.

Для подключения из Java используется драйвер (коннектор). Работает он по следующей схеме:

Коннектор позволяет из Java обмениваться с базой данных запросами и ответами, как в HTTP. Но отличие в том, что запросы идут на сервер базы данных по некоему протоколу. Например, по запросу getUser(id=1) мы получим пользователя с идентификатором 1. При этом базы данных NoSQL часто поддерживают мультиплексирование запросов: мы можем посылать запросы и получать ответы в Java с помощью CompletableFuture, но не дожидаться этого события.
Давайте подробнее посмотрим на хранение структур Java в Tarantool и попрактикуемся. Для этого понадобится собрать простой проект, а затем запустить практический код в JShell, который будет делать запросы в Tarantool, поднятый нами ранее.
Практика с использованием одиночного экземпляра Tarantool будет состоять в том, что мы поймем, как сохранять структуры данных Java в Tarantool с помощью API-коннектора.
Действуйте по инструкции:
0. Необходимо иметь предустановленный JDK.
Данная практика запускалась на openjdk 25.
1. Запустите Tarantool командой docker (если не запустили ранее).
docker run -p 3301:3301 -it tarantool/tarantool
Пример вывода:
started
2025-12-11 08:42:29.735 [1] main/104/interactive main.cc:508 I> Tarantool 3.5.1-0-ge6380801429 Linux-x86_64-RelWithDebInfo
2025-12-11 08:42:29.737 [1] main/104/interactive main.cc:510 I> log level 5 (INFO)
2025-12-11 08:42:29.737 [1] main/104/interactive gc.c:132 I> wal/engine cleanup is paused
2025-12-11 08:42:29.738 [1] main/104/interactive tuple.c:411 I> mapping 268435456 bytes for memtx tuple arena...
2025-12-11 08:42:29.738 [1] main/104/interactive memtx_engine.cc:1995 I> Actual slab_alloc_factor calculated on the basis of desired slab_alloc_factor = 1.044274
2025-12-11 08:42:29.739 [1] main/104/interactive tuple.c:411 I> mapping 134217728 bytes for vinyl tuple arena...
2025-12-11 08:42:29.746 [1] main/104/interactive box.cc:2477 I> update replication_synchro_quorum = 1
2025-12-11 08:42:29.746 [1] main/104/interactive box.cc:3563 I> The option replication_synchro_queue_max_size will actually take effect after the recovery is finished
2025-12-11 08:42:29.747 [1] main/104/interactive box.cc:5755 I> instance uuid 6dadb05a-2535-492b-be29-3bc651d87305
2025-12-11 08:42:29.748 [1] main/104/interactive evio.c:284 I> tx_binary: bound to 0.0.0.0:3301
2025-12-11 08:42:29.750 [1] main/104/interactive memtx_engine.cc:901 I> initializing an empty data directory
2025-12-11 08:42:29.787 [1] main/104/interactive replication.cc:576 I> assigned id 1 to replica 6dadb05a-2535-492b-be29-3bc651d87305
2025-12-11 08:42:29.787 [1] main/104/interactive replication.cc:594 I> assigned name instance-001 to replica 6dadb05a-2535-492b-be29-3bc651d87305
2025-12-11 08:42:29.787 [1] main/104/interactive box.cc:2477 I> update replication_synchro_quorum = 1
2025-12-11 08:42:29.788 [1] main/104/interactive alter.cc:4201 I> replicaset uuid b9598c71-4af7-4ec0-a5c1-5e381538b524
2025-12-11 08:42:29.788 [1] main/104/interactive alter.cc:4214 I> replicaset name: replicaset-001
2025-12-11 08:42:29.791 [1] snapshot/101/main memtx_engine.cc:1303 I> saving snapshot `/var/lib/tarantool/sys_env/default/instance-001/00000000000000000000.snap.inprogress'
2025-12-11 08:42:29.796 [1] snapshot/101/main memtx_engine.cc:1466 I> done
2025-12-11 08:42:29.798 [1] main/104/interactive box.cc:684 I> leaving waiting_for_own_rows mode
2025-12-11 08:42:29.798 [1] main/104/interactive box.cc:6274 I> ready to accept requests
2025-12-11 08:42:29.798 [1] main/108/gc gc.c:320 I> wal/engine cleanup is resumed
2025-12-11 08:42:29.798 [1] main/104/interactive/box.load_cfg load_cfg.lua:992 I> set 'custom_proc_title' configuration option to "tarantool - instance-001"
2025-12-11 08:42:29.798 [1] main/104/interactive/box.load_cfg load_cfg.lua:992 I> set 'instance_name' configuration option to "instance-001"
2025-12-11 08:42:29.798 [1] main/104/interactive/box.load_cfg load_cfg.lua:992 I> set 'log_nonblock' configuration option to false
2025-12-11 08:42:29.798 [1] main/104/interactive/box.load_cfg load_cfg.lua:992 I> set 'replicaset_name' configuration option to "replicaset-001"
2025-12-11 08:42:29.799 [1] main/104/interactive/box.load_cfg load_cfg.lua:992 I> set 'listen' configuration option to ["0.0.0.0:3301"]
2025-12-11 08:42:29.799 [1] main/104/interactive/box.load_cfg load_cfg.lua:992 I> set 'replication' configuration option to []
2025-12-11 08:42:29.799 [1] main/109/checkpoint_daemon gc.c:613 I> scheduled next checkpoint for Thu Dec 11 10:12:14 2025
2025-12-11 08:42:29.799 [1] main/104/interactive box.cc:446 I> box switched to rw
2025-12-11 08:42:29.800 [1] main/104/interactive/box.load_cfg load_cfg.lua:992 I> set 'metrics' configuration option to {"labels":{"alias":"instance-001"},"include":["all"],"exclude":[]}
2025-12-11 08:42:29.807 [1] main main.cc:1083 I> entering the event loop
2. Создайте простой проект.
mvn archetype:generate -DgroupId=com.example -DartifactId=java-skeleton-app-with-tarantool-lib -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false
3. Добавим клиент в секцию dependencies
cd java-skeleton-app-with-tarantool-lib
patch <<'EOL'
--- java-skeleton-app-with-tarantool-lib/pom.xml 2025-11-11 16:10:28
+++ java-skeleton-app-with-tarantool-lib/pom.xml 2025-11-11 16:10:27
@@ -9,6 +9,11 @@
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
+ <groupId>io.tarantool</groupId>
+ <artifactId>tarantool-client</artifactId>
+ <version>1.5.0</version>
+ </dependency>
+ <dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
EOL
Примечание: код из шагов 2 и 3 можно склонировать из Github с помощью Git, если шаги 2, 3 не удались:
Скрытый текст
git clone git@github.com:ArtDu/java-skeleton-app-with-tarantool-lib.git
cd java-skeleton-app-with-tarantool-libt4. Соберите и запустите приложение Hello world, чтобы удостовериться, что коннектор установлен корректно.
mvn package && mvn exec:java -Dexec.mainClass="com.example.App"
5. Создайте файл со всеми classpath-зависимостями для использования в jshell.
mvn dependency:build-classpath -Dmdep.outputFile=classpath.txt
6. Запустите jshell с classpath-файлом.
jshell --class-path $(cat classpath.txt):target/classes
7. Импортируйте необходимые классы для демонстрации.
jshell> import io.tarantool.client.*;
import io.tarantool.client.factory.*;
import io.tarantool.schema.*;
import io.tarantool.mapping.*;
import io.tarantool.client.box.options.*;
import com.fasterxml.jackson.annotation.*;
Примечание: на меньших версиях JDK, возможно, понадобятся дополнительные импорты.
8. Создайте клиент с параметрами по умолчанию и сразу убедитесь, что клиент работает и подключается к Tarantool. Сделайте ping к серверу.
jshell> var client = TarantoolFactory.box().build();
client ==> io.tarantool.client.factory.TarantoolBoxClientImpl@5812f68b
jshell> var f = client.ping();
f ==> java.util.concurrent.CompletableFuture@6dab9b6d[Not completed]
jshell> f.join();
$12 ==> true
Примечание: метод box() для фабрики указывает на то, что мы работаем с Tarantool как с единичным хранилищем (не кластером), и название метода отсылает на нативное API Tarantool. https://www.tarantool.io/ru/doc/latest/reference/reference_lua/box/
Это самое низкоуровневое API, которое позволяет максимально эффективно работать с протоколом tarantool – IPROTO.
9. Для того чтобы начать работать с Tarantool как с хранилищем, создайте спейс (аналог таблицы в классических СУБД).
Так как мы запустили Tarantool в самом его базовом виде, без дополнительных библиотек, то необходимо самим создавать спейсы.
Примечание: в кластером решений Tarantool DB встроен механизм миграций.
jshell> client.eval("""
return box.schema.create_space('person'):create_index('pk')
""").join();
$13 ==> TarantoolResponse(data = [{unique=true, parts=[{fieldno=1, sort_order=asc, type=unsigned, exclude_null=false, is_nullable=false}], hint=true, id=0, type=TREE, space_id=512, name=pk}], formats = {})
Этой командой мы создадим спейс (имя спейса person) и с индексом по умолчанию (с именем индекса pk), где первое поле обязано быть integer.
Примечание: в будущем в репозиторий можно занести обертку в Java, чтобы не писать данный lua-код напрямую.
Нашу практику сохранения структур данных начнем с линейной структуры данных — списка.
10. Сохраните List в Tarantool.
Раз мы не указали схему спейса, теоретически мы можем вставить любую последовательность элементов в Tarantool. Tarantool хранит таплы (аналог записи (raw) в классических СУБД) как массивы, и мы можем сохранить любую последовательность элементов из Java в список и положить в Tarantool. Для этого мы можем воспользоваться методом space для получения API к конкретному спейсу. В нашем случае это будет space person и метод insert для вставки списка в space.
jshell> var space = client.space("person");
space ==> io.tarantool.client.factory.TarantoolBoxSpaceImpl@48e92c5c
jshell> space.insert(
Arrays.asList(1, "Artyom", Instant.now())
).join();
$15 ==> Tuple(formatId = null,
data = [1, Artyom, 2025-12-11T09:02:22.493557Z],
format = [])
Далее мы можем получить наш тапл для проверки с помощью метода select, который по умолчанию принимает значения первичного ключа.
В таком случае мы можем:
jshell> space.select(1).join();
$16 ==> SelectResponse(
data = [
Tuple(formatId = null,
data = [1, Artyom, 2025-12-11T09:02:22.493557Z],
format = []
)
],
position = null, format = {}
)
Примечание: в Box API доступно множество методов взаимодействия с Tarantool. Вот некоторые из jshell autocomplete.
jshell> space.
delete( equals( getBalancer() getClass()
getFetcher() getPool() hashCode() insert(
insertObject( notify() notifyAll() replace(
replaceObject( select( toString() update(
upsert( wait(
jshell> client.
call( close() equals(
eval( getBalancer() getClass()
getFetcher() getPool() getServerVersion()
getType() hashCode() isClosed()
notify() notifyAll() ping(
space( toString() unwatch(
wait( watch( watchOnce(
Это базовые методы взаимодействия через низкоуровневый API Box.
Если мы не хотим иметь бесформатный спейс, то мы можем зафиксировать формат:
jshell> client.eval("""
box.space.person:format({
{'id', 'integer'},
{'name', 'string'},
{'createdAt', 'datetime'},
{'metadata', 'map', is_nullable = true}
})
""").join();
$17 ==> TarantoolResponse(data = [], formats = {})
Далее попробуем вставить вторую запись с некорректным форматом и получим ошибку:
jshell> space.insert(
Arrays.asList(2, 3)
).join();
| Exception java.util.concurrent.CompletionException: io.tarantool.core.exceptions.BoxError: BoxError{code=39, message='Tuple field 3 (createdAt) required by space format is missing', stack=[BoxErrorStackItem{type='ClientError', line=1109, file='./src/box/tuple_format.c', message='Tuple field 3 (createdAt) required by space format is missing', errno=0, code=39, details={"name":"FIELD_MISSING","field":"3 (createdAt)","tuple":[2,3],"space":"person","space_id":512}}]}
...
Вставим запись с корректным форматом, чтобы убедиться, что после создания формата вставка все еще работает:
jshell> space.insert(
Arrays.asList(2, "Ivan", Instant.now())
).join();
$20 ==> Tuple(formatId = null,
data = [2, Ivan, 2025-12-11T09:13:06.720347Z],
format = [])
11. Сохраните Map в Tarantool.
Tarantool хранит таплы отдельно от их формата. Низкоуровневый API box в Java коннекторе также требует, чтобы таплы были в плоском виде. Поэтому список — самая нативная структура, подходящая к низкоуровневому API.
Но как же нам все-таки сохранить структуру данных Map, как мы и задумали?
Здесь существует несколько вариантов.
Сохранять Map в поле тапла.
Преобразовывать Map, чтобы в итоге получилась линейная структура.
Воспользуемся первым вариантом и сохраним Map в поле, которое мы назвали map.
jshell> space.insert(
Arrays.asList(
3, "Dima", Instant.now(),
Map.of("child-uuid", UUID.randomUUID(), "age", 8)
)
).join();
$21 ==> Tuple(formatId = null,
data = [
3, Dima, 2025-12-11T09:18:21.149449Z,
{child-uuid=23de9a18-10a8-4281-9c7b-1ff4e9a022dd, age=8}
],
format = [])
Второй вариант по своей сути аналогичен вставке списка.
jshell> space.insert(
Map.of(
"id", 4,
"name", "Nikolay",
"createdAt", Instant.now()
).values()
).join();
| Exception java.util.concurrent.CompletionException: io.tarantool.core.exceptions.BoxError: BoxError{code=23, message='Tuple field 2 (name) type does not match one required by operation: expected string, got extension', stack=[BoxErrorStackItem{type='ClientError', line=1056, file='./src/box/tuple_format.c', message='Tuple field 2 (name) type does not match one required by operation: expected string, got extension', errno=0, code=23, details={"name":"FIELD_TYPE","field":"2 (name)","expected":"string","actual":"extension","tuple":[4,(4,0x-12-743a69000038-65fe0000),"Nikolay"],"space":"person","space_id":512}}]}
Но он просто так не сработает, так как важен порядок полей, а Map.of(...).values() нам возвращает не в том порядке, в котором мы задали формат в Tarantool.
jshell> var map = Map.of(
"id", 4,
"name", "Nikolay",
"createdAt", Instant.now()
)
$26 ==> {id=4, createdAt=2025-12-11T09:24:44.669049Z, name=Nikolay}
Поэтому необходимо соблюдать тот же порядок, что и был задан в формате спейса.
Например, мы можем воспользоваться API для работы со схемой данных Tarantool. Особенно это полезно, если формат спейса вы задаете не из java.
Получим формат спейса person.
jshell> TarantoolSchemaFetcher fetcher = client.getFetcher();
Space spaceInfo = fetcher.getSpace("person");
List<Field> tupleFormat = spaceInfo.getFormat();
fetcher ==> io.tarantool.schema.TarantoolSchemaFetcher@1bb564e2
spaceInfo ==> Space{id=512, owner=0, name='person', engine='mem ... , parts=[[0, unsigned]]}}}
tupleFormat ==> [Field{name='id', type='integer', isNullable=null ... nt=null, foreignKey=null}]
Теперь мы можем преобразовать Map в нужный формат с учетом правильности формата в Tarantool.
jshell> var person = tupleFormat.stream()
.map(field -> map.get(field.getName()))
.collect(Collectors.toList());
person ==> [4, Nikolay, 2025-12-11T10:40:56.166007Z, null]
jshell> space.insert(
person
).join();
$31 ==> Tuple(formatId = null,
data = [4, Nikolay, 2025-12-11T10:40:56.166007Z, null],
format = [])
Этот вариант не совсем про то, чтобы сохранять Map, а скорее о том, как преобразовать в список.
Для упрощения работы со схемой можно написать более высокоуровневый клиент, где вышеописанные преобразования будут выполняться внутри функции insert(Map.of(...))
12. Сохраните POJO в Tarantool.
Аналогично Map, POJO можно сохранять двумя способами: POJO как тапл или POJO как поле тапла.
Мы воспользуемся только первым способом, чтобы показать, что библиотека Jackson (которая используется в коннекторе) позволяет одной аннотацией преобразовывать весь POJO в список, без дополнительного кода.
Сохраняя правильный порядок полей в определении класса, определим POJO:
jshell> @JsonFormat(shape = JsonFormat.Shape.ARRAY)
@JsonIgnoreProperties(ignoreUnknown = true) // for example bucket_id for return
public class Person {
public Integer id;
public String name;
public Instant createdAt;
public Map metadata;
public Person() {
}
public Person(Integer id, String name, Instant createdAt, Map metadata) {
this.id = id;
this.name = name;
this.createdAt = createdAt;
this.metadata = metadata;
}
@Override
public String toString() {
return "Person{" +
"id=" + id +
", name='" + name + '\'' +
", createdAt=" + createdAt +
", metadata=" + metadata +
'}';
}
}
| created class Person
Сделаем вставку объекта этого класса:
jshell> space.insert(
new Person(
5, "Sasha", Instant.now(),
Map.of(
"description", "The youngest teammate"
)
)).join();
$33 ==> Tuple(formatId = null,
data = [5, Sasha, 2025-12-11T11:04:06.062687Z, {description=The youngest teammate}],
format = [])
13. Получите ответы.
Мы ранее вызывали метод select и получали ответы. Но какой тип данных всегда был в ответе?
Как мы и говорили, нативно Tarantool оперирует плоскими структурами.
Поэтому в ответе на select-запрос был список списков (можно обозначить как List<List<?>>), где первый список — это множество записей, а второй список — это уже сама запись.
jshell> space.select(
Arrays.asList(),
SelectOptions.builder()
.withLimit(10)
.build()
).join(); // таким образом можно вернуть все записи, которые мы сделали
$34 ==> SelectResponse(
data = [
Tuple(formatId = null,
data = [1, Artyom, 2025-12-11T11:42:47.923437Z], format = []),
Tuple(formatId = null,
data = [2, Ivan, 2025-12-11T09:13:06.720347Z], format = []),
Tuple(formatId = null,
data = [3, Dima, 2025-12-11T09:18:21.149449Z, {child-uuid=23de9a18-10a8-4281-9c7b-1ff4e9a022dd, age=8}], format = []),
Tuple(formatId = null,
data = [4, Nikolay, 2025-12-11T10:40:56.166007Z, null], format = []),
Tuple(formatId = null,
data = [5, Sasha, 2025-12-11T11:04:06.062687Z, {desription=The youngest teammate}], format = [])
], position = null, format = {})
Но мы можем воспользоваться перегруженными методами, которые позволяют, например, задать возвратный класс, чтобы работать с более удобной нам структурой.
Например, задав возвратный класс Person, мы получим список Person (List<Person>) как ответ:
jshell> space.select(
Arrays.asList(),
SelectOptions.builder()
.withLimit(10)
.build(),
Person.class
).join();
$35 ==> SelectResponse(
data = [
Tuple(formatId = null,
data = Person{id=1, name='Artyom', createdAt=2025-12-11T11:42:47.923437Z, metadata=null}, format = []),
Tuple(formatId = null,
data = Person{id=2, name='Ivan', createdAt=2025-12-11T09:13:06.720347Z, metadata=null}, format = []),
Tuple(formatId = null,
data = Person{id=3, name='Dima', createdAt=2025-12-11T09:18:21.149449Z, metadata={child-uuid=23de9a18-10a8-4281-9c7b-1ff4e9a022dd, age=8}}, format = []),
Tuple(formatId = null,
data = Person{id=4, name='Nikolay', createdAt=2025-12-11T10:40:56.166007Z, metadata=null}, format = []),
Tuple(formatId = null,
data = Person{id=5, name='Sasha', createdAt=2025-12-11T11:04:06.062687Z, metadata={desription=The youngest teammate}}, format = [])
], position = null, format = {})
Tarantool (cluster) + Spring Data
Tarantool как кластер можно запустить несколькими способами. Можно воспользоваться продуктом Tarantool DB, в котором из коробки есть механизм миграций, UI и множество других функций. Либо можно попробовать запустить кластер, собрав библиотеки самому. Для того чтобы все могли попробовать выполнить эту практику на своем компьютере, мы выберем второй вариант.
Запустим наш кластерный Tarantool:
1. Скачайте tt (CLI для работы с Tarantool).
# macos
brew install tt
# debian like linux
sudo apt-get install tt
# rhel like linux
sudo yum install tt
2. Создайте приложение из шаблона и запустим.
tt init
tt create vshard_cluster --name tarantool_crud_cluster
• Creating application in "/tmp/tarantool_crud_cluster"
• Using built-in 'vshard_cluster' template.
Bucket count (default: 3000):
Storage replication sets count (default: 2):
Storage replicas per replication set count (>=2) (default: 2):
Routers count (default: 1):
• Application 'tarantool_crud_cluster' created successfully
What's next?
Build and start 'tarantool_crud_cluster' application:
$ tt build tarantool_crud_cluster
$ tt start tarantool_crud_cluster
Pay attention that default passwords were generated,
you can change it in the config.yaml.
3. Добавьте библиотеку tarantool/crud, чтобы работать по crud API с кластером, и подключите библиотеку как роль.
patch <<'EOL'
--- tarantool_crud_cluster/tarantool_crud_cluster-scm-1.rockspec 2025-12-12 11:07:10
+++ tarantool_crud_cluster/tarantool_crud_cluster-scm-1.rockspec 2025-12-12 11:07:29
@@ -4,7 +4,7 @@
url = '/dev/null',
}
dependencies = {
- 'vshard == 0.1.25'
+ 'crud == 1.6.1-1'
}
build = {
type = 'none';
EOL
patch -l <<'EOL'
--- tarantool_crud_cluster/config.yaml 2025-12-12 11:05:59
+++ tarantool_crud_cluster/config.yaml 2025-12-12 11:25:30
@@ -26,6 +26,8 @@
module: storage
sharding:
roles: [storage]
+ roles:
+ - roles.crud-storage
replication:
failover: manual
replicasets:
@@ -56,6 +58,8 @@
module: router
sharding:
roles: [router]
+ roles:
+ - roles.crud-router
replicasets:
router-001:
instances:
EOL
4. Добавьте спейс такой же, как был в первой практике.
cat >> tarantool_crud_cluster/storage.lua <<EOL
box.once('person', function()
local space_name = 'person'
local space = box.schema.space.create(space_name, { if_not_exists = true })
space:format({
{'id', 'integer'},
{'name', 'string'},
{'createdAt', 'datetime'},
{'metadata', 'map', is_nullable = true},
{'bucket_id', 'unsigned'},
})
space:create_index('pk', { parts = {'id'}, if_not_exists = true})
space:create_index('bucket_id', { parts = {'bucket_id'}, unique = false, if_not_exists = true})
end)
EOL
Примечание: если не получилось выполнить шаг 3, можно склонировать готовый код
git clone git@github.com:ArtDu/tarantool_crud_cluster.git
5. Соберите кластер и запустите
Примечание: не забудьте отключить tarantool из первой практики, чтобы случайно не был занят порт по умолчанию
tt build tarantool_crud_cluster
tt start tarantool_crud_cluster
Пример вывода команды tt start:
• Starting an instance [tarantool_crud_cluster:storage-002-a]...
• Starting an instance [tarantool_crud_cluster:storage-002-b]...
• Starting an instance [tarantool_crud_cluster:router-001-a]...
• Starting an instance [tarantool_crud_cluster:storage-001-a]...
• Starting an instance [tarantool_crud_cluster:storage-001-b]...В коннекторе Tarantool Java SDK реализовано упрощенное взаимодействие и интеграция в экосистему Spring. Зачем это нужно:
унификация API;
ускорение разработки;
фичи экосистемного инструмента (реактивщина);
стандартизация.
В Spring есть абстрактный интерфейс, который позволяет легко менять базы данных в любой последовательности. Для этого достаточно поменять буквально несколько строк кода.
Эта практика будет заключаться в том, что мы создадим простое приложение, которое будет писать в Redis, а затем изменим его минимально, чтобы оно писало уже в наш поднятый кластерный Tarantool.
1. Запустите Redis server нативно.
# macos:
brew install redis
brew services start redis
# linux with apt dep manager:
sudo apt install redis-server
sudo systemctl start redis-server
2. Создайте проект клиентского приложения.
Можно склонировать готовое приложение из Github:
git clone git@github.com:ArtDu/spring-from-redis-to-tarantool.git
cd spring-from-redis-to-tarantool
Или нужно будет создать пустой проект и добавить все изменения в ручную.
Если вы выбрали склонировать приложение, то сразу переходите к шагу 9.
3. Сгенерируйте пустой проект:
Скрытый текст
mvn archetype:generate -DgroupId=com.example -DartifactId=spring-from-redis-to-tarantool -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false
cd spring-from-redis-to-tarantool4. Добавьте зависимости в pom.xml:
Скрытый текст
patch <<'EOL'
--- pom.xml 2025-12-12 12:38:46
+++ pom.xml 2025-12-12 11:55:58
@@ -14,5 +14,15 @@
<version>3.8.1</version>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter</artifactId>
+ <version>3.2.12</version>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-data-redis</artifactId>
+ <version>3.2.12</version>
+ </dependency>
</dependencies>
</project>
EOL5. Добавьте application.yaml в вашу среду redis/tarantool:
Скрытый текст
mkdir src/main/resources
cat > src/main/resources/application.yaml <<EOL
spring:
data:
redis:
host: localhost
port: 6379
EOL6. Добавьте Person POJO:
Скрытый текст
cat > src/main/java/com/example/Person.java <<EOL
package com.example;
import org.springframework.data.annotation.Id;
import org.springframework.data.keyvalue.annotation.KeySpace;
import org.springframework.data.redis.core.RedisHash;
import org.springframework.data.redis.core.index.Indexed;
import java.time.Instant;
import java.util.Map;
@KeySpace("person")
@RedisHash("person")
public class Person {
@Id
public Integer id;
@Indexed
public String name;
public Instant createdAt;
public Map metadata;
public Person(Integer id, String name, Instant createdAt, Map metadata) {
this.id = id;
this.name = name;
this.createdAt = createdAt;
this.metadata = metadata;
}
public Person() {
}
@Override
public String toString() {
return "Person{" +
"id=" + id +
", name='" + name + '\'' +
", createdAt=" + createdAt +
", metadata=" + metadata +
'}';
}
}
EOL
7. Добавьте Person Repository:
Скрытый текст
cat > src/main/java/com/example/PersonRepository.java <<EOL
package com.example;
import org.springframework.data.repository.CrudRepository;
import java.util.List;
public interface PersonRepository extends CrudRepository<Person, Integer> {
List<Person> findByName(String name);
}
EOL
8. Измените основной класс Java App:
Скрытый текст
cat > src/main/java/com/example/App.java <<EOL
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
@SpringBootApplication
@EnableRedisRepositories
public class App {
public static void main(String[] args) {
App.bootstrap(args);
}
public static ApplicationContext bootstrap(String[] args) {
return SpringApplication.run(App.class, args);
}
}
EOL
9. Создайте classpath для использования в jshell.
mvn clean package dependency:build-classpath -Dmdep.outputFile=classpath.txt
10. Запустите jshell с classpath.
jshell --class-path $(cat classpath.txt):target/classes
11. Повзаимодействуйте с redis.
Загрузите зависимости и запустите spring:
jshell> import com.example.*;
import java.time.*;
var context = App.bootstrap(new String[] {})
. ____ _ __ _ _
/\\ / ___'_ __ _ ()_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.2.12)
...
Используя repository API, сохраним POJO в Redis и сделаем поиск этого объекта:
jshell> var rep = context.getBean(PersonRepository.class);
rep ==> org.springframework.data.keyvalue.repository.supp ... eyValueRepository@480b57e2
jshell> rep.save(
new Person(1, "Yakov", Instant.now(), Map.of("has_dog", true))
);
$5 ==> Person{
id=1, name='Yakov',
createdAt=2025-12-12T09:33:09.845512Z, metadata={has_dog=true}
}
jshell> rep.findById(1);
$6 ==> Optional[
Person{
id=1, name='Yakov',
createdAt=2025-12-12T09:33:09.845512Z, metadata={has_dog=true}
}
]
Также можно проверить с помощью redis-cli, существует ли запись.
127.0.0.1:6379> keys *
1) "person:1"
2) "person"
3) "person:1:idx"
4) "person:name:Yakov"
12. Поменяйте БД в нашем приложении с Redis на Tarantool.
Удалим зависимость на Redis и добавим зависимость на Tarantool:
patch <<'EOL'
--- pom.xml 2025-12-12 11:55:58
+++ pom.xml 2025-12-12 15:38:49
@@ -20,9 +20,9 @@
<version>3.2.12</version>
</dependency>
<dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-redis</artifactId>
- <version>3.2.12</version>
+ <groupId>io.tarantool</groupId>
+ <artifactId>tarantool-spring-data-32</artifactId>
+ <version>1.5.0</version>
</dependency>
</dependencies>
</project>
EOL
Добавим аннотаций над POJO Person для Tarantool и удалим аннотации, нужные для Redis:
patch <<'EOL'
--- src/main/java/com/example/Person.java 2025-12-12 12:32:16
+++ src/main/java/com/example/Person.java 2025-12-12 13:00:49
@@ -1,19 +1,19 @@
package com.example;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import org.springframework.data.annotation.Id;
import org.springframework.data.keyvalue.annotation.KeySpace;
-import org.springframework.data.redis.core.RedisHash;
-import org.springframework.data.redis.core.index.Indexed;
import java.time.Instant;
import java.util.Map;
+@JsonFormat(shape = JsonFormat.Shape.ARRAY)
+@JsonIgnoreProperties(ignoreUnknown = true) // for example bucket_id
@KeySpace("person")
-@RedisHash("person")
public class Person {
@Id
public Integer id;
- @Indexed
public String name;
public Instant createdAt;
public Map metadata;
EOL
В точку запуска скажем Spring, чтобы использовал репозитории Tarantool вместо репозиториев Redis:
patch <<'EOL'
--- src/main/java/com/example/App.java 2025-12-12 12:41:55
+++ src/main/java/com/example/App.java 2025-12-12 12:54:48
@@ -3,10 +3,10 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
-import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
+import io.tarantool.spring.data32.repository.config.EnableTarantoolRepositories;
@SpringBootApplication
-@EnableRedisRepositories
+@EnableTarantoolRepositories
public class App {
public static void main(String[] args) {
App.bootstrap(args);
EOL
Добавим в конфиг путь и параметры подключения до Tarantool:
patch <<'EOL'
--- src/main/resources/application.yaml 2025-12-12 11:56:57
+++ src/main/resources/application.yaml 2025-12-12 15:23:37
@@ -1,6 +1,8 @@
spring:
data:
- redis:
- host: localhost
- port: 6379
+ tarantool:
+ host: localhost
+ port: 3305
+ user-name: client
+ password: secret
EOL
13. Пора взаимодействовать с Tarantool.
Соберем и запустим jshell:
mvn clean package dependency:build-classpath -Dmdep.outputFile=classpath.txt && jshell --class-path $(cat classpath.txt):target/classes
Загрузите зависимости и запустите spring:
jshell> import com.example.*;
import java.time.*;
var context = App.bootstrap(new String[] {})
. ____ _ __ _ _
/\\ / ___'_ __ _ ()_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.2.12)
...
Используя repository API, сохраним POJO в Tarantool-кластер и сделаем поиск этого объекта точно так же, как мы делали с Redis:
jshell> var rep = context.getBean(PersonRepository.class);
rep ==> io.tarantool.spring.data32.repository.support.TarantoolSimpleRepository@1386313f
jshell> rep.save(
new Person(1, "Yakov", Instant.now(), Map.of("has_dog", true))
);
$24 ==> Person{
id=1, name='Yakov',
createdAt=2025-12-12T12:48:58.090115Z, metadata={has_dog=true}
}
jshell> rep.findById(1);
$25 ==> Optional[
Person{
id=1, name='Yakov',
createdAt=2025-12-12T12:48:58.090115Z, metadata={has_dog=true}
}
]
На самом деле, spring repository обладает обширным API взаимодействия с базой данных:
jshell> rep.
count() delete( deleteAll( deleteAllById(
deleteById( equals( existsById( findAll()
findAllById( findById( findByName( getClass()
hashCode() notify() notifyAll() save(
saveAll( toString() wait(
Например, в PersonRepository мы объявляли производный метод, реализацию которого Spring подставил самостоятельно:
jshell> rep.findByName("Yakov")
$26 ==> [Person{id=1, name='Yakov', createdAt=2025-12-12T12:48:58.090115Z, metadata={has_dog=true}}]
В рамках Spring-приложения мы можем воспользоваться Crud API напрямую:
jshell> import io.tarantool.client.crud.*;
TarantoolCrudClient client = context.getBean(TarantoolCrudClient.class);
client ==> io.tarantool.client.factory.TarantoolCrudClientImpl@30db5536
Примечание: Spring Repository по умолчанию использует tarantool/crud API для работы с кластерным Tarantool.
jshell> client.
call( close() equals( eval(
getBalancer() getClass() getPool() getType()
hashCode() isClosed() notify() notifyAll()
ping( space( toString() unwatch(
wait( watch( watchOnce(
jshell> var space = client.space("person");
space ==> io.tarantool.client.factory.TarantoolCrudSpaceImpl@136ccbfe
jshell> space.
count( delete( equals(
get( getBalancer() getClass()
getPool() hashCode() insert(
insertMany( insertObject( insertObjectMany(
len( max( min(
notify() notifyAll() replace(
replaceMany( replaceObject( replaceObjectMany(
select( toString() truncate(
update( upsert( upsertMany(
upsertObject( upsertObjectMany( wait(
И точно так же мы можем получить запись, как мы делали и с Repository API:
jshell> space.get(1).join();
$11 ==> Tuple(formatId = null,
data = [1, Yakov, 2025-12-12T12:48:58.090115Z,
{has_dog=true}, 477]
, format = []
)
Заключение
Надеюсь, мне удалось показать с практической точки зрения применение Tarantool и доказать, что это далеко не так сложно, как могло показаться. Дополнительно можно почитать статьи про создание коннекторов: Руководство по построению коннекторов к СУБД на примере Tarantool и Современный клиент к NoSQL-базе данных.
Ну и конечно, призываю вас воспользоваться репозиторием, теперь уже с практическим пониманием. Дополнительно выкладываю к нему документацию, чтобы изучить детали и нюансы.