第五章:Cassandra数据建模--Cassandra:The Definitive Guide 2nd Edition

在本章中,您将学习如何为Cassandra设计数据模型,包括数据建模过程和符号。 为了应用这些知识,我们将为示例应用程序设计数据模型,我们将在接下来的几章中构建它。 这将有助于显示所有部件如何组合在一起。 在此过程中,我们将使用一个工具来帮助我们管理CQL脚本。

概念数据建模

首先,让我们创建一个在关系世界中易于理解的简单域模型,然后看看我们如何将它从关系映射到Cassandra中的分布式哈希表模型。

为了创建这个例子,我们想要使用足够复杂的东西来展示各种数据结构和设计模式,但是不会让你陷入细节之中。 此外,每个人都熟悉的域将允许您专注于如何使用Cassandra,而不是应用程序域的全部内容。

对于我们的示例,我们将使用易于理解的域,并且每个人都可以参与:进行酒店预订。

我们的概念领域包括酒店,入住酒店的客人,每间酒店的房间集合,这些房间的价格和可用性,以及为客人预订的预订记录。 酒店通常还会保留一系列“兴趣点”,包括公园,博物馆,购物廊,纪念碑或酒店附近其他客人可能希望在入住期间访问的地方。 酒店和兴趣点都需要维护地理位置数据,以便可以在地图上找到mashup,并计算距离。

我们使用Peter Chen推广的实体关系模型描绘了图5-1中的概念域。 这个简单的图表表示我们域中具有矩形的实体,以及具有椭圆的那些实体的属性。 表示项的唯一标识符的属性带有下划线。 实体之间的关系表示为菱形,并且关系和每个实体之间的连接器显示连接的多样性。

在这里插入图片描述

显然,在现实世界中,会有更多的考虑因素和更多的复杂性。 例如,酒店价格是众所周知的动态,计算它们涉及多种因素。 在这里,我们定义了一些复杂的东西,足以让人感兴趣并触及重点,但足够简单,可以专注于学习Cassandra。

RDBMS设计

当您开始构建将使用关系数据库的新数据驱动应用程序时,您可以首先将域建模为一组正确规范化的表,并使用外键引用其他表中的相关数据。

图5-2显示了如何使用关系数据库模型为我们的应用程序表示数据存储。 关系模型包括几个“连接”表,以实现从我们的酒店到兴趣点,房间到设施,房间到可用性和客人的概念模型的多对多关系 - 到房间(通过预订)。

在这里插入图片描述

RDBMS和Cassandra之间的设计差异

当然,因为这是一本Cassandra书,我们真正想要的是对我们的数据建模,以便我们可以将它存储在Cassandra中。 在我们开始创建Cassandra数据模型之前,让我们花一点时间来强调为Cassandra和关系数据库进行数据建模的一些关键差异。

没有joins

你不能在Cassandra中执行连接。 如果您设计了一个数据模型并发现需要类似连接的东西,则必须在客户端执行该工作,或者创建一个非规范化的第二个表来表示您的连接结果。 后一种选择在Cassandra数据建模中是首选。 在客户端执行连接应该是非常罕见的情况; 你真的想要复制(非规范化)数据。

没有参照完整性

尽管Cassandra支持轻量级事务和批处理等功能,但Cassandra本身并没有跨表的参照完整性概念。在关系数据库中,您可以在表中指定外键以引用另一个表中记录的主键。但是Cassandra并没有强制执行。在表中存储与其他实体相关的ID仍然是一种常见的设计要求,但是诸如级联删除之类的操作不可用。

非规范化

在关系数据库设计中,我们经常被教导归一化的重要性。使用Cassandra时这不是一个优势,因为它在数据模型非规范化时表现最佳。通常情况下,公司也会在关系数据库中对数据进行非规范化处理。这有两个常见原因。一个是表现。当企业必须在多年的数据上进行许多连接时,公司根本无法获得所需的性能,因此它们会按照已知查询的方式进行非规范化。这最终会起作用,但与设计关系数据库的方式背道而驰,并最终提出一个问题,即在这些情况下使用关系数据库是否是最佳方法。

关系数据库故意被非规范化的第二个原因是需要保留的业务文档结构。 也就是说,您有一个封闭表,它引用了许多外部表,其数据可能会随时间发生变化,但您需要将封闭文档保留为历史记录中的快照。 这里的常见例子是发票。 您已经拥有客户和产品表,并且您认为您可以制作引用这些表的发票。 但这绝不应该在实践中完成。 客户或价格信息可能会发生变化,然后您将失去发票日期的完整性,因为它可能违反审核,报告或法律,并导致其他问题。

