个性化阅读
专注于IT技术分析

为Cassandra设计数据模型

点击下载

本文概述

数据建模

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

数据建模的概念

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

为了创建示例, 我们想要使用足够复杂的东西来显示各种数据结构和设计模式, 而又不想让细节陷入困境。此外, 每个人都熟悉的域将使你可以专注于如何与Cassandra一起使用, 而不是应用程序域的全部用途。

在我们的示例中, 我们将使用一个易于理解且每个人都可以与之相关的域:进行酒店预订。

我们的概念性领域包括酒店, 入住酒店的客人, 每个酒店的房间集合, 这些房间的价格和空房情况以及为客人预订的预订记录。酒店通常还会维护“景点”的集合, 这些景点包括公园, 博物馆, 购物廊, 古迹或客人在住宿期间可能要参观的酒店附近的其他地方。旅馆和兴趣点都需要维护地理位置数据, 以便可以在地图上找到它们进行混搭, 并计算距离。

我们使用由Peter Chen推广的实体-关系模型在图1-1中描述了我们的概念领域。此简单图用矩形表示我们域中的实体, 并用椭圆表示这些实体的属性。代表项目唯一标识符的属性带有下划线。实体之间的关系表示为菱形, 并且关系与每个实体之间的连接器显示了连接的多重性。

图1-1。

酒店领域实体-关系图

显然, 在现实世界中, 将会有更多的考虑因素和更多的复杂性。例如, 臭名昭著的酒店价格是动态变化的, 计算价格会涉及多种因素。在这里, 我们定义的东西足够复杂, 足以引起人们的兴趣并触及要点, 但又足够简单, 可以使人们始终专注于学习Cassandra。

RDBMS设计

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

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

图1-2。

使用RDBMS的简单酒店搜索系统

RDBMS和Cassandra之间的设计差异

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

没有加入

你无法在Cassandra中执行联接。如果你已经设计了数据模型并发现需要连接之类的东西, 则必须在客户端上进行工作, 或者创建一个非规范化的第二张表来代表你的连接结果。后一种选择在Cassandra数据建模中是首选。在客户端执行联接应该是非常罕见的情况。你确实想复制(反规范化)数据。

没有参照完整性

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

非规范化

在关系数据库设计中, 我们经常被教导标准化的重要性。在使用Cassandra时, 这不是优势, 因为在对数据模型进行非规范化时, 其性能最佳。通常, 公司最终也会在关系数据库中对数据进行非规范化。有两个常见原因。一是性能。当公司不得不对数年的数据进行大量连接时, 他们根本无法获得所需的性能, 因此它们会按照已知的查询进行非规范化。这最终奏效了, 但是与关系数据库的设计意图背道而驰, 最终使人们产生了疑问, 在这些情况下, 使用关系数据库是否是最佳方法。

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

在关系世界中, 非规范化违反了Codd的范式, 因此我们尝试避免这种情况。但是在卡桑德拉(Cassandra)中, 非规范化是完全正常的。如果你的数据模型很简单, 则不是必需的。但不要害怕。

注意

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

从历史上看, Cassandra中的非规范化要求使用我们将立即介绍的技术来设计和管理多个表。从3.0版本开始, Cassandra提供了称为物化视图的功能, 该功能使我们能够基于基表设计创建多个数据的非规范化视图。 Cassandra在服务器上管理物化视图, 包括使视图与表保持同步的工作。在本章中, 我们将看到经典的非规范化视图和实例化视图的示例。

查询优先设计

简单来说, 关系建模意味着你从概念领域开始, 然后在表中表示领域中的名词。然后, 你可以分配主键和外键来建立模型关系。当你具有多对多关系时, 你将创建仅代表那些键的联接表。联接表在现实世界中不存在, 并且是关系模型工作方式的必要副作用。布置完所有表后, 可以开始编写查询, 这些查询使用键定义的关系将不同的数据汇总在一起。关系世界中的查询非常次要。假定只要表已正确建模, 就始终可以获取所需的数据。即使必须使用多个复杂的子查询或join语句, 通常也是如此。

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

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

设计最佳存储

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

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

排序是设计决定

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

但是, 在Cassandra中, 排序的处理方式有所不同。这是一个设计决定。查询中可用的排序顺序是固定的, 并且完全由你在CREATE TABLE命令中提供的集群列的选择确定。 CQL SELECT语句确实支持ORDER BY语义, 但仅按聚簇列指定的顺序。

定义应用查询

让我们尝试使用查询优先的方法来开始为酒店应用程序设计数据模型。应用程序的用户界面设计通常是一个很棒的工件, 可用于开始识别查询。假设我们已经与项目涉众进行了交谈, 我们的UX设计人员已经为关键用例制作了用户界面设计或线框。我们可能会列出类似以下的购物查询:

  • Q1。查找给定景点附近的酒店。
  • Q2。查找有关给定酒店的信息, 例如其名称和位置。
  • Q3。查找给定酒店附近的景点。
  • Q4。在给定的日期范围内找到可用的房间。
  • Q5。查找房间的价格和设施。
注意

编号你的查询

