思考Cassandra对于KVS的数据模型的问题
首先
以后我会负责Future Advent Calendar的第15天。
最近,“大数据”这个词已经完全渗透到了企业中,我们越来越多地听到”NoSQL”这个词。
在本文中,我将介绍Cassandra作为NoSQL话题,并希望能传达NoSQL(键值对存储)数据模型的考虑要点。
试着思考一下,到底什么是”NoSQL”的定义。
在谈论Cassandra之前,我们先来考虑一下到底什么是”NoSQL”。
NoSQL源自于”Not only SQL”这个词,多被解释为”不只有SQL = 非关系型数据库”的意思。
在企业系统中,RDB(关系数据库)被广泛采用。原因无法用一言以蔽之,但是可以说,通过表连接可以用SQL表示复杂的业务逻辑,严格的事务管理等是其原因之一。
然而,随着大数据时代的到来,出现了如下需求,传统的关系型数据库已经无法完全满足。
構造的なデータだけでなく非構造的なデータも扱いたい
⇒ RDBは扱うデータを事前に定義しておく必要がある
データを長期間蓄積してデータサイズが大きくなったとしても高速に処理したい
⇒ RDBはサーバースペックをスケールアップさせるアーキテクチャのため容量・性能共に限界がある
为了满足“Volume(数据量)”、“Velocity(处理速度)”和“Variety(多样性)”等需求,NoSQL基本上采用了无模式(Schemaless)和可扩展(Scalable)的“分布式架构”。
虽然如此,NoSQL对于在以往关系型数据库(RDB)中自然满足的功能,如排除连接处理或缺乏事务功能等,表现较弱。
换言之,我们需要根据数据的特性和处理方式来考虑是应该采用关系型数据库还是非关系型数据库,或者是两种都采用。
我认为,之所以将从企业领域完全排除RDB称为“Not only”,是因为这很困难。
Cassandra是一种什么样的数据库?
Cassandra是一个由Facebook开发并于2008年开源的数据库,它将Amazon Dynamo的概念和Google Bigtable的架构融合在一起。

