Перейти к основному содержимому

Cassandra Batch на языке запросов Cassandra и Java

· 9 мин. чтения

1. Обзор

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

Мы рассмотрим пакетную обработку в Cqlsh , а также в приложениях Java.

2. Основы пакетной обработки Кассандры

Распределенная база данных, такая как Cassandra, не поддерживает свойства ACID (атомарность, согласованность, изоляция и долговечность) , в отличие от реляционных баз данных. Тем не менее, в некоторых случаях нам нужно несколько модификаций данных, чтобы они были атомарными и/или изолированными операциями.

Оператор пакетной обработки объединяет несколько операторов языка модификации данных (например, INSERT, UPDATE и DELETE) для достижения атомарности и изоляции при работе с одним разделом или только атомарности при работе с несколькими разделами.

Вот синтаксис пакетного запроса:

BEGIN [ ( UNLOGGED | COUNTER ) ] BATCH
[ USING TIMESTAMP [ epoch_microseconds ] ]
dml_statement [ USING TIMESTAMP [ epoch_microseconds ] ] ;
[ dml_statement [ USING TIMESTAMP [ epoch_microseconds ] ] [ ; ... ] ]
APPLY BATCH;

Давайте рассмотрим приведенный выше синтаксис на примере:

BEGIN BATCH 

INSERT INTO product (product_id, variant_id, product_name)
VALUES (2c11bbcd-4587-4d15-bb57-4b23a546bd7f,0e9ef8f7-d32b-4926-9d37-27225933a5f3,'banana');

INSERT INTO product (product_id, variant_id, product_name)
VALUES (2c11bbcd-4587-4d15-bb57-4b23a546bd7f,0e9ef8f7-d32b-4926-9d37-27225933a5f5,'banana');

APPLY BATCH;

Сначала мы используем оператор BEGIN BATCH без дополнительных параметров, таких как UNLOGGED или USING TIMESTAMP , чтобы инициировать пакетный запрос, а затем включаем все операции DML, т. е. операторы вставки для таблицы продуктов .

Наконец, мы используем оператор APPLY BATCH для выполнения пакета.

Следует отметить, что мы не сможем отменить какой-либо пакетный запрос, поскольку пакетный запрос не поддерживает функцию отката.

2.1. Один раздел

Пакетный оператор применяет все операторы DML в одном разделе, обеспечивая атомарность и изоляцию.

Хорошо спроектированный пакет, нацеленный на один раздел, может уменьшить трафик клиент-сервер и более эффективно обновить таблицу с изменением одной строки. Это связано с тем, что пакетная изоляция происходит только в том случае, если пакетная операция выполняет запись в один раздел.

Пакет с одним разделом также может включать две разные таблицы, имеющие один и тот же ключ раздела и представленные в одном пространстве ключей.

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

На приведенной ниже диаграмме показан поток пакетных запросов одного раздела от узла координации H к узлу раздела B и его узлам репликации C , D :

./dabe19538d942c7fdaf9c6a1c9f30cba.png

Предоставлено: Датастакс

2.2. Несколько разделов

Пакет, включающий несколько разделов, должен быть хорошо спроектирован, поскольку он требует координации между несколькими узлами. Лучший вариант использования многораздельного пакета — запись одних и тех же данных в две связанные таблицы, т. е. две таблицы с одинаковыми столбцами и разными ключами раздела.

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

Рекомендуется избегать использования пакетных запросов с несколькими разделами. Это связано с тем, что такие запросы создают огромную нагрузку на узел координации и серьезно влияют на его производительность.

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

На приведенной ниже диаграмме показан поток пакетных запросов на несколько разделов от координационного узла H к узлам разделов B , E и соответствующим узлам репликации C , D и F , G :

./c8b9fb1b0ef9d219de8cec38c95ae8d9.png

Предоставлено: Датастакс

3. Пакетное выполнение в Cqlsh

Во-первых, давайте создадим таблицу продуктов для выполнения некоторых пакетных запросов:

CREATE TABLE product (
product_id UUID,
variant_id UUID,
product_name text,
description text,
price float,
PRIMARY KEY (product_id, variant_id)
);

3.1. Пакет с одним разделом без метки времени