能够以简写形式引用查询而不是完整地解释查询通常会很有帮助。此处列出的查询编号为Q1, Q2, 依此类推, 这就是我们在示例中移动时如何在图中引用它们的方式。

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

作为数据建模人员, 我们的自然趋势是首先集中精力设计用于存储预订和来宾记录的表, 然后才开始考虑可以访问它们的查询。当我们之前开始讨论购物查询时, 你可能已经感到过类似的压力, 并想“但是, 酒店和兴趣点数据从何而来?”不用担心, 我们会尽快解决。以下是一些查询, 描述了我们的用户将如何访问预订:

  • Q6。通过确认号码查找预订。
  • Q7。通过酒店, 日期和来宾姓名查找预订。
  • Q8。通过来宾名称查找所有预订。
  • Q9。查看客人的详细信息。

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

图1-3。

酒店申请查询

数据建模逻辑

定义好查询之后, 我们就可以开始设计Cassandra表了。首先, 我们将创建一个逻辑模型, 其中包含每个查询的表格, 并从概念模型中捕获实体和关系。

为命名每个表, 我们将标识要查询的主要实体类型, 并使用该类型来开始实体名称。如果要按其他相关实体的属性进行查询, 则将其附加到表名中, 并用_by_分隔。例如, hotels_by_poi。

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

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

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

Chebotko图介绍

Cassandra社区中的一些人已经提出了表示法, 以图形方式捕获数据模型。我们选择使用由Artem Chebotko流行的符号, 该符号提供了一种简单而翔实的方式来可视化设计中查询和表格之间的关系。图1-4显示了逻辑数据模型的Chebotko表示法。

图1-4。

Chebotko逻辑图

每个表均显示其标题和列列表。主键列通过符号来标识, 例如K代表分区键列, 而C↑或C↓则代表聚类列。显示行进入表或表之间的行, 以指示每个表旨在支持的查询。

酒店逻辑数据模型

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

图1-5。

酒店领域逻辑模型

让我们探索每个表格的详细信息。

我们的第一个查询Q1是查找景点附近的酒店, 因此我们将其称为表格hotels_by_poi。我们正在按已命名的兴趣点进行搜索, 因此可以暗示该兴趣点应成为我们主键的一部分。让我们按名称引用兴趣点, 因为根据我们的工作流程, 这就是用户将如何开始搜索的方式。

你会注意到, 在给定的兴趣点附近, 我们肯定会拥有多家酒店, 因此我们在主键中需要另一个组件, 以确保每个酒店都有唯一的分区。因此, 我们将酒店钥匙添加为集群列。

注意

使你的主键唯一

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

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

从工作流程图中, 我们知道, hotels_by_poi表用于显示带有每个酒店基本信息的酒店列表, 并且应用程序知道返回的酒店的唯一标识符。当用户选择旅馆以查看详细信息时, 我们可以使用Q2, 该Q2用于获取有关旅馆的详细信息。由于我们已经有了第一季度的hotel_id, 因此我们将其用作要查找的酒店的参考。因此, 我们的第二张桌子被称为酒店。

另一个选择是将一组poi_names存储在hotels表中。这是同样有效的方法。你将通过经验学习哪种方法最适合你的应用程序。

注意

使用唯一标识符作为参考

你会发现, 使用唯一ID唯一地引用元素, 以及将这些uuid用作代表其他实体的表中的引用, 通常会很有帮助。这有助于最小化不同实体类型之间的耦合。如果你为应用程序使用微服务架构样式, 其中每种实体类型都有单独的服务, 这可能会特别有用。

但是, 出于本书的目的, 我们将主要使用文本属性作为标识符, 以使示例简单易读。例如, 酒店业的惯例是使用“ AZ123”或“ NY229”之类的短代码来引用属性。我们会将这些值用于我们的hotel_id, 同时要知道它们不一定是全局唯一的。

第3季度与第1季度恰好相反, 它在寻找酒店附近的兴趣点, 而不是寻找兴趣点附近的酒店。但是, 这一次, 我们需要访问每个兴趣点的详细信息, 如pois_by_hotel表所示。正如我们之前所做的那样, 我们将兴趣点名称添加为聚类键以确保唯一性。

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

注意

搜索范围

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

为了使数据模型的购物部分更完整, 我们添加了comforts_by_room表以支持Q5。这将使我们的用户可以在所需的住宿日期内查看其中一间客房的设施。

预留逻辑数据模型

现在, 我们切换一下齿轮以查看预订查询。图1-6显示了用于保留的逻辑数据模型。你会注意到这些表代表了非规范化的设计;相同的数据出现在多个表中, 但键不同。

图1-6。

用于预订的非规范化逻辑模型

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

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

旅馆工作人员可能希望按日期查看即将到来的预订记录, 以便洞悉旅馆的经营状况, 例如旅馆售罄或售罄的日期。 Q8支持按日期检索给定酒店的预订。

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

注意

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

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

模式和反模式

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

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

