首页 > 解决方案 > 使用 Cassandra 获得最新的独特结果

问题描述

我有一项服务可以处理不同服务的用户状态。多个 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 行。

我想到了两个都有问题的解决方案:

  1. 使用创建主表PRIMARY KEY (service, user)并使用创建物化视图PRIMARY KEY (service, user, updated_at)。但这会损害性能。
  2. 在写入之前创建表PRIMARY KEY (service, user)并以完全一致的方式读取以检查是否未写入较旧的更新。但这放弃了 Cassandra 的可用性和反模式。

有没有办法在没有写前读/物化视图的情况下做到这一点?


编辑

写入不一定按顺序进行,因此时间戳是在外部提供的。
我不需要保留历史记录,只需保留最后一次更新(通过外部时间戳)。

标签: cassandra

解决方案


对于您的选择:

  1. 使用 PRIMARY KEY (service, user) 创建主表并使用 PRIMARY KEY (service, user, updated_at) 创建物化视图。但这会损害性能。

物化视图并不会真正对性能造成太大影响,并且写入路径非常快,所以我不会担心,但目前 MV 存在很多问题,并且出于某种原因标记为实验性 - 我不会推荐它们,否则您将面临很多一致性当前版本中的问题。

  1. 使用 PRIMARY KEY (service, user) 创建表并在写入前以完全一致性读取以检查是否未写入较旧的更新。但这放弃了 Cassandra 的可用性和反模式。

也许我错过了一些你没有解释的要求,但你不需要在写之前先读。在我看来,这似乎是迄今为止对我来说最好的解决方案。就在您有更新时,将更改推送到 (service, user) 表,然后当您从表中读取时,您将获得每个用户的最新更新。IF EXISTS在使用 paxos 的插入/更新中也有always或 IF 子句。

如果您需要历史记录(不仅仅是最新的)并且您不想要第二张表,您可以使用 group by:

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 之类的)。然后在边界时间附近查询当前和最后一个存储桶。否则,分区将增长,直到它们引起问题。


推荐阅读