卡桑德拉分页

Cassandra是一个用于不同用例的出色数据库。 在不同的情况下,您需要稍微扭转一下Cassandra,研究其中一种可能是一个有助于更好地了解Cassandra内容的有用练习。 数据库是复杂的野兽,以正确的抽象水平接近它们是至关重要的。 他们的最终目标不是存储数据本身,而是使数据可访问。 这些读取模式将定义哪个数据库是完成该工作的最佳工具。

卡桑德拉(Cassandra)中的时间序列

时间序列是与某个变量相关的数据的集合。 Facebook的时间表将是一个很好的例子。 用户将随着时间的推移写一系列帖子。 对该数据的访问方式将类似于“返回用户1234的最后20条帖子”。 对该查询进行建模的表的DDL为:

CREATE TABLE timeline (
    user_id uuid,
    post_id timeuuid,
    content text,
    PRIMARY KEY (user_id, post_id)
)
WITH CLUSTERING ORDER BY (post_id DESC);

在Cassandra中,主键由分区键和群集键组成。 主键以与关系数据库不同的方式强制某些单元的唯一性。 如果您尝试插入与已经存在的主键相关的某些单元格,则不会强烈地强制执行该唯一性,它将被更新。 还有另一种方法:“缺失”更新将最终作为插入。 这就是所谓的upsert。

分区键可确保数据将驻留在群集的哪个节点上。 如果您至少包含一个集群键,则分区键将标识N行。 对于那些来自传统关系数据库的人来说,这可能会造成混乱。 Cassandra尽最大努力将其概念引入SQL术语中,但有时对于新手来说可能很奇怪。 时间线表的示例为:

user_id--------------------------------post_id--------content
346e896a-c6b4-4d4e-826d-a5a9eda50636---today----------Hi
346e896a-c6b4-4d4e-826d-a5a9eda50636---yesterday------Hola
346e896a-c6b4-4d4e-826d-a5a9eda50636---one week ago---Bye
346e896a-c6b4-4d4e-826d-a5a9eda50636---two weeks ago--Ciao

为了理解该示例,我将post_id值转换为对读者有意义的内容。 如您所见,有几个具有相同分区键(user_id)的值,并且在我们定义了一个群集键(post_id)时起作用,该键对这些值进行群集并对其进行排序(在这种情况下为降序)。 请记住,唯一性是由主键(分区加群集键)定义的,因此,如果我们插入以“ 346e896a-c6b4-4d4e-826d-a5a9eda50636”和“今天”标识的行,则内容将被更新。 由于Cassandra可以处理磁盘中的不可变结构,因此磁盘上的任何内容都无法真正更新,但是在读取时,具有相同主键的不同写入将以降序解决。

让我们看一些查询来完成此示例:

SELECT * FROM timeline
where user_id = 346e896a-c6b4-4d4e-826d-a5a9eda50636

->将返回四行,按post_id DESC排序

SELECT content FROM timeline
where user_id = 346e896a-c6b4-4d4e-826d-a5a9eda50636 LIMIT 1

->它将返回“嗨”

SELECT content FROM
timeline where user_id = 346e896a-c6b4-4d4e-826d-a5a9eda50636 and post_id > today LIMIT 2

->将返回“ Hola”和“ Bye”

如您所见,在Cassandra中为“时间序列”建模时,实现排序的分页非常容易。 此外,由于Cassandra将由单个分区键标识的所有行存储在同一节点中,因此性能非常好,因此需要一次往返来获取此数据(假设读取一致性级别为ONE)。

让我们看看当我们想在不同的用例中实现排序的分页时会发生什么。

卡桑德拉(Cassandra)中的排序集

如果在上一个示例中以数据结构抽象级别考虑,我们可以看到我们只是对一个Map建模,其值是Sorted Sets。 如果我们想用Cassandra为排序集建模,会发生什么?

我们的情况如下。 我们系统的用户可以通过某些管理门户网站暂停或取消暂停。 管理员希望查看由于暂停原因而被暂停的最后一个用户,以便核实该决定或撤销该决定。 这与我们之前的分页查询非常相似,因此让我们来使用Cassandra进行建模。

CREATE TABLE suspended_users (
    user_id uuid,
    occurred_on timestamp,
    reason text
)

我故意从此DDL中删除了主键,因此我们可以讨论不同的选项。

了解集群键

以前,我们使用聚类键为数据提供一些顺序。 让我们选择该选项:

PRIMARY KEY (user_id, occurred_on)

您能知道这有什么问题吗? 忘记执行细节一秒钟,然后回答这个问题,用户将在此表中出现多少次? 作为您的自选产品所有者,我只说一个。 取消暂停用户后,我想从该表中删除该用户,并且已暂停的用户无法再次暂停。 下一个问题:我们要在哪里保留一些订单? 不是内部用户(在这种情况下甚至更少,因为我们的单个用户将始终被“订购”),而是在用户之间。 此设计无法正常工作。

了解分区键和分区程序

我有一些新的信息可能会对您有所帮助。 该表将实时更新,这意味着该表应保持某种逻辑插入顺序。 由于我们没有深入了解Cassandra的细节,因此可以认为以下方法会起作用:

PRIMARY KEY (user_id)