时间序列模式对于测量以外的数据也很有用。考虑银行应用程序的示例。我们可以连续存储每个客户的余额, 但是当各个客户检查他们的余额或进行交易时, 这可能导致大量读写争用。我们可能会想将交易封装在写入内容的周围, 只是为了保护余额不被错误地更新。相反, 时间序列样式的设计会将每个事务存储为带有时间戳的行, 而将计算当前余额的工作留给应用程序。

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

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

物理数据建模

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

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

分配数据类型后, 我们将通过执行大小计算并测试模型的工作方式来分析模型。我们可能会根据我们的发现进行一些调整。通过查看示例, 我们将再次详细介绍数据建模过程。

在开始之前, 我们先来看一下Chebotko物理数据模型符号的一些补充。

Chebotko物理图

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

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

图1-7。

为物理数据模型扩展Chebotko表示法

酒店物理数据模型

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

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

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

图1-8。

酒店实物模型

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

注意

利用用户定义的类型

像我们使用地址用户定义类型所做的那样, 利用用户定义类型通常有助于减少非主键列的重复。这样可以减少设计的复杂性。

请记住, UDT的范围是定义它的键空间。要在我们将要设计的预订键空间中使用地址, 我们将不得不再次声明它。这只是我们在数据模型设计中必须进行的众多取舍之一。

预留物理数据模型

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

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

图1-9。

预订物理模型

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

物化视图

引入了实例化视图来帮助解决二级索引的一些缺点, 我们在第4章中已经讨论了这些缺点。在基数高的列上创建索引往往会导致性能下降, 因为查询了环需求中的大多数或所有节点。

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

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

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

为了了解与实例化视图相关的语法和约束, 我们将看一下CQL命令, 该命令从保留物理模型创建了Reservations_by_confirmation表:

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早期版本中)从基表中创建带有多个非主键列的物化视图, 所以这将要求我们在hotels_by_confirmation中将hotel_id或date指定为集群列。两种设计都是可以接受的, 但是这应该可以让你更深入地了解在选择几种非规格化表设计中的哪一种用作基础表时要考虑的折衷。

评估和完善

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

计算分区大小

我们要查找的第一件事是表中的分区是否会过大, 或者换句话说, 是分区过大。分区大小由存储在分区中的单元格(值)的数量来衡量。 Cassandra的硬限制是每个分区20亿个单元, 但是在达到该上限之前, 我们可能会遇到性能问题。

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

\(N_v = N_r(N_c – N_ {pk} – N_s)+ N_s \)

分区(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 = 1N_r \)

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

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

\(N_r = 100 \:\ mathrm {房间/旅馆} \时间730 \:\ mathrm {天} = 73, 000 \:\ mathrm {行} \)

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

注意

最坏情况的估计

在执行大小计算时, 很容易假设变量的标称或平均大小(例如行数)。还应考虑计算最坏的情况, 因为在成功的系统中, 这类预测可以成为现实。

计算磁盘上的大小

除了计算分区的大小, 对于我们来说, 估计计划存储在集群中的每个表所需的磁盘空间量也是一个好主意。为了确定大小, 我们使用以下公式确定分区的大小St:

\(S_t = \ displaystyle \ sum_i sizeOf \ big(c_ {k_i} \ big)+ \ displaystyle \ sum_j sizeOf \ big(c_ {s_j} \ big)+ N_r \ times \ bigg(\ displaystyle \ sum_k sizeOf \ big( c_ {r_k} \ big)+ \ displaystyle \ sum_l sizeOf \ big(c_ {c_l} \ big)\ bigg)+ N_v \ times sizeOf \ big(t_ {avg} \ big)\)

这比我们以前的公式复杂一点, 但是我们将一次分解一下。我们先来看一下表示法:

在这个公式中, c

指分区键列, c

s

到静态列, c

[R

到常规列, 然后c

C

群集列。

术语t

平均

指每个单元存储的元数据的平均字节数, 例如时间戳。通常为此值使用8个字节的估计值。

我们认识到行数N

[R

和值的数量N

v

根据我们之前的计算

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内置限制的宽行样式表。对表执行大小分析可能会发现分区可能太大, 无论分区是值的数量, 磁盘上的大小还是两者都有。

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

继续检查我们的可用客房示例, 如果将date列添加到available_rooms_by_hotel_date表的分区键中, 则每个分区将代表特定日期特定酒店的客房可用性。由于连续几天的数据可能会在单独的节点上, 因此肯定会产生明显较小的分区, 甚至可能太小。

通常使用另一种称为存储的技术将数据分为中等大小的分区。例如, 我们可以通过在分区键上添加一个month列来对available_rooms_by_hotel_date表进行存储。尽管此列部分复制了日期, 但它提供了一种很好的方式将相关数据分组到不会太大的分区中。

如果我们真的很想保留宽行设计, 可以改为将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开发中心

我们已经有很多使用cqlsh创建架构的实践, 但是现在我们开始创建具有更多表的应用程序数据模型, 因此要跟踪所有CQL成为更大的挑战。

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

图1-10。

在DataStax DevCenter中编辑酒店架构

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

总结

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

赞(0)
未经允许不得转载:srcmini » 为Cassandra设计数据模型

评论 抢沙发

评论前必须登录!