Мы выполним приведенный ниже пакетный запрос, нацеленный на один раздел таблицы продуктов , и не будем предоставлять метку времени:

BEGIN BATCH 

INSERT INTO product (product_id, variant_id, product_name)
VALUES (2c11bbcd-4587-4d15-bb57-4b23a546bd7f,0e9ef8f7-d32b-4926-9d37-27225933a5f3,'banana') IF NOT EXISTS;

INSERT INTO product (product_id, variant_id, product_name)
VALUES (2c11bbcd-4587-4d15-bb57-4b23a546bd7f,0e9ef8f7-d32b-4926-9d37-27225933a5f5,'banana') IF NOT EXISTS;

UPDATE product SET price = 7.12, description = 'banana v1'
WHERE product_id = 2c11bbcd-4587-4d15-bb57-4b23a546bd7f AND variant_id=0e9ef8f7-d32b-4926-9d37-27225933a5f3;

UPDATE product SET price = 11.90, description = 'banana v2'
WHERE product_id = 2c11bbcd-4587-4d15-bb57-4b23a546bd7f AND variant_id=0e9ef8f7-d32b-4926-9d37-27225933a5f5;

APPLY BATCH;

В приведенном выше запросе используется логика сравнения и установки (CAS), т. е. предложение IF NOT EXISTS , и все такие условные операторы должны возвращать значение true для выполнения пакета. Если какие-либо такие операторы возвращают false , весь пакет не обрабатывается.

После выполнения вышеуказанного запроса мы получим следующее успешное подтверждение:

./608d1fc45f5769052834ba89949a5b1d.png

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

cqlsh:testkeyspace> select product_id, variant_id, product_name, description, price, writetime(product_name) from product;

@ Row 1
-------------------------+--------------------------------------
product_id | 3a043b68-20ee-4ece-8f4b-a07e704bc9f5
variant_id | b84b9366-9998-4b2d-9a96-7e9a59a94ae5
product_name | Banana
description | banana v1
price | 12
writetime(product_name) | 1639275574653000

@ Row 2
-------------------------+--------------------------------------
product_id | 3a043b68-20ee-4ece-8f4b-a07e704bc9f5
variant_id | facc3997-299d-419b-b133-a54b5d4dfc3b
product_name | Banana
description | banana v2
price | 12
writetime(product_name) | 1639275574653000

3.2. Пакет с одним разделом с отметкой времени

Теперь мы увидим примеры пакетных запросов с опцией USING TIMESTAMP для предоставления метки времени в формате времени эпохи , т. е. в микросекундах.

Ниже приведен пакетный запрос, который применяет одну и ту же метку времени ко всем операторам DML:

BEGIN BATCH USING TIMESTAMP 1638810270 

INSERT INTO product (product_id, variant_id, product_name)
VALUES (2c11bbcd-4587-4d15-bb57-4b23a546bd7f,0e9ef8f7-d32b-4926-9d37-27225933a5f3,'banana');

INSERT INTO product (product_id, variant_id, product_name)
VALUES (2c11bbcd-4587-4d15-bb57-4b23a546bd7f,0e9ef8f7-d32b-4926-9d37-27225933a5f5,'banana');

UPDATE product SET price = 7.12, description = 'banana v1'
WHERE product_id = 2c11bbcd-4587-4d15-bb57-4b23a546bd7f AND variant_id=0e9ef8f7-d32b-4926-9d37-27225933a5f3;

UPDATE product SET price = 11.90, description = 'banana v2'
WHERE product_id = 2c11bbcd-4587-4d15-bb57-4b23a546bd7f AND variant_id=0e9ef8f7-d32b-4926-9d37-27225933a5f5;

APPLY BATCH;

Давайте теперь укажем пользовательскую метку времени для любого из отдельных операторов DML:

BEGIN BATCH 

INSERT INTO product (product_id, variant_id, product_name)
VALUES (2c11bbcd-4587-4d15-bb57-4b23a546bd7f,0e9ef8f7-d32b-4926-9d37-27225933a5f3,'banana');

INSERT INTO product (product_id, variant_id, product_name)
VALUES (2c11bbcd-4587-4d15-bb57-4b23a546bd7f,0e9ef8f7-d32b-4926-9d37-27225933a5f5,'banana') USING TIMESTAMP 1638810270;

