1. 概述html
Apache Cassandra将数据存储在表中,每一个表都由行和列组成。CQL(Cassandra查询语言)用于查询存储在表中的数据。Apache Cassandra数据模型基于查询并针对查询进行了优化。Cassandra不支持用于关系数据库的关系数据建模。Cassandra数据建模专一于查询。数据库
Cassandra中的数据建模使用查询驱动(query-driven)的方法,其中特定查询是组织数据的关键。查询(Query)是从表中选择数据的结果,模式(Schema)是对表中数据的排列方式的定义。Cassandra的数据库设计基于对快速读写的需求,所以架构设计越好,数据写入和检索的速度就越快。apache
相反,关系型数据库根据设计的表和关系对数据进行规范化,而后编写将要进行的查询。关系数据库中的数据建模是表驱动(table-driven)的,表之间的任何关系都表示为查询中的表链接。架构
1.1. 什么是数据建模数据库设计
数据建模是识别实体及其关系的过程。在关系数据库中,数据放在具备外键的规范化表中,外键用于引用其余表中的相关数据。应用程序将进行的查询由表的结构驱动,相关数据做为表链接进行查询。分布式
在Cassandra中,数据建模是查询驱动(query-driven)的。 数据访问模式和应用程序查询肯定数据的结构和组织,而后将其用于设计数据库表。ide
数据围绕特定查询建模。查询的最佳设计是访问单个表,这意味着查询中涉及的全部实体必须位于同一表中,以使数据访问(读取)变得很是快。 数据被建模为最适合一个查询或一组查询。一个表可能具备一个或多个最适合查询的实体。因为实体之间一般确实具备关系,而且查询可能涉及实体之间具备关系的实体,所以单个实体能够包含在多个表中。post
1.2. 查询驱动的建模性能
在关系数据库模型中,查询使用表链接从多个表获取数据,而在Cassandra中不支持链接,所以全部必需字段(列)必须组合在一个表中。因为每一个查询都由一个表支持,所以在称为非规范化的过程当中,数据会在多个表之间冗余。 数据冗余和高写入吞吐量用于实现高读取性能。 优化
1.3. 目标
主键(primary key)和分区键(partition key)的选择对于在整个集群中均匀分布数据很重要。使查询读取的分区数量保持最少也很重要,由于不一样的分区可能位于不一样的节点上,而且协调器将须要向每一个节点发送请求,从而增长了请求的开销和延迟。即便查询中涉及的不一样分区位于同一节点上,较少的分区也能够提升查询效率。
1.4. 分区
分区键(partition key)是从主键(primary key)的第一个字段生成的。使用分区键分区到哈希表的数据能够提供更快的查询。用于查询的分区越少,查询的响应时间就越快。
下面是一个分区的例子,假设表t有一个主机id
CREATE TABLE t ( id int, k int, v text, PRIMARY KEY (id) );
分区键是从主键id生成的,用于在群集中的各个节点之间进行数据分配。
下面这个例子,是一个复合主键
CREATE TABLE t ( id int, c text, k int, v text, PRIMARY KEY (id,c) );
对于具备复合主键的表t,第一个字段id用于生成分区键,第二个字段c是用于在分区内排序的聚类键。使用聚类键对数据进行排序能够提升检索相邻数据的效率。
一般,对主键的第一个字段进行哈希处理以生成分区键,而其他字段则是用于对分区内的数据进行排序的聚类关键字。对数据进行分区能够提升读写效率。不是主键字段的其余字段能够单独创建索引,以进一步提升查询性能。
接下来这个例子,id1和id2用户生成分区键,c1和c2用于在分区内排序的聚类关键字。
CREATE TABLE t ( id1 int, id2 int, c1 text, c2 text k int, v text, PRIMARY KEY ((id1,id2),c1,c2) );
1.5. 与关系数据模型比较
关系数据库使用外键将数据存储在与其余表有关系的表中。关系数据库的数据建模方法是以表为中心的。查询必须使用表链接从多个表中获取数据,这些表之间存在关系。Apache Cassandra没有外键或关系完整性的概念。Cassandra的数据模型是基于设计高效的查询,不涉及多个表的查询。关系数据库对数据进行规范化以免重复。相反,Cassandra经过在以查询为中心的数据模型的多个表中冗余数据来对数据进行非规范化。若是Cassandra数据模型不能彻底整合用于特定查询的不一样实体之间关系的复杂性,则可使用应用程序代码中的客户端链接(client-side joins)。
1.6. 数据建模示例
假设有一组杂志数据,属性有杂志id、杂志名称、出版频率、出版日期和出版商。
查询一:列出全部杂志名称,包括其发布频率。
因为不须要查询全部属性,所以数据模型将仅由ID(用于分区键),杂志名称和发布频率组成,以下图所示:
查询二:按出版商列出全部杂志名称
输出列增长出版商,同时用出版商做分区键,以下图所示:
1.7. 定义Schema
对于查询一,定义以下:
CREATE TABLE magazine_name ( id int PRIMARY KEY, name text, publication_requency text )
对于查询二,定义以下:
CREATE TABLE magazine_publisher ( publisher text, id int, name text, publication_requency text, PRIMARY KEY (publisher, id) ) WITH CLUSTERING ORDER BY (id DESC)
2. 概念数据建模
首先,建立一个简单的域模型,它在关系型世界中很容易理解,而后看看如何在Cassandra中将它从关系型映射到分布式哈希表模型。
以酒店预订为例,概念性领域包括酒店、入住酒店的客人、每一个酒店的房间集合、这些房间的价格和空房状况以及为客人预订的预订记录。酒店一般还会维护“景点”的集合,这些景点包括公园,博物馆,购物画廊,古迹或客人在住宿期间可能要参观的酒店附近的其余地方。旅馆和兴趣点都须要维护地理位置数据,以即可以在地图上找到它们进行混搭,并计算距离。ER图以下:
一目了然,一个酒店有多个房间,一个房间里面有多个休闲设施,房间的空闲状况也是分时段的,酒店附近有多个景点,一位顾客能够订多个房间,每个预订记录对应多个房间。
3. 关系型数据库设计
当咱们构建一个新的数据驱动应用程序时,将使用关系型数据库。首先,将领域对象转化为一组规范化的表,并使用外键引用其余表中的相关数据。
3.1. RDBMS和Cassandra之间的设计差别
• 没有链接(join)
在Cassandra中,没法执行链接(join)操做。若是你已经设计了一个数据模型而且须要一个链接其它表的数据,那么你不得不在客户端作这种链接,或者建立一个非规范化的第二个表来表示链接结果,后一种方法是Cassandra数据建模的首选方法。
• 没有外键引用
在关系数据库中,能够在表中指定外键来引用另外一个表的主键。但Cassandra并无强制要求必须定义外键来引用。在表中存储与其余实体相关的ID仍然是常见的设计需求,可是级联删除等操做不可用。
• 非规范化
在关系数据库设计中,常常会强调范式重要性,数据库设计三范式。可是在Cassandra中,遵循范式不是一个好的选择,由于一般在不遵循范式时执行得最好。
关系数据库故意去规范化的第二个缘由是须要保留的业务文档结构。也就是说,有一个封闭的表,它引用了不少外部表,这些表的数据可能会随着时间的变化而变化,可是你须要将封闭的文档保存为历史记录中的一个快照。这里最多见的例子是发票。假设已经有了customer和product表,可能你认为能够只制做一个针对这些表的发票,但在实践中永远不该该这样作。顾客或价格信息可能会改变,而后你将失去失去发票单据上发票日期的完整性,这可能违反审计、报告、或法律,并致使其余问题。能够看到,在这种状况下,冗余是必要的。
在Cassandra中,非规范化是彻底正常的。
• 查询优先
简单的来讲,关系建模意味着概念领域模型开始,用表来表示领域对象,用表字段来表示领域对象的属性,而后设置主键和外键来表示领域对象以前的关系。若是有多对多的关系,还要再建一张中间表。关系世界中的查询是次要的。只要对表进行适当的建模,就能够始终得到所需的数据。即便必须使用几个复杂的子查询或链接语句,这一般也是正确的。
相比之下,在Cassandra中,不是从数据模型开始的,而是从查询模型开始的。Cassandra不是先对数据建模,而后编写查询,而是先对查询建模,让数据围绕查询进行组织。考虑应用程序将使用的最多见的查询路径,而后建立支持它们所需的表。
• 最佳存储设计
在关系数据库中,对于用户来讲,表是如何存储在磁盘上的一般是透明的,基本不用关心。然而,这是Cassandra中的重要考虑因素。因为Cassandra表都存储在磁盘上的独立文件中,所以将相关列一块儿定义在同一个表中很是重要。
在Cassandra中建立数据模型时,一个关键目标是最小化必须搜索的分区数量,以知足给定的查询。因为分区是不跨节点划分的存储单元,所以搜索单个分区的查询一般会产生最佳性能。
• 排序是一个设计决策
在RDBMS中,能够经过在查询中使用ORDER BY轻松地更改记录返回的顺序。默认的排序顺序是不可配置的;默认状况下,记录是按照写入的顺序返回的。若是要更改顺序,只需修改查询便可,而且能够根据任何列进行排序。
可是,在Cassandra中,排序的处理方式有所不一样。这是一个设计决策。查询中可用的排序顺序是固定的,而且彻底由在CREATE TABLE命令中提供的集群列的选择肯定。CQL SELECT语句确实支持ORDER BY语义,但仅按聚簇列指定的顺序。
4. 定义应用程序的查询
既然是查询驱动的,那么就先看看针对酒店预订这个例子,业务都须要查询,毕竟技术是为业务服务的,抛开业务弹设计是不可取的。
(画外音:此刻,忽然想到那句“技术支撑商业、技术拓展商业、技术创做商业”)
言归正传,在酒店预订的例子中,能够大体梳理出如下业务查询:
Q1: 查找某个景点附近的酒店
Q2: 查找某个酒店的信息
Q3: 查找某个酒店附近的景点
Q4: 在给定的日期范围内找到一个可用的房间
Q5: 查找房间的价格和设施
Q6: 经过确认码查找预订
Q7: 根据酒店、日期和顾客姓名查找预订
Q8: 按顾客姓名查找全部预订
Q9: 查看顾客详细信息
5. 逻辑数据建模
为了更生动形象地表示数据模型,这里采用下面这种图表方式:
5.1. Hotel逻辑数据模型
按照上面的图表方式,酒店逻辑数据模型表示以下:
5.2. Reservation逻辑数据模型
一样的方式,预订逻辑数据模型表示以下:
6. 物理数据建模
(画外音:一切设计都是为了查询,这话听着很耳熟,哈哈,Elasticsearch说过,一切设计都是为了提升检索的性能)
为了方便理解,采用以下格式来表示。不用多说,看图说话:
酒店数据模型:
预订数据模型:
7. 评估和完善数据模型
7.1. 计算分区大小
首先要考虑的是,表的分区是否太大,或者换句话说,太宽。分区大小是经过存储在分区中的单元格(值)的数量来度量的。Cassandra的硬限制是每一个分区有20亿个单元格(PS:类比Excel中的单元格),可是在达到这个限制以前,可能会遇到性能问题。
分区大小计算公式:N_v = N_r (N_c - N_{pk} - N_s) + N_s
其中:
N_r表示行数
N_c表示列数
N_pk表示主键列数
N_s表示静态列数
N_v表示单元格数量
那么,单元格数量 = 行数 × (总列数 - 主键列数 - 静态列数) + 静态列数
以available_rooms_by_hotel_date表为例,根据公式,该表的单元格总数 = 行数 × (4 - 3 -0) + 0
7.2. 计算磁盘大小
每种数据类型所占磁盘空间大小不一,粗略地能够用全部列所占磁盘大小乘以行数来计算
7.3. 拆分大分区
有一种称为bucketing的技术常被用来将数据分割成中等大小的分区。例如,能够经过向分区键添加一个month列(可能表示为一个整数)来分解available_rooms_by_hotel_date表。与原设计的对好比下图所示。虽然month列部分重复了date,但它提供了一种很好的方法,能够在分区中对相关数据进行分组,并且分区不会变得太大。
8. 定义数据库Schema
schema能够理解为数据库,一个schema就是指一个数据库
下面是为hotel keyspace定义的schema:
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 ) 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’;
reservation keyspace 的 schema 以下:
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_confirmation ( confirm_number text, hotel_id text, start_date date, end_date date, room_number smallint, guest_id uuid, PRIMARY KEY (confirm_number) ) WITH comment = ‘Q6. Find reservations by confirmation number’; 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 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, phone_numbers list, addresses map<text, frozen<address>, confirm_number text ) WITH comment = ‘Q9. Find guest by ID’;
9. 文档
https://cassandra.apache.org/doc/latest/data_modeling/index.html
https://cassandra.apache.org/doc/latest/data_modeling/intro.html
https://cassandra.apache.org/doc/latest/data_modeling/data_modeling_rdbms.html