让我们看看逻辑插入顺序如何映射到物理插入顺序。 Cassandra将其数据存储在一个节点环中。 每个节点被分配一个令牌(如果使用vnode,则分配多个令牌)。 当你CRUD一些数据卡桑德拉将计算其中环生活中使用数据分区程序将散列分区键。 使用推荐的分区程序时, Cassandra行按其值的哈希值排序,因此行的顺序没有意义 ,因此逻辑插入顺序将是逻辑顺序,仅此而已。 这意味着此查询将返回20个用户,而没有任何有意义的顺序:

SELECT * FROM suspended_users LIMIT 20;

使用令牌功能,因为它是在解释,我们可以分页的大型数据集在这里

SELECT * FROM suspended_users where token(user_id) > token([Last user_id received]) LIMIT 20;

但是,我们想按暂停时间和降序对已排序的集合进行分页。

呈现反向查询

非规范化是Cassandra中常见的事情。 为了克服Cassandra实施所施加的限制,建议对数据进行非规范化。 由于前面的示例,我们了解到要在数据之间保持一定顺序,我们需要将其聚类。 没有人强迫我们使用suspended_users表,即使我们的域在谈论它也是如此。 由于我们需要一些固定变量来创建时间序列,因此我们将使用以下状态:

CREATE TABLE users_by_status (
  status text,
  occurred_on timestamp,
  user_id uuid
  reason text,
  PRIMARY KEY (status, occurred_on, user_id)
) WITH CLUSTERING ORDER BY (occurred_on DESC);

分区键和群集键可以混合使用。 在此特定密钥中,“状态”将是分区密钥,“ occurred_on” /“ user_id”将是集群密钥。 默认顺序是ASC,所以这就是为什么我们在CLUSTERING ORDER BY中指定了“ occurred_on” DESC的原因。 重要的是要注意,即使在两个用户在非常确切的时间被挂起的极少数情况下,“ user_id”在此设计中也将用于唯一性目的。

现在,我们创建了一个“人工”聚类,我们可以像第一个示例那样以分页的方式进行分页。 但是,这带来了几个问题。 Cassandra不会在一行内拆分数据,并且建议分区内的行的最大大小为200k。 如果您预见到您的系统将增长得更多,则可以使用临时存储区通过复合分区键技术来拆分行。

CREATE TABLE users_by_status (
  bucket text,
  status text,
  occurred_on timestamp,
  user_id uuid
  reason text,
  PRIMARY KEY ((bucket, status), occurred_on, user_id)
) WITH CLUSTERING ORDER BY (occurred_on DESC);

像是MM-YYYY之类的存储桶,或者是您的数据会建议您的任何细粒度的规定。 在这里,我介绍了新的CQL(卡桑德拉查询语言),它是复合分区键。 如您所见,这些嵌套括号内的任何内容都是分区键。

下一个问题是我们将如何删除或更新需要暂停的用户。 管理员可以具有user_id和existed_on,这不是问题,因为他可以执行以下查询:

DELETE FROM users_by_status WHERE status = 'SUSPENDED' and occurred_on = ... and user_id = ...

不幸的是,管理员可能会从某些特权管理员那里获得取消用户暂停的请求。 经理不知道暂停发生的时间,他们只知道谁是用户。 这意味着我们无法访问具体的行,因为我们没有'occurred_on'。 请记住,要在Cassandra中进行查询,您需要提供整个分区键(否则,Cassandra将不知道必须在哪个节点中获取数据)和集群键的可选部分(但始终是从左到右)。

为了克服这个问题,我们可以在“ user_id”列中创建二级索引。 在关系数据库中,索引使我们可以更快地查询一些创建非规范化结构的数据。 在Cassandra中,这些二级索引允许我们按列查询,否则将无法使用。 但是,由于它们对性能的影响很大,因此不鼓励使用它们,因为它们需要多次往返于不同的节点。

下一个解决方案是通过一种称为反向查找的方法手动创建我们自己的二级索引。 让我们看看它的外观:

CREATE TABLE suspended_users (
  user_id uuid,
  occurred_on timestamp,
  PRIMARY KEY (user_id)
);

该表将用作反向查询。 只要拥有“ user_id”,我们就可以访问“ occurred_on”值,然后我们就可以查询users_by_status表。 这种方法有一些缺点。 每当我们插入或删除用户时,我们都必须转到两个表,但这是一个固定的数字。 使用二级索引,在最坏的情况下,我们将不得不转到N个节点。 因此它从O(1)变为O(N)。 我们的代码也将更加复杂,因为我们必须联系两个不同的表。

这带来了更严重的缺点,即最终的一致性和Cassandra中的事务。 事务不是在Cassandra的核心中构建的(存在轻量级事务或批处理之类的概念,但它们的效率也很低),因此这意味着我们的代码需要手动注意事务。

如果要删除用户,则应从users_by_status表开始。 如果从另一方向开始,而第二次删除失败,则由于删除了反向查询条目,将来我们将无法删除该行。 我们可以引入Saga模式,该模式基本上在程序化事务的每个步骤中都定义了回滚克星。

结论

如您所见,在Cassandra中,一旦引入一些需求,关系数据库中一些非常简单的事情(例如查询一组已排序数据的分页)可能会很棘手。 如果基础架构允许,则应使用多语言持久性方法,该方法针对每个用例使用最佳工具。 无论如何,即使不是最佳用例,Cassandra仍可为您提供足够的灵活性来对数据进行建模。

翻译自: https://www.javacodegeeks.com/2016/04/sorted-pagination-cassandra.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值