Получить последний уникальный результат с Кассандрой - PullRequest
3 голосов
/ 26 июня 2019

У меня есть сервис, который обрабатывает состояние пользователей на разных сервисах. Трафик может быть очень высоким на нескольких DC, поэтому я подумал, что Cassandra подойдет для хранения этих данных.
Мне нужно только сохранить последнее обновление для каждой службы и пользователя.
Я думал о создании этой таблицы:

CREATE TABLE db.state (
   service uuid,
   user uuid,
   updated_at timestamp,
   data varchar,

   PRIMARY KEY (service, user, updated_at)
) WITH CLUSTERING ORDER BY (updated_at DESC);

Вопрос в том, как запросить последние 100 уникальных пользовательских состояний.
С этим запросом:

SELECT service, user, data, updated_at FROM db.state WHERE service = :service LIMIT 100.

Если у определенного пользователя было много обновлений, я не собираюсь получать последние 100 пользователей, но меньше. Я не хочу объединять уникальных пользователей в клиенте, потому что для получения 100 пользователей мне иногда нужно получить 10000 строк.

Я подумал о 2 решениях, у которых есть проблемы:

  1. создать основную таблицу с PRIMARY KEY (service, user) и создать материализованное представление с помощью PRIMARY KEY (service, user, updated_at). Но это повредит производительности.
  2. создайте таблицу с PRIMARY KEY (service, user) и прочитайте с полной согласованностью перед записью, чтобы проверить, что старое обновление не записано. Но это оставляет доступность и анти-шаблон для Cassandra.

Есть ли способ сделать это без представления "чтение перед записью / материализация"?


редактировать

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

1 Ответ

1 голос
/ 26 июня 2019

Для ваших вариантов:

  1. создать основную таблицу с помощью PRIMARY KEY (сервис, пользователь) и создать материализованное представление с помощью PRIMARY KEY (сервис, пользователь, updated_at). Но это повредит производительности.

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

  1. создать таблицу с ПЕРВИЧНЫМ КЛЮЧОМ (сервис, пользователь) и прочитать с полным согласованность перед записью, чтобы проверить, что старое обновление не записано. Но это оставляет доступность и анти-паттерн для Кассандры.

Может быть, я пропускаю некоторые требования, которые вы не объяснили, но вам не нужно читать перед записью. Это кажется мне лучшим решением для меня. Если у вас есть обновление, нажмите на изменение таблицы (служба, пользователь), а затем при чтении из таблицы вы получите последнее обновление для каждого пользователя. На вашей вставке / обновлении всегда есть IF EXISTS или предложения IF, также с использованием paxos.

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

CREATE TABLE state (  // simplified a little
   service int,
   user int,
   updated_at timeuuid,
   data text,
   PRIMARY KEY (service, user, updated_at)
) WITH CLUSTERING ORDER BY (user ASC, updated_at DESC);

INSERT INTO state (service, user, updated_at, data) VALUES ( 1, 1, now(), '1');
INSERT INTO state (service, user, updated_at, data) VALUES ( 1, 1, now(), '2');
INSERT INTO state (service, user, updated_at, data) VALUES ( 1, 1, now(), '3');
INSERT INTO state (service, user, updated_at, data) VALUES ( 1, 2, now(), '1');
INSERT INTO state (service, user, updated_at, data) VALUES ( 1, 2, now(), '2');
INSERT INTO state (service, user, updated_at, data) VALUES ( 2, 1, now(), '1');
INSERT INTO state (service, user, updated_at, data) VALUES ( 1, 3, now(), '2');
INSERT INTO state (service, user, updated_at, data) VALUES ( 1, 3, now(), '3');
INSERT INTO state (service, user, updated_at, data) VALUES ( 1, 3, now(), '1');
INSERT INTO state (service, user, updated_at, data) VALUES ( 1, 3, now(), '2');

SELECT * FROM state WHERE service = 1 GROUP BY service, user;

 service | user | updated_at                           | data
---------+------+--------------------------------------+------
       1 |    1 | 7c2bd900-981e-11e9-a27a-7b01c564a3f0 |    3
       1 |    2 | 7c2d1180-981e-11e9-a27a-7b01c564a3f0 |    2
       1 |    3 | 7c88c610-981e-11e9-a27a-7b01c564a3f0 |    2

Это не удивительно эффективно или что-то еще, но оно будет работать, если вы никогда не позволите одному сервисному разделу стать слишком большим. Я бы на самом деле настоятельно рекомендовал бы добавить к нему компонент даты / корзины, например:

CREATE TABLE state (
   bucket text
   service int,
   user int,
   updated_at timeuuid,
   data text,
   PRIMARY KEY ((bucket, service), user, updated_at)
) WITH CLUSTERING ORDER BY (user ASC, updated_at DESC);

где bucket - строка YYYY-MM-DD (или YYYY-WEEKOFYEAR или что-то в этом роде). Затем, как раз в граничное время, вы запрашиваете как текущий, так и последний сегмент. В противном случае разделы будут расти, пока не возникнут проблемы.

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