UPDATE product SET price = 7.12, description = 'banana v1'
WHERE product_id = 2c11bbcd-4587-4d15-bb57-4b23a546bd7f AND variant_id=0e9ef8f7-d32b-4926-9d37-27225933a5f3 USING TIMESTAMP 1638810270;

UPDATE product SET price = 11.90, description = 'banana v2'
WHERE product_id = 2c11bbcd-4587-4d15-bb57-4b23a546bd7f AND variant_id=0e9ef8f7-d32b-4926-9d37-27225933a5f5;

APPLY BATCH;

Теперь мы увидим недопустимый пакетный запрос, который имеет как пользовательскую метку времени, так и логику сравнения и установки (CAS) , т . е. предложение IF NOT EXISTS :

BEGIN BATCH USING TIMESTAMP 1638810270 

INSERT INTO product (product_id, variant_id, product_name)
VALUES (2c11bbcd-4587-4d15-bb57-4b23a546bd7f,0e9ef8f7-d32b-4926-9d37-27225933a5f3,'banana') IF NOT EXISTS;

INSERT INTO product (product_id, variant_id, product_name)
VALUES (2c11bbcd-4587-4d15-bb57-4b23a546bd7f,0e9ef8f7-d32b-4926-9d37-27225933a5f5,'banana') IF NOT EXISTS;

UPDATE product SET price = 7.12, description = 'banana v1'
WHERE product_id = 2c11bbcd-4587-4d15-bb57-4b23a546bd7f AND variant_id=0e9ef8f7-d32b-4926-9d37-27225933a5f3;

UPDATE product SET price = 11.90, description = 'banana v2'
WHERE product_id = 2c11bbcd-4587-4d15-bb57-4b23a546bd7f AND variant_id=0e9ef8f7-d32b-4926-9d37-27225933a5f5;

APPLY BATCH;

Мы получим следующую ошибку при выполнении вышеуказанного запроса:

InvalidRequest: Error from server: code=2200 [Invalid query]
message="Cannot provide custom timestamp for conditional BATCH"

Вышеупомянутая ошибка связана с тем, что временные метки на стороне клиента запрещены для любой условной вставки или обновления.

3.3. Пакетный запрос с несколькими разделами

Лучший вариант использования пакетной обработки в нескольких разделах — вставка точных данных в две связанные таблицы .

Давайте вставим одни и те же данные в таблицы product_by_name и product_by_id , имеющие разные ключи партиций:

BEGIN BATCH 

INSERT INTO product_by_name (product_name, product_id, description, price)
VALUES ('banana',2c11bbcd-4587-4d15-bb57-4b23a546bd7f,'banana',12.00);

INSERT INTO product_by_id (product_id, product_name, description, price)
VALUES (2c11bbcd-4587-4d15-bb57-4b23a546bd7f,'banana','banana',12.00);

APPLY BATCH;

Давайте теперь включим опцию UNLOGGED для вышеуказанного запроса:

BEGIN UNLOGGED BATCH 

INSERT INTO product_by_name (product_name, product_id, description, price)
VALUES ('banana',2c11bbcd-4587-4d15-bb57-4b23a546bd7f,'banana',12.00);

INSERT INTO product_by_id (product_id, product_name, description, price)
VALUES (2c11bbcd-4587-4d15-bb57-4b23a546bd7f,'banana','banana',12.00);

APPLY BATCH;

Приведенный выше пакетный запрос UNLOGGED не гарантирует атомарность или изоляцию и не использует пакетный журнал для записи данных.

3.4. Пакетная обработка обновлений счетчика

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

Давайте создадим таблицу product_by_sales , которая хранит sales_vol как тип данных счетчика :

CREATE TABLE product_by_sales (
product_id UUID,
sales_vol counter,
PRIMARY KEY (product_id)
);

Приведенный ниже пакетный запрос счетчика дважды увеличивает sales_vol на 100:

BEGIN COUNTER BATCH

UPDATE product_by_sales
SET sales_vol = sales_vol + 100
WHERE product_id = 6ab09bec-e68e-48d9-a5f8-97e6fb4c9b47;