在关系世界中,非规范化违反了Codd的正常形式,我们试图避免它。 但在卡桑德拉,非规范化是完全正常的。 如果您的数据模型很简单,则不需要。 但不要害怕它。

具有物化视图的服务器端非规范化

从历史上看,Cassandra中的非规范化需要使用我们将暂时引入的技术来设计和管理多个表。 从3.0版本开始,Cassandra提供了一种称为物化视图的功能,它允许我们基于基表设计创建多个非规范化数据视图。 Cassandra管理服务器上的物化视图,包括保持视图与表同步的工作。 在本章中,我们将看到经典非规范化和物化视图的示例。

查询优先设计

简单来说,关系建模意味着您从概念域开始,然后在表中表示域中的名词。 然后,您可以将主键和外键分配给模型关系。 如果您具有多对多关系,则可以创建仅表示这些键的连接表。 连接表在现实世界中不存在,并且是关系模型工作方式的必要副作用。 将所有表格布局后,您可以开始编写查询,使用键定义的关系将不同的数据组合在一起。 关系世界中的查询非常次要。 只要您正确地建模表,就可以假设您始终可以获得所需的数据。 即使您必须使用多个复杂的子查询或连接语句,通常也是如此。

相比之下,在Cassandra中,您不会从数据模型开始; 你从查询模型开始。 不是先对数据进行建模,然后使用Cassandra编写查询,而是对查询进行建模并让数据围绕它们进行组织。 考虑应用程序将使用的最常见的查询路径,然后创建支持它们所需的表。

批评者建议首先设计查询过度限制应用程序设计,更不用说数据库建模了。 但是,期望您应该在应用程序中仔细考虑查询是完全合理的,就像您可能会想到您的关系域一样。 你可能弄错了,然后你会在任何一个世界都遇到问题。 或者您的查询需求可能会随着时间的推移而发生变化,然后您将不得不努力更新数据集。 但这与在RDBMS中定义错误的表或需要其他表没有什么不同。

设计最佳存储空间

在关系数据库中,用户通常对表如何存储在磁盘上是透明的,并且很少听到有关基于RDBMS如何在磁盘上存储表的数据建模的建议。 然而,这是卡桑德拉的一个重要考虑因素。 由于Cassandra表每个都存储在磁盘上的单独文件中,因此将相关列保持在同一个表中是很重要的。

当我们开始在Cassandra中创建数据模型时,我们将看到的一个关键目标是最小化必须搜索的分区数量,以满足给定查询。 由于分区是不跨节点划分的存储单元,因此搜索单个分区的查询通常会产生最佳性能。

排序是一项设计决策

在RDBMS中,您可以在查询中使用ORDER BY轻松更改记录返回给您的顺序。 默认排序顺序不可配置; 默认情况下,记录按写入顺序返回。 如果要更改顺序,只需修改查询,就可以按任何列列表进行排序。

然而,在Cassandra中,排序的处理方式不同; 这是一个设计决定。 查询上可用的排序顺序是固定的,完全由您在CREATE TABLE命令中提供的聚类列的选择决定。 CQL SELECT语句支持ORDER BY语义,但仅支持聚类列指定的顺序。

定义应用程序查询

让我们尝试使用查询优先方法开始为酒店应用程序设计数据模型。 应用程序的用户界面设计通常是用于开始识别查询的很好的工件。 让我们假设我们已经与项目利益相关者进行了交谈,我们的UX设计师已经为关键用例生成了用户界面设计或线框图。 我们可能会有一个购物查询列表,如下所示:

  • Q1 查找附近特定兴趣点的酒店。

  • Q2 查找有关特定酒店的信息,例如其名称和位置。

  • Q3 查找特定酒店附近的景点。

  • Q4 查找给定日期范围内的可用房间。

  • Q5 查找房间的价格和设施。

现在,如果我们的申请要取得成功,我们当然希望我们的客户能够在我们的酒店预订。这包括选择可用房间和输入客人信息等步骤。很明显,我们还需要一些查询来解决概念数据模型中的预留和来宾实体。但是,即使在这里,我们也不仅要从客户的角度考虑数据的编写方式,还要考虑下游用例如何查询数据。

作为数据建模者,我们的自然倾向是首先关注设计表以存储预订和访客记录,然后才开始考虑将访问它们的查询。当我们之前开始讨论购物查询时,您可能已经感受到了类似的紧张情绪,想“但酒店和兴趣点数据来自哪里?”别担心,我们很快就能做到这一点。以下是一些描述我们的用户如何访问预订的查询:

  • Q6 通过确认号码查找预订。

  • Q7 按酒店,日期和客人姓名查询预订。

  • Q8 按客人姓名查找所有预订。

  • Q9 查看客人详细信息