卡桑德拉是NoSQL数据库中专为应用程序使用而设计的,在一般情况下,NoSQL数据库倾向于采用无固定模式的数据存储,但从应用程序的角度来看,结构化的数据在开发、运营和管理方面具有优势,因此需要预先定义数据模式。卡桑德拉支持列式存储,可以定义灵活且动态的数据模型,包括列表和映射等数组类型。
此外,它还具有以下特点,可以说它是一个注重可扩展性和可用性的数据库。
-
- データをクラスタ内の複数ノードで分散保持しているため、性能・容量のリニアにスケール可能
-
- マスタレスアーキテクチャで、単一障害点がなくノード障害時のマスタ切り替え不要で可用性を厳格に保証
- データセンターを跨ぐクロスリージョン構成を取ることができるため広域災害などBCP要件を満たすことが可能
由于本篇文章是关于数据模型的,所以关于这个架构的解释可能会在以后的某个地方完成…
理解Cassandra的数据模型。
Cassandra以与关系数据库(RDB)相同的方式,通过“表”这个单位进行数据管理。
表被创建在称为“键空间”的区域中。类似于Oracle中的“模式(schema)”。
如前所述,Cassandra在创建表时需要预先声明列和类型,因为它依赖于模式定义。值得一提的是,Cassandra支持一种名为CQL的专有查询语言,可以像SQL一样使用,因此即使对于熟悉关系数据库的人来说,也可以直观地操作。
CREATE TABLE test_table (
id text
, body text
, tag list<text>
, keyword map<text, text>
, PRIMARY KEY(id)
);
与RDB的不同之处在于它支持列表、映射等数组类型。这使得在定义模式的同时,还能灵活处理半结构化数据,如Json等。
INSERT INTO test_table (
id
, body
, tag
, keyword
) VALUES (
'01'
, 'AdventCalendar15日目'
, ['Future','NoSQL','AdventCalendar']
, {'name': 'Iwasaki', 'age':'26'}
);
-- ListとMapの要素数を増やしてみる
INSERT INTO test_table (
id
, body
, tag
, keyword
) VALUES (
'02'
, 'AdventCalendar99日目'
, ['Future','NoSQL','AdventCalendar','Cassandra']
, {'name': 'future-taro', 'age':'30','gender':'male'}
);
无论是List类型的tag还是Map类型的keyword,它们的元素数量的差异都不会影响数据的插入,因此可以说它是一个灵活的数据模型。
SELECT * FROM test_table ;
id | body | keyword | tag
----+----------------------+--------------------------------------------------------+----------------------------------------------------
02 | AdventCalendar99日目 | {'age': '30', 'gender': 'male', 'name': 'future-taro'} | ['Future', 'NoSQL', 'AdventCalendar', 'Cassandra']
01 | AdventCalendar15日目 | {'age': '26', 'name': 'Iwasaki'} | ['Future', 'NoSQL', 'AdventCalendar']
还有一个Cassandra数据模型中必须要注意的事项是数据访问的方式。由于Cassandra是一种键值存储(准确地说是宽列存储),所以不能将主键以外的列作为筛选条件。
让我们以将”body”列指定为WHERE子句的条件为例。
SELECT * FROM test_table WHERE body = 'AdventCalendar15日目' ;
-- エラー発生
InvalidRequest: Error from server: code=2200 [Invalid query] message="Cannot execute this query as it might involve data filtering and thus may have unpredictable performance. If you want to execute this query despite the performance unpredictability, use ALLOW FILTERING"
发生错误并且无法获取数据。
在访问Cassandra数据时,必须始终指定主键。
SELECT * FROM test_table WHERE id = '01' ;
id | body | keyword | tag
----+----------------------+----------------------------------+---------------------------------------
01 | AdventCalendar15日目 | {'age': '26', 'name': 'Iwasaki'} | ['Future', 'NoSQL', 'AdventCalendar']
顺便提一下,仔细读错误信息可以看到有一句话说”请使用ALLOW FILTERING”。如果不担心性能问题的话,只需添加”ALLOW FILTERING”选项即可执行。
让我们考虑一下为什么在WHERE语句中指定非主键列会导致性能下降。
Cassandra通常在多个节点上进行集群配置,并根据分区键的哈希值来分布式存储数据。
在创建表时,默认情况下,“主键=分区键”,因此在上述例子中,”id”将作为分区键来进行分布式存储。
在将数据存储到表中时,只会创建分区键的索引,因此要根据分区键去查找数据存在于哪个节点上。
换句话说,如果不指定分区键进行访问,就无法在任何节点上满足条件,因此可以说使用”ALLOW FILTERING”进行数据访问是一种导致性能下降的反模式访问方式。
通过在表中创建次要索引,您可以为任意列创建索引。
CREATE INDEX body_idx ON test_table (body);
SELECT * FROM test_table WHERE body = 'AdventCalendar15日目' ;
id | body | keyword | tag
----+----------------------+----------------------------------+---------------------------------------
01 | AdventCalendar15日目 | {'age': '26', 'name': 'Iwasaki'} | ['Future', 'NoSQL', 'AdventCalendar']
在DynamoDB中,对于创建辅助索引有一定限制,而这是Cassandara中没有的小特点。
考虑Cassandra的数据模型设计要点
我觉得现在已经可以大概想象出 Cassandra 的数据模型是什么样子了,但我们需要思考如何设计才能更好地进行设置。
我在下面列出了一些数据模型设计时的要点。
虽然我希望能够宣称这是最佳实践,但由于我也在进行中,所以只能介绍其中的一部分,请谅解。
-
- データモデルのネストを深くしすぎない
-
- インデックス使用を前提としたテーブル設計にしない
-
- リレーションは非正規化するか疑似結合するか検討する
- ロックはなるべく取得しない(後勝ち)ように設計する
不要把数据模型的嵌套设计得太深。
Cassandra需要预先定义表格以确定接收数据的形式。如前所述,为了适应灵活的数据模型,可以使用数组类型,如列表和映射。然而,往往会希望处理多层次的数据,如下所示的嵌套映射。
{
"address" : {
"country": "japan",
"city": "saitama"
}
}
在Cassandra中,您可以处理对数组的嵌套,但在表定义时需要指定为”frozen”。
CREATE TABLE test_table2 (
id text PRIMARY KEY,
f_map map<text, frozen<map<text, text>>>
);
INSERT INTO test_table2 (
id
, f_map
) VALUES (
'01'
, {'address': {'city': 'saitama', 'country': 'japan'}}
);
SELECT * FROM test_table2;
id | f_map
----+------------------------------------------------------
01 | {'address': {'city': 'saitama', 'country': 'japan'}}
在表达深度嵌套时,冻结是非常方便的,但在使用时也有以下注意事项。
- Map,Listのように要素に対する更新、追加、削除処理が行えない
如果是Map或List,您可以执行以下操作来操作元素。
SELECT * FROM test_table WHERE id = '01';
id | body | keyword | tag
----+----------------------+----------------------------------+---------------------------------------
01 | AdventCalendar15日目 | {'age': '26', 'name': 'Iwasaki'} | ['Future', 'NoSQL', 'AdventCalendar']
-- tagの要素'Future'を'future architect'に変更してみる
UPDATE test_table SET tag[0] = 'Future Architect' WHERE id = '01';
id | body | keyword | tag
----+----------------------+----------------------------------+-------------------------------------------------
01 | AdventCalendar15日目 | {'age': '26', 'name': 'Iwasaki'} | ['Future Architect', 'NoSQL', 'AdventCalendar']
-- tagの要素を追加してみる
UPDATE test_table SET tag = tag + ['append'] WHERE id = '01';
id | body | keyword | tag
----+----------------------+----------------------------------+-----------------------------------------------------------
01 | AdventCalendar15日目 | {'age': '26', 'name': 'Iwasaki'} | ['Future Architect', 'NoSQL', 'AdventCalendar', 'append']
只能用一种方式写成以下的中文释义:
无法对冻结项目执行此操作。要对冻结项目进行更新,需要将整个元素替换。
UPDATE test_table2 SET f_map = {'address': {'city': 'tokyo', 'country': 'japan'}} WHERE id = '01';
SELECT * FROM test_table2;
id | f_map
----+----------------------------------------------------
01 | {'address': {'city': 'tokyo', 'country': 'japan'}}
如果应用程序预计对嵌套数组的部分更新,而且这些更新是被冻结定义的,那么就需要同时传输更新目标以外的项目,导致传输的数据量过大,同时也可能导致同时更新时出现不一致的可能性。
- Mapのキーやエントリ値を条件句に使用することが出来ない
对于Map项目,您可以通过为其添加索引来将其用作搜索条件,但对于被冻结的项目无法添加索引。
SELECT * FROM test_table;
id | body | keyword | tag
----+----------------------+--------------------------------------------------------+-----------------------------------------------------------
02 | AdventCalendar99日目 | {'age': '30', 'gender': 'male', 'name': 'future-taro'} | ['Future', 'NoSQL', 'AdventCalendar', 'Cassandra']
01 | AdventCalendar15日目 | {'age': '26', 'name': 'Iwasaki'} | ['Future Architect', 'NoSQL', 'AdventCalendar', 'append']
-- keywordのKEY値に対してインデックスを作成する
CREATE INDEX key_idx ON test_table ( KEYS (keyword) );
SELECT * FROM test_table WHERE keyword CONTAINS KEY 'gender';
id | body | keyword | tag
----+----------------------+--------------------------------------------------------+----------------------------------------------------
02 | AdventCalendar99日目 | {'age': '30', 'gender': 'male', 'name': 'future-taro'} | ['Future', 'NoSQL', 'AdventCalendar', 'Cassandra']
-- keywordのエントリ値に対してインデックスを作成する
CREATE INDEX ent_idx ON test_table ( ENTRIES (keyword) );
SELECT * FROM test_table WHERE keyword['name'] = 'Iwasaki';
id | body | keyword | tag
----+----------------------+----------------------------------+-----------------------------------------------------------
01 | AdventCalendar15日目 | {'age': '26', 'name': 'Iwasaki'} | ['Future Architect', 'NoSQL', 'AdventCalendar', 'append']
如果数据模型嵌套得太深,就无法用于搜索条件,也无法进行部分更新,而且可能需要意想不到地传输大量数据进行更新。因此,我们应尽量避免使用frozen来设计数据模型。
不要使用基于索引的表设计。
在RDB中,即使你在建立索引的前提下,也不要随意设计数据模型。特别是在KVS中,如果没有索引,可能连数据访问都无法实现,所以要谨慎选择哪个列作为分区键。
在Cassandra中,通过创建次要索引,可以对任意列进行条件检索,但和RDB一样,需要正确意识到不同的访问方式。
假设有以下表格和记录,其中id是分区键,记录被分散存储在5个节点上。
为了按照date列进行筛选,需要创建索引date_idx。
通过使用这个方法,您可以指定日期进行搜索。例如,如果您指定条件为“2018-01-01”,您将需要访问“id=02”和“id=04”这两个分区。

