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
:
Предоставлено: Датастакс
2.2. Несколько разделов
Пакет, включающий несколько разделов, должен быть хорошо спроектирован, поскольку он требует координации между несколькими узлами. Лучший вариант использования многораздельного пакета — запись одних и тех же данных в две связанные таблицы, т. е. две таблицы с одинаковыми столбцами и разными ключами раздела.
Пакетная операция с несколькими разделами использует механизм пакетного журнала
для обеспечения атомарности . Координационный узел отправляет запросы журналов пакетной обработки на узлы журналов пакетной обработки и, получив подтвержденное получение, выполняет операторы пакетной обработки. Затем он удаляет пакетный журнал
с узлов и отправляет подтверждение клиенту.
Рекомендуется избегать использования пакетных запросов с несколькими разделами. Это связано с тем, что такие запросы создают огромную нагрузку на узел координации и серьезно влияют на его производительность.
Мы должны использовать пакет с несколькими разделами только тогда, когда нет другого жизнеспособного варианта.
На приведенной ниже диаграмме показан поток пакетных запросов на несколько разделов от координационного узла H
к узлам разделов B
, E
и соответствующим узлам репликации C
, D
и F
, G
:
Предоставлено: Датастакс
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
, весь пакет не обрабатывается.
После выполнения вышеуказанного запроса мы получим следующее успешное подтверждение:
Теперь давайте проверим, совпадает ли время записи
данных после пакетного выполнения:
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 .