我们在图5-3中的应用程序工作流程的上下文中显示了所有查询。 图中的每个框表示应用程序工作流中的一个步骤,箭头指示步骤和关联查询之间的流程。 如果我们已经很好地建模了我们的应用程序,则工作流程的每个步骤都会完成“解锁”后续步骤的任务。 例如,“查看POI附近的酒店”任务有助于应用程序了解几家酒店,包括其独特的钥匙。 所选酒店的钥匙可用作Q2的一部分,以获得酒店的详细描述。 预订房间的行为创建了预订记录,客人和酒店工作人员可以在稍后通过各种附加查询来访问该预订记录。

在这里插入图片描述

逻辑数据建模

现在我们已经定义了查询,我们已经准备好开始设计我们的Cassandra表了。 首先,我们将创建一个逻辑模型,其中包含每个查询的表,从概念模型中捕获实体和关系。

为了命名每个表,我们将识别我们要查询的主要实体类型,并使用它来启动实体名称。 如果我们通过其他相关实体的属性查询,我们将它们附加到表名称,用_by_分隔。 例如,hotels_by_poi。

接下来,我们确定表的主键,根据所需的查询属性添加分区键列,以及聚类列,以保证唯一性并支持所需的排序顺序。

我们通过添加查询标识的任何其他属性来完成每个表。 如果这些附加属性中的任何一个对于分区键的每个实例都相同,我们将该列标记为静态。

现在,这是一个相当快速的描述一个相当复杂的过程,所以值得我们花时间来完成一个详细的例子。 首先,让我们介绍一种可以用来表示逻辑模型的符号。

介绍Chebotko图

Cassandra社区内的几个人提出了以图解形式捕获数据模型的符号。 我们选择使用由Artem Chebotko推广的符号,它提供了一种简单,信息丰富的方式来可视化我们设计中查询和表格之间的关系。 图5-4显示了逻辑数据模型的Chebotko表示法。

在这里插入图片描述

每个表都显示其标题和列列表。 主键列通过符号(例如K表示分区键列)和C↑或C↓表示聚类列。 显示输入表或表之间的行以指示每个表旨在支持的查询。

酒店逻辑数据模型

图5-5显示了涉及酒店,兴趣点,房间和便利设施的查询的Chebotko逻辑数据模型。 我们立刻注意到的一件事是,我们的Cassandra设计不包括用于房间或设施的专用桌子,就像我们在关系设计中所做的那样。 这是因为我们的工作流程未识别出任何需要此直接访问的查询。

在这里插入图片描述

让我们来探索每个表的细节。

我们的第一个查询Q1是查找兴趣点附近的酒店,因此我们将把我们的酒店称为hotels_by_poi。 我们正在搜索一个有名的兴趣点,因此这是一个线索,即兴趣点应该是我们主键的一部分。 让我们按名称引用兴趣点,因为根据我们的工作流程,我们的用户将开始搜索。

你会注意到我们当然可以在一个给定的兴趣点附近拥有多家酒店,所以我们需要在我们的主键中使用另一个组件,以确保每个酒店都有一个独特的分区。 因此,我们将酒店的id添加为群集列。

使您的主键唯一

设计表的主键时,一个重要的考虑因素是确保它定义一个唯一的数据元素。 否则,您将冒着意外覆盖数据的风险

现在,对于我们的第二个查询(Q2),我们需要一张表来获取有关特定酒店的信息。 一种方法是将酒店的所有属性放在hotels_by_poi表中,但我们选择仅添加应用程序工作流所需的那些属性。

从我们的工作流程图中,我们知道hotels_by_poi表用于显示酒店列表,其中包含每家酒店的基本信息,并且应用程序知道所返回酒店的唯一标识符。 当用户选择酒店查看详细信息时,我们可以使用Q2,用于获取有关酒店的详细信息。 因为我们已经拥有了Q1的hotel_id,所以我们将其用作我们正在寻找的酒店的参考。 因此我们的第二张表格叫做酒店。

另一种选择是在酒店表中存储一组poi_names。 这是一种同样有效的方法。 您将通过经验了解哪种方法最适合您的应用。

使用唯一标识符作为参考

您会发现使用唯一ID来唯一引用元素并将这些uuids用作表示其他实体的表中的引用通常很有帮助。 这有助于最小化不同实体类型之间的耦合。 如果您为应用程序使用微服务架构风格,这可能会特别有用,其中有单独的服务负责每个实体类型。

但是,出于本书的目的,我们将主要使用文本属性作为标识符,以使我们的示例简单易读。 例如,酒店业的一个共同惯例是通过诸如“AZ123”或“NY229”的短代码来引用属性。 我们将这些值用于我们的hotel_ids,同时承认它们不一定是全球唯一的。

