Кассандра: список 10 самых последних измененных записей

у меня возникли проблемы с моделированием моих данных, чтобы я мог эффективно запрашивать Кассандру для последних 10 (любое число на самом деле) записей, которые были недавно изменены. Каждая запись имеет столбец last_modified_date, который устанавливается приложением при вставке / обновлении записи.

Я исключил столбцы данных из этого примера кода.

основная таблица данных (содержит только одну строку для каждой записи):

CREATE TABLE record (
    record_id int,
    last_modified_by text,
    last_modified_date timestamp,
    PRIMARY KEY (record_id)
);

Решение 1 (Плохо)

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

таблица (одна строка для каждой записи; только вставка последней измененной даты):

CREATE TABLE record_by_last_modified_index (
    record_id int,
    last_modified_by text,
    last_modified_date timestamp,
    PRIMARY KEY (record_id, last_modified_date)
) WITH CLUSTERING ORDER BY (last_modified_date DESC);

запрос:

SELECT * FROM record_by_last_modified_index LIMIT 10

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

решение 2 (Неэффективно)

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

решение 3

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

3 ответов


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

Кассандра сортирует только вещи на основе столбцов кластеризации,но порядок сортировки не ожидается. Это связано с тем, что когда memtables записываются на диск как SSTables (сортированные строковые таблицы), SSTables неизменяемы и не могут быть повторно отсортированы эффективно. Вот почему вам не разрешено обновлять значение столбца кластеризации.

если вы хотите повторно отсортировать кластеризованные строки, единственный способ, который я знаю, - удалить старую строку и вставить новую в пакет. Чтобы сделать это еще более неэффективным, вам, вероятно, нужно сначала сделать чтение, чтобы выяснить, что last_modified_date было для record_id, чтобы вы могли удалить его.

поэтому я бы искал другой подход, например, просто писать обновления как новые кластеризованные строки и оставлять старые там (возможно, очищать их с течением времени с помощью TTL). Так что ваш новый обновления всегда будут сверху, когда вы выполняете запрос LIMIT.

что касается секционирования, вам нужно будет разбить данные на несколько категорий, чтобы распространить данные по узлам. Это означает, что вы не получите глобальную сортировку таблицы, а только внутри категорий, что связано с распределенной моделью. Если вам действительно нужна глобальная сортировка, то, возможно, посмотрите на что-то вроде сопряжения Кассандры с Spark. Сортировка очень дорогая по времени и ресурсам, поэтому подумайте хорошенько, если вы очень нужно.

обновление:

подумав об этом еще немного, вы сможете сделать это в Cassandra 3.0, используя материализованные представления. Представление позаботится о грязном удалении и вставке для вас, чтобы повторно упорядочить кластеризованные строки. Итак, вот как это выглядит в выпуске 3.0 alpha:

сначала создайте базовую таблицу:

CREATE TABLE record_ids (
    record_type int,
    last_modified_date timestamp,
    record_id int,
    PRIMARY KEY(record_type, record_id));

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

CREATE MATERIALIZED VIEW last_modified AS
    SELECT record_type FROM record_ids
    WHERE record_type IS NOT NULL AND last_modified_date IS NOT NULL AND record_id IS NOT NULL
    PRIMARY KEY (record_type, last_modified_date, record_id)
    WITH CLUSTERING ORDER BY (last_modified_date DESC);

теперь вставьте некоторые записи:

insert into record_ids (record_type, last_modified_date, record_id) VALUES ( 1, dateof(now()), 100);
insert into record_ids (record_type, last_modified_date, record_id) VALUES ( 1, dateof(now()), 200);
insert into record_ids (record_type, last_modified_date, record_id) VALUES ( 1, dateof(now()), 300);

SELECT * FROM record_ids;

 record_type | record_id | last_modified_date
-------------+-----------+--------------------------
           1 |       100 | 2015-08-14 19:41:10+0000
           1 |       200 | 2015-08-14 19:41:25+0000
           1 |       300 | 2015-08-14 19:41:41+0000

SELECT * FROM last_modified;

 record_type | last_modified_date       | record_id
-------------+--------------------------+-----------
           1 | 2015-08-14 19:41:41+0000 |       300
           1 | 2015-08-14 19:41:25+0000 |       200
           1 | 2015-08-14 19:41:10+0000 |       100

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

UPDATE record_ids SET last_modified_date = dateof(now()) 
WHERE record_type=1 AND record_id=200;

Итак, в базовой таблице мы видим, что временная метка для record_id=200 была обновлена:

SELECT * FROM record_ids;

 record_type | record_id | last_modified_date
-------------+-----------+--------------------------
           1 |       100 | 2015-08-14 19:41:10+0000
           1 |       200 | 2015-08-14 19:43:13+0000
           1 |       300 | 2015-08-14 19:41:41+0000

и в поле зрения мы видим:

 SELECT * FROM last_modified;

 record_type | last_modified_date       | record_id
-------------+--------------------------+-----------
           1 | 2015-08-14 19:43:13+0000 |       200
           1 | 2015-08-14 19:41:41+0000 |       300
           1 | 2015-08-14 19:41:10+0000 |       100

Итак, вы видите, что record_id=200 переместился в представлении, и если вы сделаете ограничение N на этой таблице, вы получите N самых последних изменений строки.


