Привет, Хабр! Меня зовут Кирилл Курдюков, я разработчик YDB. Ранее мы рассматривали интеграцию YDB c Liquibase, теперь поговорим о результатах поддержки инструмента Flyway для управления миграциями схемы данных YDB.
Введение
Flyway — это инструмент для миграций схем баз данных с открытым исходным кодом. Он имеет расширения для различных систем управления базами данных (СУБД), включая YDB. Является таким же широко используемым и популярным инструментом для управления миграциями схем базы данных, как и Liquibase.
Flyway и Liquibase определяют, какие изменения уже были применены к базе данных, а какие еще не были. Для этого они ведут журнал выполненных миграций в самой базе данных.
Они используют распределенную блокировку для обеспечения последовательности исполнения и целостности данных. Это важно, когда множество экземпляров приложения работает одновременно и может попытаться выполнить новые миграции параллельно. При отсутствии блокировки такая ситуация могла бы привести к несогласованности в базе данных.
Распределенная блокировка гарантирует, что только одна миграция может быть выполнена в любой момент времени.
Как использовать Flyway вместе с YDB
Чтобы использовать Flyway вместе с YDB в Java / Kotlin приложении или в Gradle / Maven плагинах, требуется установить зависимость расширения Flyway для YDB и YDB JDBC Driver:
Maven:
<!-- Set actual versions -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
<version>${flyway.core.version}</version>
</dependency>
<dependency>
<groupId>tech.ydb.jdbc</groupId>
<artifactId>ydb-jdbc-driver</artifactId>
<version>${ydb.jdbc.version}</version>
</dependency>
<dependency>
<groupId>tech.ydb.dialects</groupId>
<artifactId>flyway-ydb-dialect</artifactId>
<version>${flyway.ydb.dialect.version}</version>
</dependency>
Gradle:
dependencies {
// Set actual versions
implementation "org.flywaydb:flyway-core:$flywayCoreVersion"
implementation "tech.ydb.dialects:flyway-ydb-dialect:$flywayYdbDialecVersion"
implementation "tech.ydb.jdbc:ydb-jdbc-driver:$ydbJdbcVersion"
}
Для работы с YDB через Flyway CLI требуется установить утилиту flyway
любым из рекомендованных способов.
Затем Flyway нужно расширить диалектом YDB и JDBC-драйвером:
# install flyway
# cd $(which flyway) // prepare this command for your environment
cd libexec
# set actual versions .jar files
cd drivers && curl -L -o ydb-jdbc-driver-shaded-2.1.0.jar https://repo.maven.apache.org/maven2/tech/ydb/jdbc/ydb-jdbc-driver-shaded/2.1.0/ydb-jdbc-driver-shaded-2.1.0.jar
cd ../lib && curl -L -o flyway-ydb-dialect.jar https://repo.maven.apache.org/maven2/tech/ydb/dialects/flyway-ydb-dialect/1.0.0-RC0/flyway-ydb-dialect-1.0.0-RC0.jar
Управление миграциями с помощью Flyway
baseline
Команда baseline инициализирует Flyway в существующей базе данных, исключая все миграции вплоть до baselineVersion
включительно.
Предположим, что мы имеем существующий проект с текущей схемой базы данных:
Запишем наши существующие миграции следующим образом (таблицы взяты из примера):
db/migration:
V1__create_series.sql
V2__create_seasons.sql
V3__create_episodes.sql
Установим baselineVersion = 3
, затем выполним следующую команду:
flyway -url=jdbc:ydb:grpc://localhost:2136/local -locations=db/migration -baselineVersion=3 baseline
Результатом исполнения будет созданная таблица flyway_schema_history
с baseline
записью:
migrate
Команда migrate обновляет схему базы данных до последней версии. Если таблица истории схемы не была создана, то Flyway создаст ее автоматически.
Добавим к предыдущему примеру миграцию загрузки данных:
db/migration:
V1__create_series.sql
V2__create_seasons.sql
V3__create_episodes.sql
V4__load_data.sql
Применим последнюю миграцию следующей командой:
flyway -url=jdbc:ydb:grpc://localhost:2136/local -locations=db/migration migrate
Результатом исполнения будет загрузка данных в таблицы series
, seasons
и episodes
:
Обновим схему путем добавления вторичного индекса:
db/migration:
V1__create_series.sql
V2__create_seasons.sql
V3__create_episodes.sql
V4__load_data.sql
V5__create_series_title_index.sql
Содержимое файла V5__create_series_title_index.sql
:
ALTER TABLE `series` ADD INDEX `title_index` GLOBAL ON (`title`);
Применим последнюю миграцию следующей командой:
flyway -url=jdbc:ydb:grpc://localhost:2136/local -locations=db/migration migrate
Результатом будет появление вторичного индекса у таблицы series
:
info
Команда info печатает подробные сведения и информацию о состоянии всех миграций.
Добавим еще одну миграцию, которая переименовывает раннее добавленный вторичный индекс:
db/migration:
V1__create_series.sql
V2__create_seasons.sql
V3__create_episodes.sql
V4__load_data.sql
V5__create_series_title_index.sql
V6__rename_series_title_index.sql
Содержимое файла V6__rename_series_title_index.sql
:
ALTER TABLE `series` RENAME INDEX `title_index` TO `title_index_new`;
После исполнения следующей команды:
flyway -url=jdbc:ydb:grpc://localhost:2136/local -locations=db/migration info
Результатом будет подробная информация о состоянии миграций:
+-----------+---------+---------------------------+----------+---------------------+--------------------+----------+
| Category | Version | Description | Type | Installed On | State | Undoable |
+-----------+---------+---------------------------+----------+---------------------+--------------------+----------+
| Versioned | 1 | create series | SQL | | Below Baseline | No |
| Versioned | 2 | create seasons | SQL | | Below Baseline | No |
| Versioned | 3 | create episodes | SQL | | Ignored (Baseline) | No |
| | 3 | << Flyway Baseline >> | BASELINE | 2024-04-16 12:09:27 | Baseline | No |
| Versioned | 4 | load data | SQL | 2024-04-16 12:35:12 | Success | No |
| Versioned | 5 | create series title index | SQL | 2024-04-16 12:59:20 | Success | No |
| Versioned | 6 | rename series title index | SQL | | Pending | No |
+-----------+---------+---------------------------+----------+---------------------+--------------------+----------+
validate
Команда validate проверяет соответствие примененных миграций к миграциям, которые находятся в файловой системе пользователя.
После применения к текущим миграциям следующей команды:
flyway -url=jdbc:ydb:grpc://localhost:2136/local -locations=db/migration validate
В логах будет написано, что последняя миграция не была применена к нашей базе данных:
ERROR: Validate failed: Migrations have failed validation
Detected resolved migration not applied to database: 6.
To fix this error, either run migrate, or set -ignoreMigrationPatterns='*:pending'.
Давайте ее применим, выполнив flyway .. migrate
. Теперь валидация проходит успешно, вторичный индекс переименован.
Далее изменим файл уже ранее примененной миграции V4__load_date.sql
, удалив комментарии в SQL-скрипте.
После исполнения команды валидации, получим закономерную ошибку о том, что checksum
различается в измененной миграции:
ERROR: Validate failed: Migrations have failed validation
Migration checksum mismatch for migration version 4
-> Applied to database : 591649768
-> Resolved locally : 1923849782
repair
Команда repair пытается устранить выявленные ошибки и расхождения с таблицей историей схемы базы данных.
Устраним проблему с разными checksum
, выполнив следующую команду:
flyway -url=jdbc:ydb:grpc://localhost:2136/local -locations=db/migration repair
Результатом будет обновление колонки checksum
в таблице flyway_schema_history
у записи, отвечающей за миграцию V4__load_data.sql
:
После восстановления таблицы лога валидация проходит успешно.
Также с помощью команды repair
можно удалить неудавшийся DDL-скрипт.
clean
Команда clean удаляет все таблицы в схеме базы данных.
Так как в отличие от других СУБД YDB не имеет такой сущности, как schema
, команда clean
удалит все таблицы в вашей базе данных.
Удалим все таблицы в нашей базе данных следующей командой:
flyway -url=jdbc:ydb:grpc://localhost:2136/local -locations=db/migration -cleanDisabled=false clean
Результатом будет пустая база данных:
Распределенные блокировка в Flyway
При определении новых миграций, которые еще не были применены в базе данных, Flyway берет распределенную блокировку перед их применением.
Здесь нет общепринятого решения, у каждой СУБД свое решение.
В Oracle и Derby:
LOCK TABLE flyway_history_schema IN EXCLUSIVE MODE
В PostgreSQL:
SELECT pg_try_advisory_xact_lock("flyway1")
Для YDB взятие блокировки похоже на подход CockroachDB, Google Spanner или Apache Ignite.
В таблицу истории миграций flyway_schema_history
клиент пытается вставить фиктивную запись c ID = -100:
INSERT INTO flyway_schema_history(installed_rank, version, description) VALUES (-100, $tableLockId, 'flyway-lock')
И кто успел вставить такую запись, тот и лидер.
Отпускается такая блокировка следующей SQL-командой:
DELETE FROM flyway_schema_history FROM installed_rank = -100
Важным отличием от CockroachDB и Google Spanner является то, что мы не проставляем тайм-аут истечения блокировки, а приостанавливаем процесс для расследования DBA, как сделано в Apache Ignite. Так как мы не знаем, в какой момент отказал лидер и в каком состоянии наша база данных.
А еще ScheduledThreadPool не закрывается, он нужен для процесса продления такой блокировки.
Поддержка и контакты
Если вы столкнулись с какой-то проблемой или у вас есть идея по улучшению диалекта YDB, то можно открыть issue в репозитории ydb-java-dialects с тегом flyway
.
Либо приходите обсудить её в публичный Telegram чат YDB.
Документация по Flyway находится по ссылке.
KirillKurdyukov Автор
Распределенные блокировки в Liquibase
Liquibase создает таблицу
DATABASECHANGELOGLOCK
, в которой содержится одна запись.Блокировка для большинства СУБД происходит следующим образом:
В текущем виде этот подход не позволит получить корректно распределенную блокировку для YDB, так как на данный момент UPDATE не возвращает количество обновленных строк. Этот способ становится рабочим, если взятие блокировки делать в транзакции уровня
SERIALIZABLE
.На этом уровне изоляции транзакция фиксирует состояние записи согласно модели MVCC. Если другая транзакция изменяет те же данные, текущая транзакция становится недействительной, поскольку она работала с уже неактуальной версией данных.
Клиенты, которые не получили блокировку пытаются получить ее снова с интервалом 1 секунда.