Q3与Q1相反 - 寻找酒店附近的兴趣点,而非景点附近的酒店。 但是,这一次,我们需要访问每个兴趣点的详细信息,如pois_by_hotel表所示。 正如我们之前所做的那样,我们将兴趣点名称添加为聚类键以保证唯一性。

在这一点上,我们现在考虑如何支持查询Q4,以帮助我们的用户在他们感兴趣的住宿的晚上找到所选酒店的可用房间。 请注意,此查询涉及开始日期和结束日期。 因为我们查询范围而不是单个日期,所以我们知道我们需要将日期用作聚类键。 我们使用hotel_id作为主键,为单个分区上的每个酒店分组房间数据,这应该有助于我们的搜索速度超快。 我们称之为available_rooms_by_hotel_date表。

搜索范围

使用群集列存储您需要在范围查询中访问的属性。 请记住,聚类列的顺序很重要。 我们将在第9章中详细了解范围查询。

为了完善我们数据模型的购物部分,我们添加了amenities_by_room表以支持Q5。 这样,我们的用户就可以查看其中一个房间的设施,并提供所需的住宿日期。

预定逻辑数据模型

现在我们切换到查看预订查询。 图5-6显示了预定的逻辑数据模型。 你会注意到这些表代表了非规范化的设计; 相同的数据出现在多个表中,具有不同的键。

在这里插入图片描述

为了满足Q6,reservations_by_confirmation表支持通过在预订时提供给客户的唯一确认号来查找预订。

如果访客没有确认号,则reservations_by_guest表可用于按访客姓名查找预订。 我们可以设想在自助网站上代表客人使用查询Q7,或者试图协助客人的呼叫中心代理。 由于访客姓名可能不是唯一的,因此我们在此处也将访客ID包括为群集列。

酒店工作人员可能希望按日期查看即将到来的预订记录,以便深入了解酒店的表现,例如酒店售罄或售罄的日期。 Q8支持按日期检索给定酒店的预订。

最后,我们创建一个来宾表。 您会注意到它与第4章中的用户表具有相似的属性。这提供了一个可用于存储guest虚拟机的位置。 在这种情况下,我们为访客记录指定一个单独的唯一标识符,因为访客具有相同的名称并不罕见。 在许多组织中,客户数据库(例如guest表)将成为单独的客户管理应用程序的一部分,这就是我们从示例中省略了其他访客访问模式的原因。

为所有利益相关者设计查询

Q8和Q9特别有助于提醒我们,我们需要创建支持应用程序的各种利益相关者的查询,不仅是客户,还有员工,甚至是分析团队,供应商等等。

模式和反模式

与其他类型的软件设计一样,Cassandra中有一些众所周知的模式和反模式用于数据建模。 我们已经使用了酒店模型中最常见的模式之一 - 宽排。

时间序列模式是宽行模式的扩展。 在该模式中,以特定时间间隔的一系列测量存储在宽行中,其中测量时间用作分区键的一部分。 这种模式经常用于包括业务分析,传感器数据管理和科学实验在内的领域。

时间序列模式对于除测量之外的数据也是有用的。考虑银行应用程序的示例。我们可以连续存储每个客户的余额,但这可能会导致大量的读写争用,因为各种客户都会查看余额或进行交易。我们可能想要围绕我们的写操作包装事务只是为了保护余额不被错误更新。相反,时间序列式设计会将每个事务存储为带时间戳的行,并将计算当前余额的工作留给应用程序。

许多新用户陷入的一个设计陷阱是尝试将Cassandra用作队列。队列中的每个项目都存储有一个宽行的时间戳。项目附加到队列的末尾并从前面读取,在读取后将被删除。这是一种看似有吸引力的设计,特别是考虑到它与时间序列模式的明显相似性。这种方法的问题在于删除的项目现在是Cassandra必须扫描过去的墓碑,以便从队列的前面读取。随着时间的推移,越来越多的墓碑开始降低读取性能。

队列反模式用作提醒,任何依赖于删除数据的设计都可能是性能不佳的设计。

物理数据建模

一旦我们定义了逻辑数据模型,创建物理模型就是一个相对简单的过程。

我们遍历每个逻辑模型表,为每个项目分配类型。 我们可以使用第4章中介绍的任何类型,包括基本类型,集合和用户定义类型。 我们可以识别可以创建的其他用户定义类型以简化我们的设计。

在我们分配了数据类型之后,我们通过执行大小计算并测试模型的工作原理来分析我们的模型。 我们可能会根据调查结果做出一些调整。 通过我们的示例,我们将再次更详细地介绍数据建模过程。

