选择正确的数据模型是使用Cassandra最困难的部分。如果你有关系型数据库背景,那么CQL看起来很熟悉,但是你使用它的方式可能会有很大的不同。这篇文章的目的是帮助你在设计Cassandra数据模型时应该记住的基本规则。如果你遵循这些规则,你会得到相当不错的回报。更好的是,在将节点添加到群集时,性能应该按线性调整。
Non-Goals
来自关系型数据库背景的开发人员通常会使用关系型数据库建模的规则应用于Cassandra。
为了避免浪费时间在Cassandra无关的规则上,我想指出一些非目标(non-goal):
尽量减少写操作的次数
写在Cassandra的代价不是免费的,但他们非常便宜。Cassandra针对高写入吞吐量进行了优化,几乎所有的写入操作都是高效的。如果您通过执行额外的写入来提高读取查询的效率,那么会是一个好的权衡。因为读操作往往是更昂贵的,更难以调整。
尽量减少数据复制
非规范化和重复数据是Cassandra的一个事实。不要害怕它,磁盘空间通常是最便宜的资源(与CPU,内存,磁盘IOP或网络相比),而Cassandra是围绕这一事实构建的。为了获得最高效的读取,您经常需要复制数据。
此外,Cassandra没有JOIN操作,如果你还惦记着这些,那么你不是真的想用分布式的方式来使用它们。
Basic Goals
这是数据模型的两个高级目标:
1.在集群周围均匀分布数据
2.最小化读取的分区数量
还有其他更小的目标,但这些是最重要的。大多数情况下,我将重点讨论实现这两个目标的基础知识。还有其他一些奇特的技巧可以使用,但是首先你应该知道如何评估它们。
规则1:在集群周围均匀分布数据
您希望群集中的每个节点具有大致相同的数据量。Cassandra使这个很容易实现,但它不是一个给定的值。行根据分区键的散列分布在集群周围,这是PRIMARY KEY的第一个元素。所以,平均分配数据的关键是:选一个好的主键。我会解释一下如何做到这一点。
规则2:最小化读取的分区数量
行上的分区都共享相同的分区键(partition key)。当您发出读取查询时,您希望从尽可能少的分区中读取行。
为什么这很重要?每个分区可能驻留在不同的节点上。协调员通常需要为每个请求的分区发出不同的命令。这增加了很多开销并增加了延迟的变化。而且,即使在单个节点上,由于存储行的方式,从多个分区读取比从单个分区读取更昂贵。
规则冲突
如果最大限度地减少读取的分区数量,为什么不把所有内容放在一个大的分区中?您最终会违反规则1,即在集群周围均匀分布数据。
关键是,这两个目标经常发生冲突,所以你需要努力平衡它们。
围绕您的查询建模
最小化分区读取的方法是对数据进行建模以适合您的查询。不要围绕着关联建模,不要围绕着实体建模。围绕您的查询模型。以下是你如何做到这一点:
第1步:确定支持哪些查询
确定您需要支持的查询。这可能包括许多您一开始可能不会想到的考虑事项。例如,您可能需要考虑:
l 按属性分组
l 按属性排序
l 基于一些条件进行过滤
l 强化结果集的唯一性(去重)
l 等等。。
对其中一个查询需求的更改常常需要一个数据模型更改以获得最大的效率。
第2步:尝试创建一个表,您可以通过检索(大致)一个分区来满足您的查询
在实践中,这通常意味着您将每个查询模式大致使用一个表。如果您需要支持多种查询模式,则通常需要多个表。
换句话说,每个表都应该预先为您需要支持的高级查询构建“答案”。如果你需要不同类型的答案,你通常需要不同的表格。这是你优化查询的方法。
记住,数据重复是可以的。许多表格可能会重复相同的数据。
实战:例子
为了展示一个良好的思维过程的例子,我将引导你通过一个简单的问题的数据模型的设计。
示例1:用户查找
须求是“我们有一些用户,想要查找他们”。我们来看看这些步骤:
第1步:确定要支持哪些特定的查询
比方说,我们希望能够通过他们的用户名或电子邮件来查找用户。通过这个查询方法,我们应该得到用户的所有细节。
第2步:尝试创建一个表,您可以通过查询(大致)一个分区来满足您的查询
由于我们希望通过查询方法获取用户的完整详细信息,因此最好使用两个表格:CREATE TABLE users_by_username (
username text PRIMARY KEY,
email text,
age int
)
CREATE TABLE users_by_email (
email text PRIMARY KEY,
username text,
age int
)
现在,我们来看看这个模型的两个规则:
数据均匀分布?每个用户都有自己的分区,所以是的。
最小的分区读取?我们只需要阅读一个分区,所以是的。
现在,我们假设试图用non-goals方式,然后提出这个数据模型:
CREATE TABLE users (
id uuid PRIMARY KEY,
username text,
email text,
age int
)
CREATE TABLE users_by_username (
username text PRIMARY KEY,
id uuid
)
CREATE TABLE users_by_email (
email text PRIMARY KEY,
id uuid
)
这个数据模型也均匀分布数据,但是有一个缺点:我们现在必须读取两个分区,一个来自users_by_username(或users_by_email),另一个来自用户。所以读取大概是两倍昂贵。
例子2:用户组
现在的须求已经改变了。用户有一个分组属性,我们希望获得一个组中的所有用户。
第1步:确定要支持哪些特定的查询
我们希望获取特定组中每个用户的完整用户信息。用户顺序无关紧要。
第2步:尝试创建一个表,您可以通过查询(大致)一个分区来满足您的查询
我们如何将一个组合划分成一个分区?我们可以使用一个复合主键来做:
CREATE TABLE groups (
groupname text,
username text,
email text,
age int,
PRIMARY KEY (groupname, username)
)
请注意,PRIMARY KEY包含两个组件:groupname(分区键)和username(称为集群键)。这会给我们每个groupname分配一个分区。在特定的分区(组)中,行将按用户名排序。获取一个组的操作就会变得很简单:
| |
SELECT * FROM groups WHERE groupname = ?
|
这符合最小化读取的分区数量的目的,因为我们只需要读取一个分区。但是,在集群周围均匀分布数据的第一个目标方面做得并不好。如果我们有成千上万的小组,每个组有数百的用户,我们的数据将会分布得很均匀。但是如果我们有一个组有数百万的用户,整个数据将由一个节点(或一组副本)承担存放。
如果我们想更均匀地分散数据到各个节点,我们可以使用一些策略。基本技巧是将另一列添加到PRIMARY KEY以形成复合分区键。这里有一个例子:
CREATE TABLE groups (
groupname text,
username text,
email text,
age int,
hash_prefix int,
PRIMARY KEY ((groupname, hash_prefix), username)
)
新添加的列hash_prefix,prefix的前面的hash是username的hash值。比如,他可以是username的hash值然后进行模运算除以4(比如有四个节点)的第一个字节。与groupname一起,这两列构成复合分区键。这样的话每个组的数据将分布在四个分区上,而不是全部留在一个分区上。虽然我们的数据分布更均匀,但我们现在必须从四个分区上读取数据,这样读取的次数是单个分区读取的4倍。这是两个规则冲突的一个例子。您需要为您的特定用例找到一个好的平衡点。如果你读取须求很大,并且group的数据不是很大,也许把模运算数值从4改为2是个不错的选择。另一方面,如果你做的读的次数很少,但是每个组的数据很大,将四变成十是更好的选择。
还有其他的方法来分割一个分区,我将在下一个例子中介绍。
在我们继续之前,让我指出一些关于这个数据模型的其他内容:我们可能多次复制用户信息,每个组都重复一次。您可能会尝试使用这样的数据模型来减少重复:
CREATE TABLE users (
id uuid PRIMARY KEY,
username text,
email text,
age int
)
CREATE TABLE groups (
groupname text,
user_id uuid,
PRIMARY KEY (groupname, user_id)
)
显然,这可以最大限度地减少重复。但是我们需要读多少个分区?如果一个组有1000个用户,我们需要读取1001个分区。这大概比我们的第一个数据模型要贵100倍。如果查询对效率要求比较高,这不是一个好的模型。另一方面,如果查询不是很频繁,但是更新用户信息(比如用户名)是很经常用,那么这个数据模型可能是有意义的。所以在你设计你的数据库的时候你必须将读取/更新比率考虑进去。
例子3:用户组添加****Join Date****属性
假设我们继续前面的例子(例子2),但是需要增加获取组中最新用户的须求。
我们可以新建一个类似的表:
CREATE TABLE group_join_dates (
groupname text,
joined timeuuid,
username text,
email text,
age int,
PRIMARY KEY (groupname, joined)
)
在这里,我们使用一个timeuuid(这就像一个时间戳,但避免冲突)作为clustering列。在一个组(分区)内,行将在用户加入组时进行排序。这使我们能够像这样获得一个组中的最新用户:
SELECT * FROM group_join_dates
WHERE groupname = ?
ORDER BY joined DESC
LIMIT ?
这是相当高效的,因为我们正在从单个分区读取行数据。但是,不要总是使用ORDER BY和DESC关键词,这会使查询效率降低,我们可以简单通过建表的语句将写入时就进行排序:
CREATE TABLE group_join_dates (
groupname text,
joined timeuuid,
username text,
email text,
age int,
PRIMARY KEY (groupname, joined)
) WITH CLUSTERING ORDER BY (joined DESC)
现在我们可以使用更高效的查询:
SELECT * FROM group_join_dates
WHERE groupname = ?
LIMIT ?
和前面的例子一样,如果组里的数据变得太大,我们不能在集群周围均匀分布数据。
在这个例子中,我们有些随机地分割partition,但在这种情况下,我们可以利用‘时间’来把partition进行分割。
例如,我们利用’date’来分割partition:
CREATE TABLE group_join_dates (
groupname text,
joined timeuuid,
join_date text,
username text,
email text,
age int,
PRIMARY KEY ((groupname, join_date), joined)
) WITH CLUSTERING ORDER BY (joined DESC)
我们再次使用复合分区键,但是这次我们使用了join_date这个字段。每一天,都会生成一个新的分区。在查询x组最新的用户时,我们首先查询今天的所在分区,然后是昨天前。。等等,直到我们找到这个用户。在查询到结果时,我们可能需要读取多个分区。
为了最大限度地减少需要查询的分区数量,请尝试选择一个partition的时间范围,通常只须查询一个或两个分区。例如,如果我们通常需要10个最新的用户,而一个组通常每天要获得3个用户,那么我们须要将join_date的值从每天改成4天[1]。
[1]: I suggest using a timestamp truncated by some number of seconds. For example, to handle four-day ranges, you might use something like this:
now = time()
four_days = 4 * 24 * 60 * 60
shard_id = now - (now % four_days)
参考文献:https://www.datastax.com/dev/blog/basic-rules-of-cassandra-data-modeling
网友评论