如果「2018-01-01」的基数很低,那就没有问题;但是,如果「2018-01-01」的基数增加,就会导致大量的分区访问,可能会成为分散处理的瓶颈。
因此,如果已经将指定日期的搜索作为要求固定下来,那么可以将日期作为分区键来构建,这样一来,索引本身也就不再需要了,访问也将始终限定在一个分区中,提高了搜索效率并且性能更加稳定。

整理并审议应用程序搜索要求和用于搜索的项目以进行数据模型考虑将是一个不错的选择。
由于NoSQL可以轻松扩展数据容量,因此考虑为每个搜索条件创建最优表格也被认为是一个意外的现实解决方案。
順便提一下,在Cassandra中,您还可以通过指定新的主键和列来重新构建为物化视图。这是一个很方便的功能,但需要注意的是,无法直接更新视图,而是在原始表更新时以异步方式协调。
3. 考虑非正规化或伪连接的关系
由于KVS基本上是基于键访问的,因此它是针对单个表的数据访问。
换句话说,与在RDB中常见的表连接操作相反,它从一开始就不支持该思想。
然而,这个问题的解决方案是根据NoSQL的特点,放弃规范化并冗余地存储数据,这样可以快速保证性能。
然而,实际上,在某些情况下,将数据以非规范化的方式保存可能很困难。
虽然与NoSQL的理念相悖,但在特定情况下,选择采用一种模拟连接的方法也是一个选项。
例如,可以通过使用List或Map数组类型来表示关系。通过从作者获取文章关系的列表,并通过嵌套循环按键访问该列表的元素来获取值,从而可以表示伪连接。
SELECT * FROM author;
id | author | relations
----+---------+-----------------------
01 | Iwasaki | ['aaa', 'bbb', 'ccc']
SELECT * FROM article ;
id | body | title
-----+----------+--------------------------------------------------
aaa | hogehoge | SQLonHadoop(ApachDrill)を導入する際のポイント
bbb | hugahuga | Tableau × R で時系列分析をやってみる
ccc | hugehuge | CassandraからKVSのデータモデルについて考えてみる
由于1键访问是以毫秒为单位的访问,所以如果关联的元素数量在10到20左右,则它是一种反模式,但由于它可以以现实的速度返回响应,它可能成为数据模型设计时的一个选择。
无论是通过次要索引还是访问超过千万个元素的情况,都不应该使用关键访问。
基本上应该考虑非规范化路线,只有在确实需要表示关系时才考虑使用伪连接是一种思路。
如果概念数据模型中涉及的实体较多,选择关系数据库(RDB)而不是NoSQL可能会更加愉快。
尽量设计不要获取锁
RDB中的必需功能是事务,但在NoSQL中往往不支持。
原因是因为NoSQL本身就排除了连接表的思想。
Cassandra 提供了适用于单个记录的轻量级事务功能。
通过使用这个轻量级事务,您可以在插入或更新记录时检查最新的值,然后执行处理。
UPDATE test_table SET body = '軽量トランザクションを利用' WHERE id = '01' IF keyword['name'] = 'Iwasaki';
[applied]
-----------
True
SELECT * FROM test_table WHERE id = '01';
id | body | keyword | tag
----+----------------------------+----------------------------------+-----------------------------------------------------------
01 | 軽量トランザクションを利用 | {'age': '26', 'name': 'Iwasaki'} | ['Future Architect', 'NoSQL', 'AdventCalendar', 'append']
如果给予诸如锁标志等控制项,就可以利用轻量事务实现乐观锁式的更新。将锁表剥离出来,并且通过查看锁表后再进行更新,就可以实现悲观锁式的实现。
然而,在使用轻量级事务时,必须先读取最新结果再进行更新,因此在写入之前会出现读取操作,导致操作延迟比普通更新高出近4倍,所以必须尽量减少使用。
由于Cassandra可以根据每个查询更改读取和写入时的一致性级别,因此可以将其设置为Quorum(多数派)以确保结果的一致性,并且通过始终实现后者胜利而无需获取锁定,从而使其变得简单。
建议在进行选择之前认真进行PoC测试,因为即使宣称在NoSQL中支持事务管理的产品也可能存在性能问题,这并不局限于Cassandra。
总结
希望这篇文章能对正在考虑采用NoSQL或者正在研究数据模型的人们有所帮助,虽然我写得有些杂乱无章。
追加注释
請點擊連結閱讀我的最新技術部落格文章,關於cassandra。
我们又新增了一篇新文章。
https://future-architect.github.io/articles/20210412a/