在我们开始之前,让我们看一下Chebotko表示法对物理数据模型的一些补充。

Chebotko物理图

要绘制物理模型,我们需要能够为每列添加输入信息。 图5-7显示了为样本表中的每列添加类型。

该图包括指定包含每个表的键空间和使用集合和用户定义类型表示的列的可视提示。 我们还注意到静态列和二级索引列的指定。 将这些作为逻辑模型的一部分进行分配没有限制,但它们通常更多地是物理数据建模问题。

在这里插入图片描述

酒店物理数据模型

现在让我们开始研究我们的物理模型。 首先,我们需要表空间的键空间。 为了保持设计相对简单,我们将创建一个酒店密钥空间来包含我们的酒店和可用性数据表,以及一个预留密钥空间,用于包含预订和访客数据的表。 在实际系统中,我们可能会将表划分为更多的键空间,以便分离关注点。

对于我们的酒店表,我们将使用Cassandra的文本类型来表示酒店的ID。 对于地址,我们将使用我们在第4章中创建的地址类型。我们使用文本类型来表示电话号码,因为各国之间的数字格式存在很大差异。

当我们在逻辑酒店数据模型中创建各种表的物理表示时,我们使用相同的方法。 最终设计如图5-8所示。

在这里插入图片描述

请注意,我们还在设计中包含了地址类型。 它用星号表示,表示它是用户定义的类型,并且没有标识主键列。 我们在酒店和hotels_by_poi表中使用这种类型。

利用用户定义的类型

使用用户定义的类型来帮助减少非主键列的重复通常很有帮助,就像我们使用地址用户定义的类型一样。 这可以降低设计的复杂性。

请记住,UDT的范围是定义它的键空间。 要在我们即将设计的预留键空间中使用地址,我们必须再次声明它。 这只是我们在数据模型设计中必须做出的许多权衡之一。

预约物理数据模型

现在,让我们将注意力转向我们设计中的预订表。 请记住,我们的逻辑模型包含三个非规范化表,以支持通过确认号,来宾,酒店和日期查询预订。 在我们努力实现这些不同的设计时,我们需要考虑是手动管理非规范化还是使用Cassandra的物化视图功能。

图5-9中为预留键空间显示的设计使用了这两种方法。 我们选择将reservations_by_hotel_date和reservations_by_guest实现为常规表,并将reservations_by_confirmation实现为reservations_by_hotel_date表的物化视图。 我们将暂时讨论这种设计选择背后的原因。

在这里插入图片描述

请注意,我们已在此键空间中重现了地址类型,并在所有表中将guest_id建模为uuid类型。

物化视图

引入物化视图以帮助解决二级索引的一些缺点,我们将在第4章中讨论。在具有高基数的列上创建索引往往会导致性能不佳,因为需要查询环中的大多数或所有节点。

物化视图通过存储预配置视图来解决此问题,这些视图支持对不属于原始群集键的其他列的查询。 物化视图简化了应用程序开发:而不是应用程序必须保持多个非规范化表同步,Cassandra负责更新视图以使它们与基表保持一致。

物化视图会对写入产生较小的性能影响,以保持这种一致性。 但是,与在应用程序客户端中管理非规范化表相比,物化视图表现出更高效的性能。 在内部,物化视图更新是使用批处理实现的,我们将在第9章中讨论。

与二级索引类似,可以在现有表上创建实例化视图。

要理解与物化视图相关的语法和约束,我们将查看从预留物理模型创建reservations_by_confirmation表的CQL命令:


cqlsh> CREATE MATERIALIZED VIEW reservation.reservations_by_confirmation   AS SELECT *   FROM reservation.reservations_by_hotel_date  WHERE confirm_number IS NOT NULL and hotel_id IS NOT NULL and    start_date IS NOT NULL and room_number IS NOT NULL  PRIMARY KEY (confirm_number, hotel_id, start_date, room_number);

CREATE MATERIALIZED VIEW命令中子句的顺序可能会有些反转,因此我们将按照更容易处理的顺序浏览这些子句。

命令后面的第一个参数是物化视图的名称 - 在本例中为reservations_by_confirmation。 FROM子句标识实例化视图的基表,reservations_by_hotel_date。

PRIMARY KEY子句标识实现视图的主键,该主键必须包括基表主键中的所有列。 此限制使Cassandra不会将基表中的多行折叠为实体化视图中的单个行,这将极大地增加管理更新的复杂性。

主键列的分组使用与普通表相同的语法。 最常见的用法是首先将附加列作为分区键,然后是基表主键列,用作实体化视图的聚类列。