UPDATE product_by_sales
SET sales_vol = sales_vol + 100
WHERE product_id = 6ab09bec-e68e-48d9-a5f8-97e6fb4c9b47;

APPLY BATCH

4. Пакетная операция в Java

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

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

Во-первых, нам нужно будет включить зависимости Maven, связанные с DataStax :

<dependency>
<groupId>com.datastax.oss</groupId>
<artifactId>java-driver-core</artifactId>
<version>4.1.0</version>
</dependency>
<dependency>
<groupId>com.datastax.oss</groupId>
<artifactId>java-driver-query-builder</artifactId>
<version>4.1.0</version>
</dependency>

4.2. Пакет с одним разделом

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

Мы создадим пакетный запрос, используя экземпляр BatchStatement . BatchStatement создается с использованием перечисления DefaultBatchType и экземпляров BoundStatement . ``

Во-первых, мы создадим метод для получения экземпляра BoundStatement путем привязки атрибутов продукта к запросу на вставку PreparedStatement :

BoundStatement getProductVariantInsertStatement(Product product, UUID productId) {
String insertQuery = new StringBuilder("")
.append("INSERT INTO ")
.append(PRODUCT_TABLE_NAME)
.append("(product_id, variant_id, product_name, description, price) ")
.append("VALUES (")
.append(":product_id")
.append(", ")
.append(":variant_id")
.append(", ")
.append(":product_name")
.append(", ")
.append(":description")
.append(", ")
.append(":price")
.append(");")
.toString();

PreparedStatement preparedStatement = session.prepare(insertQuery);

return preparedStatement.bind(
productId,
UUID.randomUUID(),
product.getProductName(),
product.getDescription(),
product.getPrice());
}

Теперь мы выполним BatchStatement для созданного выше BoundStatement, используя тот же UUID продукта : ``

UUID productId = UUID.randomUUID();
BoundStatement productBoundStatement1 = this.getProductVariantInsertStatement(productVariant1, productId);
BoundStatement productBoundStatement2 = this.getProductVariantInsertStatement(productVariant2, productId);

BatchStatement batch = BatchStatement.newInstance(DefaultBatchType.UNLOGGED,
productBoundStatement1, productBoundStatement2);

session.execute(batch);

Приведенный выше код вставляет два варианта продукта в один и тот же ключ раздела, используя пакет UNLOGGED .

4.3. Пакет с несколькими разделами

Теперь давайте посмотрим, как вставить одни и те же данные в две связанные таблицы — product_by_id и product_by_name .

Во-первых, мы создадим повторно используемый метод для получения экземпляра BoundStatement для запроса на вставку PreparedStatement :

BoundStatement getProductInsertStatement(Product product, UUID productId, String productTableName) {
String cqlQuery1 = new StringBuilder("")
.append("INSERT INTO ")
.append(productTableName)
.append("(product_id, product_name, description, price) ")
.append("VALUES (")
.append(":product_id")
.append(", ")
.append(":product_name")
.append(", ")
.append(":description")
.append(", ")
.append(":price")
.append(");")
.toString();

PreparedStatement preparedStatement = session.prepare(cqlQuery1);

return preparedStatement.bind(
productId,
product.getProductName(),
product.getDescription(),
product.getPrice());
}

Теперь мы выполним BatchStatement, используя тот же UUID продукта : ``

UUID productId = UUID.randomUUID();

BoundStatement productBoundStatement1 = this.getProductInsertStatement(product, productId, PRODUCT_BY_ID_TABLE_NAME);
BoundStatement productBoundStatement2 = this.getProductInsertStatement(product, productId, PRODUCT_BY_NAME_TABLE_NAME);

BatchStatement batch = BatchStatement.newInstance(DefaultBatchType.LOGGED,
productBoundStatement1,productBoundStatement2);

session.execute(batch);

Это вставит одни и те же данные о продукте в таблицы product_by_id и product_by_name , используя пакет LOGGED .

5. Вывод

В этой статье мы узнали о пакетном запросе Cassandra и о том, как применять его в Cqlsh и Java с помощью BatchStatement .

Как всегда, полный исходный код примеров доступен на GitHub .