единственный способ запроса CQL всей таблицы / представления, отсортированного по полю, - сделать ключ раздела постоянным. Ровно одна машина (коэффициент репликации times) будет содержать всю таблицу. Е. Г. с partition INT ключ раздела, который всегда равен нулю, и ключ кластеризации в качестве поля, требующего сортировки. Вы должны наблюдать производительность чтения/записи/емкости, аналогичную одноузловой базе данных с индексом в отсортированном поле, даже если у вас больше узлов в кластере. Это не победить цель Кассандры, потому что она может помочь масштабировать в будущем.

если производительность недостаточна, вы можете решить масштабировать, увеличивая разнообразие разделов. Например. случайный выбор от 0, 1, 2, 3 для вставок будет до четырехкратного чтения/записи / емкости perf, когда используются 4 узла. Затем, чтобы найти "10 самых последних" элементов, вам придется вручную запросить все 4 раздела и объединить-отсортировать результаты.

теоретически Кассандра могла бы предоставить этот объект динамического node-count-max - modulo ключ раздела для вставки и слияния-сортировка для SELECT (с ALLOW FILTERING).

цели дизайна Кассандры запрещают глобальную сортировку

чтобы разрешить запись, чтение и емкость для хранения линейно масштабироваться с подсчетом узлов Cassandra требуется:

  • каждая вставка земли на одном узле.
  • каждый выберите землю на одном узле.
  • клиенты распределяют рабочую нагрузку аналогично между всеми узлами.

Если Я поймите правильно, следствием этого является запрос с одним полем в полной таблице, который всегда требует чтения из всего кластера и сортировки слиянием.

Примечание материализованные представления эквивалентны таблицам, у них нет никаких магических свойств, которые делают их лучше в глобальной сортировке. См.http://www.datastax.com/dev/blog/we-shall-have-order где Аарон Плоец согласен с тем, что cassandra и cql не могут сортировать по одному полю без раздела и масштаб.

Пример Решения

CREATE KEYSPACE IF NOT EXISTS
    tmpsort
WITH REPLICATION =
    {'class':'SimpleStrategy', 'replication_factor' : 1};

USE tmpsort;

CREATE TABLE record_ids (
    partition int,
    last_modified_date timestamp,
    record_id int,
    PRIMARY KEY((partition), last_modified_date, record_id))
    WITH CLUSTERING ORDER BY (last_modified_date DESC);

INSERT INTO record_ids (partition, last_modified_date, record_id) VALUES ( 1, DATEOF(NOW()), 100);
INSERT INTO record_ids (partition, last_modified_date, record_id) VALUES ( 2, DATEOF(NOW()), 101);
INSERT INTO record_ids (partition, last_modified_date, record_id) VALUES ( 3, DATEOF(NOW()), 102);
INSERT INTO record_ids (partition, last_modified_date, record_id) VALUES ( 1, DATEOF(NOW()), 103);
INSERT INTO record_ids (partition, last_modified_date, record_id) VALUES ( 2, DATEOF(NOW()), 104);
INSERT INTO record_ids (partition, last_modified_date, record_id) VALUES ( 3, DATEOF(NOW()), 105);
INSERT INTO record_ids (partition, last_modified_date, record_id) VALUES ( 3, DATEOF(NOW()), 106);
INSERT INTO record_ids (partition, last_modified_date, record_id) VALUES ( 3, DATEOF(NOW()), 107);
INSERT INTO record_ids (partition, last_modified_date, record_id) VALUES ( 2, DATEOF(NOW()), 108);
INSERT INTO record_ids (partition, last_modified_date, record_id) VALUES ( 3, DATEOF(NOW()), 109);
INSERT INTO record_ids (partition, last_modified_date, record_id) VALUES ( 1, DATEOF(NOW()), 110);
INSERT INTO record_ids (partition, last_modified_date, record_id) VALUES ( 1, DATEOF(NOW()), 111);

SELECT * FROM record_ids;

-- Note the results are only sorted in their partition
-- To try again:
-- DROP KEYSPACE tmpsort;

обратите внимание, что без WHERE предложение вы получаете результаты в порядке токена (ключа раздела). Смотри https://dba.stackexchange.com/questions/157537/querying-cassandra-without-a-partition-key

другие модели распространения баз данных

если я правильно понял-CockroachDB будет аналогично производительности чтения/записи бутылки на монотонном приращении данных на один узел в любой момент времени, но емкость хранилища будет линейно масштабироваться. Также другие запросы диапазона, такие как" oldest 10 "или" между датой X и датой Y", будут распределять нагрузку на большее количество узлов в отличие от Cassandra. Это связано с тем, что база данных CockroachDB-это одно гигантское хранилище отсортированных ключей, где всякий раз, когда диапазон отсортированных данных достигает определенного размера, он перераспределяется.


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

из документов datastax:

теперь() - В узле координатор генерирует новый уникальный timeuuid в миллисекундах при выполнении инструкции. Часть метки времени timeuuid соответствует стандарту UTC (Universal Time). Этот метод полезен для вставки значений. Значение, возвращаемое now() гарантированно будет уникальным.

когда у вас есть несколько реплик, у вас также есть несколько узлов координатора, так как любой узел может быть выбран в качестве узла координатора. Это означает, что ваши вставки не в порядке из-за каких-либо небольших изменений времени на узлах. Таким образом, одна вставка, которая произошла на самом деле позже в вашей системе отсчета, может быть отсортирована до ранее вставленной записи, потому что now() просто генерирует дату на узле координатора, которая немного позади.

вы пытаетесь получить некоторое согласованное (или единственную ссылку на правду) представление о ваших данных. К сожалению, в распределенной среде нет ни одной ссылки на правду.