WHERE子句提供对过滤的支持。请注意,必须为实例化视图的每个主键列指定过滤器,即使它只是指定值为IS NOT NULL这么简单。

AS SELECT子句标识我们希望物化视图包含的基表中的列。 我们可以引用各个列,但在这种情况下,通过使用通配符*选择所有列都是视图的一部分。

增强的物化视图功能

3.0版本中物化视图的初始实现在选择主键列和过滤器时存在一些限制。 有几个JIRA问题正在进行中,以在物化视图主键CASSANDRA-9928中添加多个非主键列或在物化视图CASSANDRA-9778中使用聚合。 如果您对这些功能感兴趣,请跟踪JIRA问题,以了解它们何时会包含在发行版中。

既然我们对物化视图的设计和使用有了更好的理解,我们就可以重新考虑先前为预留物理设计做出的决定。 具体来说,由于确认号码的高基数,reservations_by_confirmation是实现作为物化视图的良好候选者 - 毕竟,您不能获得比每个预订的唯一值更高的基数。

另一种设计是使用reservations_by_confirmation作为基表,并使用reservations_by_hotel_date作为物化视图。但是,因为我们不能(至少在早期的3.X版本中)使用基表中的多个非主键列创建物化视图,所以我们需要在reservations_by_confirmation中将hotel_id或date指定为聚类列。这两种设计都是可以接受的,但是这应该可以让您深入了解在选择使用哪种非规范化表设计作为基表时需要考虑的权衡。

评估和提炼

一旦我们创建了物理模型,我们就需要采取一些步骤来评估和优化我们的表设计,以帮助确保最佳性能。

计算分区大小

我们想要寻找的第一件事是我们的表是否会有过大的分区,或者换句话说,分区太宽。 分区大小通过存储在分区中的单元(值)的数量来度量。 Cassandra的硬限制是每个分区20亿个单元,但是在达到该限制之前我们可能会遇到性能问题。为了计算分区的大小,我们使用以下公式:

为了计算分区的大小,我们使用以下公式:

N v = N r ( N c − N p k − N s ) + N s N_v = N_r(N_c - N_{pk} -N_s) + N_s Nv=Nr(NcNpkNs)+Ns

分区(Nv)中的值(或单元)的数量等于静态列的数量(Ns)加上行数(Nr)与每行的值的数量的乘积。 每行的值的数量被定义为列数(Nc)减去主键列(Npk)和静态列(Ns)的数量。

列数往往是相对静态的,尽管我们已经看到很有可能在运行时更改表。 因此,分区大小的主要驱动程序是分区中的行数。 这是确定分区是否有可能变得过大时必须考虑的关键因素。 20亿个值听起来很多,但在传感器系统中,每毫秒测量数十或数百个值,值的数量开始相当快。

让我们看看我们的一个表来分析分区大小。 因为它有一个宽行设计,每个酒店都有一个分区,我们将选择available_rooms_ by_hotel_date表。 该表总共有四列(Nc = 4),包括三个主键列(Npk = 3)和无静态列(Ns = 0)。 将这些值插入我们的公式中,我们得到:

N v = N r ( 4 − 3 − 0 ) + 0 = 1 N r N_v = N_r (4 - 3 - 0 ) + 0 = 1 N_r Nv=Nr(430)+0=1Nr

因此,此表的值的数量等于行数。 我们仍然需要确定多个行。 为此,我们根据我们正在设计的应用程序进行一些估算。 我们的桌子每晚为每个房间存储每个房间的记录。 我们假设我们的系统将用于存储两年的库存,我们的系统中有5,000家酒店,每家酒店平均有100个房间。

由于每个酒店都有一个分区,我们估计每个分区的行数如下:

N r = 100 r o o m s / h o t e l × 730 d a y s = 73000 r o w s N_r = 100 rooms/hotel \times 730 days = 73000 rows Nr=100rooms/hotel×730days=73000rows

每个分区的行数相对较少并不会给我们带来太多麻烦,但如果我们开始存储更多的库存日期,或者使用TTL不能很好地管理库存的大小,我们就会开始遇到问题。 我们仍然可能想要分解这个大分区,我们很快就会这样做。

评估最坏情况

在执行大小计算时,很容易假设变量的名义或平均情况,例如行数。 考虑计算最坏的情况,因为这些类型的预测在成功的系统中有一种方法可以实现。

计算磁盘大小

除了计算分区的大小之外,我们还可以估计我们计划在群集中存储的每个表所需的磁盘空间量。 为了确定大小,我们使用以下公式来确定分区的大小St:

S t = ∑ s i z e O f ( c k ) + ∑ s i z e O f ( c s ) + N r × ( ∑ s i z e O f ( c r ) + ∑ s i z e O f ( c c ) ) + N v × s i z e O f ( t a v g ) S_t = \sum sizeOf(c_k) + \sum sizeOf(c_s) + N_r \times (\sum sizeOf(c_r) + \sum sizeOf(c_c)) + N_v \times sizeOf(t_{avg}) St=sizeOf(ck)+sizeOf(cs)+Nr×(sizeOf(cr)+sizeOf(cc))+Nv×sizeOf(tavg)

这比我们之前的公式复杂一点,但我们会一次分解一下。 我们先来看一下符号:

  • 在此公式中,ck表示分区键列,cs表示静态列,cr表示常规列,cc表示聚类列。

  • 术语tavg是指每个单元存储的元数据的平均字节数,例如时间戳。 通常对该值使用8字节的估计值。

  • 我们从先前的计算中识别行数Nr和值Nv的数量。

  • sizeOf()函数引用每个引用列的CQL数据类型的大小(以字节为单位)。

第一项要求我们将分区键列的大小相加。 对于我们的示例,available_rooms_by_hotel_date表有一个分区键列,即hotel_id,我们选择将其作为类型文本。 假设我们的酒店标识符是简单的5字符代码,我们有一个5字节的值,因此我们的分区键列大小的总和是5个字节。

第二个术语要求我们总结静态列的大小。 我们的表没有静态列,所以在我们的例子中这是0字节

第三个术语涉及最多,并且有充分理由 - 它正在计算分区中单元格的大小。我们总结了聚类列和常规列的大小。我们的两个聚类列是日期,我们假设它是4个字节,而room_number是一个2字节的短整数,给出了6个字节的总和。只有一个常规列,布尔值is_available,大小为1个字节。将常规列大小(1个字节)加上聚类列大小(6个字节)相加可得到总共7个字节。要完成该术语,我们将此值乘以行数(73,000),得到511,000字节(0.51 MB)。

第四个术语是简单地计算Cassandra为每个单元格存储的元数据。在Cassandra 3.0及更高版本使用的存储格式中,给定单元格的元数据量根据存储的数据类型以及是否为单个单元格指定自定义时间戳或TTL值而有所不同。对于我们的表,我们重复使用先前计算中的值的数量(73,000)并乘以8,这给出了0.58 MB。

将这些术语加在一起,我们得到最终估计:

分区大小= 16字节+0字节+ 0.51 MB + 0.58 MB = 1.1 MB

此公式是磁盘上分区的实际大小的近似值,但是非常准确,非常有用。 记住分区必须能够适合单个节点,看起来我们的表设计不会给我们的磁盘存储带来很大压力。

更紧凑的存储格式

如第2章所述,Cassandra的存储引擎已针对3.0版本重新实现,包括SSTable文件的新格式。 以前的格式存储了一个单独的聚类列副本,作为每个单元格记录的一部分。 较新的格式消除了这种重复,从而减少了存储数据的大小,并简化了计算该大小的公式。

请记住,此估算仅计算我们数据的单个副本。 我们需要将此处获得的值乘以分区数和密钥空间复制策略指定的副本数,以确定每个表所需的总容量。 当我们在第14章讨论如何规划集群时,这将派上用场。

打破大型分区

如前所述,我们的目标是设计能够提供触摸单个分区的查询所需的数据的表,或者尽可能少的分区。 但是,正如我们在示例中看到的那样,很有可能设计出接近Cassandra内置限制的宽行样式表。 对表执行大小分析可能会显示可能太大的分区,无论是值的数量,磁盘的大小,还是两者。

拆分大分区的技术很简单:在分区键中添加一个额外的列。 在大多数情况下,将一个现有列移动到分区键中就足够了。 另一种选择是在表中引入一个附加列作为分片键,但这需要额外的应用程序逻辑。

继续检查我们的可用房间示例,如果我们将日期列添加到available_rooms_by_hotel_date表的分区键,则每个分区将表示特定日期特定酒店的房间可用性。这肯定会产生明显更小,可能太小的分区,因为连续几天的数据可能会在不同的节点上。

另一种称为bucketing的技术通常用于将数据分成中等大小的分区。例如,我们可以通过向分区键添加月份列来对我们的available_rooms_ by_hotel_date表进行bucketize。虽然此列与日期部分重复,但它提供了一种在分区中对相关数据进行分组的好方法,该分区不会太大。

如果我们真的对保留宽行设计非常感兴趣,我们可以将room_id添加到分区键,以便每个分区代表所有日期的房间可用性。由于我们尚未确定涉及搜索特定房间可用性的查询,因此第一种或第二种设计方法最适合我们的应用需求。

定义数据库模式

