在Cassandra中对版本化层次结构进行高效建模

时间:2014-08-22 14:40:03

标签: cassandra data-modeling hierarchical-data cassandra-2.0

声明:
这是一个很长的帖子。我首先解释我正在处理的数据,以及我想用它做什么 然后我详细介绍了我考虑过的三种可能的解决方案,因为我已经尝试过做作业了(我发誓:])。我最终得到了“最佳猜测”,这是第一种解决方案的变体。

我的最终问题是:使用Cassandra解决问题最明智的方法是什么?这是我的尝试之一,还是别的什么? 我正在寻找经验丰富的Cassandra用户的建议/反馈......

我的数据:
我有很多SuperDocuments在树形结构(标题,副标题,部分......)中拥有文档。

每个SuperDocument结构都可以随着时间的推移而改变(主要是重命名标题),从而为我提供了多个版本的结构,如下所示。

superdocument versions

我在寻找什么:
对于每个SuperDocument,我需要按照上面的日期对这些结构加时间戳,并且我希望在给定的日期找到最接近的早期版本的SuperDocument结构。 (即version_date < given_date

的最新版本

这些考虑可能有助于更轻松地解决问题:

  • 版本是不可变的:变化非常罕见,我可以在每次更改时创建整个结构的新表示。
  • 我不需要访问结构的子树。
  • 我说可以说我不需要找到给定叶子的所有祖先,也不需要访问树内的特定节点/叶子。一旦我拥有整棵树,我就可以在我的客户端代码中完成所有这些工作。

好吧,让我们这样做
请记住,我真的只是开始使用Cassandra。我已经阅读/观看了很多关于数据建模的资源,但是在该领域没有太多(任何!)经验!
这也意味着一切都将用CQL3编写...对不起节俭爱好者!

我解决这个问题的第一个尝试是创建下表:

CREATE TABLE IF NOT EXISTS superdoc_structures (
    doc_id varchar,
    version_date timestamp,
    pre_pos int,
    post_pos int,
    title text,

    PRIMARY KEY ((doc_id, version_date), pre_pos, post_pos)

) WITH CLUSTERING ORDER BY (pre_pos ASC);

这会给我以下结构:

enter image description here

我在这里用树Nested Sets model;我认为保持结构有序会很好,但我愿意接受其他建议。

我喜欢这个解决方案:每个版本都有自己的行,其中每列代表层次结构的级别 但问题是我(坦率地)打算按如下方式查询我的数据:

SELECT * FROM superdoc_structures 
    WHERE doc_id="3399c35...14e1" AND version_date < '2014-03-11' LIMIT 1

卡珊德拉很快提醒我,我不被允许这样做! (因为分区程序不保留群集节点上的行顺序,因此无法扫描分区键)

那么......?
好吧,因为Cassandra不会让我在分区键上使用不等式,所以就这样吧! 我将version_date作为一个聚类键,我所有的问题都将消失。是的,不是真的......

首先尝试:

CREATE TABLE IF NOT EXISTS superdoc_structures (
    doc_id varchar,
    version_date timestamp,
    pre_pos int,
    post_pos int,
    title text,

    PRIMARY KEY (doc_id, version_date, pre_pos, post_pos)

) WITH CLUSTERING ORDER BY (version_date DESC, pre_pos ASC);

我发现这个不太优雅:所有版本结构级别都被制作成一个非常宽的行的列(与我以前的解决方案相比):

second modeling attempt

问题:使用相同的请求,使用LIMIT 1只会返回第一个标题。并且使用no LIMIT将返回所有版本结构级别,我必须过滤以仅保留最新版本。

第二次尝试:

还没有第二次尝试......但我有一个想法,但我觉得它没有明智地使用Cassandra。

我们的想法是仅按version_date进行集群,以某种方式将整个层次结构存储在每个列值中。听起来不好不是吗?

我会做这样的事情:

CREATE TABLE IF NOT EXISTS superdoc_structures (
    doc_id varchar,
    version_date timestamp,
    nested_sets map<int, int>,
    titles list<text>,

    PRIMARY KEY (doc_id, version_date)

) WITH CLUSTERING ORDER BY (version_date DESC);

结果行结构将为:

third modeling attempt

事实上,我看起来没那么好,但是我可能会有更多的数据而不是级别标题来反规范化到我的列中。如果它只有两个属性,我可以使用另一个地图(例如将标题与ID相关联),但更多的数据会导致更多的列表,我觉得它很快就会变成反模式。
另外,当数据进入时,我必须在我的客户端应用程序中合并所有列表!

ALTERNATIVE&amp;最佳GUESS
在给予它更多的思考之后,有一个“混合”解决方案可能会起作用并且可能是高效和优雅的:

我可以使用另一个只列出SuperDocument&amp;的版本日期的表。将这些日期缓存到Memcache实例(或Redis或其他)中以便快速访问 这样我就可以快速找到我需要获取的版本,然后使用我的第一个解决方案的复合键来请求它。

这是两个查询,还有一个要管理的内存缓存存储。但无论如何我最终可能会有一个,所以也许这是最好的妥协? 也许我甚至不需要缓存存储?

总而言之,我真的觉得第一个解决方案是对数据建模最优雅的解决方案。你呢?!

1 个答案:

答案 0 :(得分:4)

首先,您不需要使用memcache或redis。 Cassandra将为您提供非常快速的访问信息。你当然可以有一个类似的表:

create table superdoc_structures {
    doc_id varchar;
    version_date timestamp;
    /* stuff */
    primary key (doc_id, version_date)
} with clustering order by (version_date desc);

这将为您提供访问给定版本的快速方法(此查询可能看起来很熟悉; - ):

select * from superdoc_structures 
    where doc_id="3399c35...14e1" and
        version_date < '2014-03-11'
    order by version_date desc
    limit 1;

由于文档树结构的任何内容似乎都与架构的观点无关,而且每当有新版本时,您都很高兴能够完整地创建文档,我不明白为什么你甚至打扰树分开排。为什么不将表中的整个文档作为文本或blob字段?

create table superdoc_structures {
    doc_id varchar;
    version_date timestamp;
    contents text;
    primary key (doc_id, version_date)
} with clustering order by (version_date desc);

因此,要在新的一年中获取文档的内容,您需要:

select contents from superdoc_structures
where doc_id="...." and 
    version_date < '2014-01-1'
order by version_date > 1

现在,如果 希望维护文档组件的某种层次结构,我建议您使用closure table表来表示它。或者,既然您愿意在每次写入时复制整个文档,为什么不在每次写入时复制整个部分信息,为什么不这样做并且具有如下模式:

create table superdoc_structures {
    doc_id varchar;
    version_date timestamp;
    section_path varchar;
    contents text;
    primary key (doc_id, version_date, section_path)
) with clustering order by (version_date desc, section_path asc);

然后让section path具有类似“first_level next_level sub_level leaf_name”的语法。作为附带好处,当你拥有文档的version_date时(或者如果你在section_path上创建了一个二级索引),因为一个空间在词法上“低于”任何其他有效字符,你实际上可以非常干净地抓住一个子部分:

select section_path, contents from superdoc_structures
where doc_id = '....' and
    version_date = '2013-12-22' and
    section_path >= 'chapter4 subsection2' and
    section_path < 'chapter4 subsection2!';

或者,您可以使用Cassandra对集合的支持来存储这些部分,但是再次......我不确定为什么你甚至打扰它们,因为它们可以很好地解决它们。