一旦我们完成了对物理模型的评估和优化,我们就可以在CQL中实现模式了。 以下是酒店键空间的架构,使用CQL的注释功能来记录每个表支持的查询模式:


CREATE KEYSPACE hotel    WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 3};


CREATE TYPE hotel.address (    
street text,    
city text,    
state_or_province text,    
postal_code text,    
country text);


CREATE TABLE hotel.hotels_by_poi (    
poi_name text,    
hotel_id text,    
name text,    
phone text,    
address frozen<address>,    
PRIMARY KEY ((poi_name), hotel_id)
) WITH comment = 'Q1. Find hotels near given poi'
AND CLUSTERING ORDER BY (hotel_id ASC) ;


CREATE TABLE hotel.hotels (    
id text PRIMARY KEY,    
name text,    
phone text,    
address frozen<address>,    
pois set<text>) 
WITH comment = 'Q2. Find information about a hotel';


CREATE TABLE hotel.pois_by_hotel (    
poi_name text,    
hotel_id text,    
description text,    
PRIMARY KEY ((hotel_id), poi_name)
) WITH comment = 'Q3. Find pois near a hotel';


CREATE TABLE hotel.available_rooms_by_hotel_date (    
hotel_id text,    
date date,    
room_number smallint,    
is_available boolean,    
PRIMARY KEY ((hotel_id), date, room_number)
) WITH comment = 'Q4. Find available rooms by hotel / date';

CREATE TABLE hotel.amenities_by_room (    
hotel_id text,    
room_number smallint,    
amenity_name text,    
description text,    
PRIMARY KEY ((hotel_id, room_number), amenity_name)
) WITH comment = 'Q5. Find amenities for a room';

明确识别分区键

我们选择通过用括号括住分区键的元素来表示我们的表,即使分区键由单列poi_name组成。 这是一种最佳实践,它使我们选择的分区键对于阅读我们的CQL的其他人更明确。

同样,这是预留键空间的模式:

CREATE KEYSPACE reservation    WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 3};


CREATE TYPE reservation.address (    
    street text,    
    city text,    
    state_or_province text,    
    postal_code text,    
    country text);


CREATE TABLE reservation.reservations_by_hotel_date (    
    hotel_id text,    
    start_date date,    
    end_date date,    
    room_number smallint,    
    confirm_number text,    
    guest_id uuid,    
    PRIMARY KEY ((hotel_id, start_date), room_number)
    ) WITH comment = 'Q7. Find reservations by hotel and date';


CREATE MATERIALIZED VIEW reservation.reservations_by_confirmation AS    SELECT * FROM reservation.reservations_by_hotel_date    WHERE confirm_number IS NOT NULL and hotel_id IS NOT NULL and        start_date IS NOT NULL and room_number IS NOT NULL    PRIMARY KEY (confirm_number, hotel_id, start_date, room_number);


CREATE TABLE reservation.reservations_by_guest (    
    guest_last_name text,    
    hotel_id text,    
    start_date date,    
    end_date date,    
    room_number smallint,    
    confirm_number text,    
    guest_id uuid,    
    PRIMARY KEY ((guest_last_name), hotel_id)
    ) WITH comment = 'Q8. Find reservations by guest name';


CREATE TABLE reservation.guests (    
    guest_id uuid PRIMARY KEY,    
    first_name text,    
    last_name text,    
    title text,    
    emails set<text>,    
    phone_numbers list<text>,    
    addresses map<text, frozen<address>>,    
    confirm_number text
) WITH comment = 'Q9. Find guest by ID';

DataStax DevCenter

我们已经有很多使用cqlsh创建模式的实践,但是现在我们开始创建一个包含更多表的应用程序数据模型,跟踪所有CQL开始变得更加困难。

值得庆幸的是,DataStax提供了一个名为DevCenter的强大开发工具。 该工具可从DataStax Academy免费下载。 图5-10显示了在DevCenter中编辑的酒店架构。

中间窗格显示当前选定的CQL文件,其中包含CQL命令,CQL类型和名称文字的语法突出显示。 当您键入CQL命令并解释您键入的命令时,DevCenter会提供命令完成功能,突出显示您所做的任何错误。 该工具提供了用于管理多个CQL脚本和多个集群连接的窗格。 连接用于针对实时集群运行CQL命令并查看结果。

在这里插入图片描述

总结

在本章中,我们了解了如何创建一个完整的,有效的Cassandra数据模型,并将其与等效的关系模型进行比较。 我们以逻辑和物理形式表示我们的数据模型,并学习了一种用CQL实现数据模型的新工具。 现在我们有了一个工作数据模型,我们将在接下来的章节中继续构建我们的酒店应用程序。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值