前面一篇文章我粗略地介绍了 Cassandra 的设计特点及基本特性,这篇文章我会重点介绍它的数据模型。
首先我们需要知道,Cassandra 的数据模型借鉴了谷歌 BigTable 的设计思想,包括以下四个概念:
除了这些以外,Cassandra3.0 开始引入了物化视图(Materialized Views)概念,用于加快分布在集群内的数据查询效率,后面会重点介绍。
Cassandra 各主要概念之间的包含关系如图 1 所示。
点击查看大图
在上面这张图里,KeySpace 中的 Settings,主要设置副本数量、Hash 策略。ColumnFamily 中的 Settings 主要设置 key 缓存、读修复概率、列的排序方式等属性。Column 是 Cassandra 存储的基本单元,它是一个三元组(name、value、timestamp)。一般情况下它不支持基于列值的查询。V1.1 版本之后(目前是 3.10 版本),Cassandra 对它的 Column 扩展了一个新的属性,即 TTL(存活时间)。
如图 2 所示,该图初略地概括了关系型数据库模型和 Cassandra 数据模型之间的对比。
点击查看大图
用一句话概括,KeySpace 是一个或者多个列族的容器(Container),列族是一系列行的容器,每一行包括了若干个有序的列,列族呈现的是数据的结构,每一个 KeySpace 有至少一个列族。
创建 KeySpace
创建 KeySpace 的基本语法如清单 1 所示。
- CREATE KEYSPACE | SCHEMA IF NOT EXISTS keyspace_name
- WITH REPLICATION = Map
- AND DURABLE_WRITES = true | false
假设我们想要设置一个副本策略是 SimpleStrategy 的 class,副本数量是 3,如清单 2 所示。
- KEYSPACE Excelsior
- WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 3 };
如果我们想要设置支持跨数据中心备份的 NetworkToplogyStrategy 策略,我们需要先知道数据中心的名字,可以通过 nodetool 命令获得这些信息,如清单 3 所示。
- $ nodetool status
- Datacenter: datacenter1
- =======================
- Status=Up/Down
- |/ State=Normal/Leaving/Joining/Moving
- -- Address Load Tokens Owns Host ID Rack
- UN 127.0.0.1 46.59 KB 256 100.0% dd867d15-6536-4922-b574-e22e75e46432 rack1
从清单里面我们可以知道当前所使用的数据中心名字是 datacenter1,和前面类似,可以采用如下语句创建 KeySpace,如清单 4 所示。
- CREATE KEYSPACE
- NTSkeyspace
- WITH REPLICATION
- =
- {
- 'class'
- :
- 'NetworkTopologyStrategy'
- ,
- 'datacenter1'
- :
- 1
- };
注意,如果想要在生产环境下使用 NetworkToplogyStrategy,需要改变默认的 SimpleSnitch 为 network-aware-snitch,在 snitch 配置文件中定义超过 1 个数据中心名称。
如果设置 DURABLE_WRITES 为 false,那么在写入数据到 keyspace 的时候会绕开提交日志。采用这种方式可能会丢失数据。注意,不要在使用 SimpleStrategy 的时候使用这个属性。
KeySpace 源码解释
如果我们想要删除一个 keySpace,你会怎么写代码?要知道 KeySpace 是非常重要的共享资源,会有很多人同时在访问,如果又有多个人同时想要提交删除请求怎么办?怎么保护呢?最简单的方式是采用同步代码块方式,如清单 5 所示,这是删除 KeySpace 的源代码。
- synchronized
- (
- Keyspace
- .
- class
- )
- {
- Keyspace t = schema.removeKeyspaceInstance(keyspaceName);
- if (t != null)
- {
- for (ColumnFamilyStore cfs : t.getColumnFamilyStores())
- t.unloadCf(cfs);
- t.metric.release();
- }
- return t;
- }
首先要从 NonBlockingHashMap(这里是 keyspaceInstances)里移除数据,如清单 6 所示。
- public
- Keyspace
- removeKeyspaceInstance
- (
- String
- keyspaceName
- )
- {
- return keyspaceInstances.remove(keyspaceName);
- }
如果我们发现表中存在列族,那么需要遍历 ConcurrentHashMap(ColumnFamilyStores),找到所有的列族,并且删除它们,最后才能正式推出同步代码块,确认 KeySpace 已经被彻底删除了。
Cassandra 的数据模型包括两个维度:行和列。如果一些用户需要更多的维度,比如说你想要通过接收人进行消息的分组,例如下面这个例子,如清单 7 所示。
- “alice”:{
- “ccd10d-d200-11e2-b7f6-29cc17aeed4c”:{
- “sender”: “bob”,
- “sent”: “2017-08-01 19:29:00+0100”,
- “subject”: “hello”,
- “body”: “hi”
- }
- }
基于这个原因,Cassandra 引入了 Super Column。也就是说,列可以包含列。你可以在一个 Super Column 里包含多个列,多个 Super Column 之间的列数量可以是不同的。允许你删除 Super Column 里面的一个或者多个列。
大家是不是觉得这样做很好?确实,可以让我们更加容易定义数据结构,但是,你有没有发现问题?我想问题是有的。如果你需要读取一个子列,这时候你不得不反序列化整个 Super Column,随着单个 Super Column 所包含的数据量增大而产生更多消耗。对应地,数据压缩过程中的合并过程也会很耗时。
从 V0.8.1开始,Cassandra 引入了组合列概念,既保留了原有 Super Column 的优点,又解决了上面提到的缺点。从 V2.0.0 开始,Cassandra 正式淘汰了 Super Column。
你可以把 Cassandra 的列族(Column Family)想象成为 Map 里面放了 Map,通过 Row Key 连接,通过列键(Column Key)连接内置的 Map。注意,两个 Map 都是排序的。
定义表的几种设计方式
如图 3 所示,该图描述的是列族里面的一行,每个 Column Key 对应一个 Colum Value。
点击查看大图
如图 4 所示,该图描述的是超级列族里的一行,每个 Super Column Key 下面有若干个子列键和对应的值。
点击查看大图
如果我们想要查看每个列族的阈值,同时也查看以下 memtable 的相应配置信息,我们可以通过 nodetool 命令查看,如清单 8 所示:
- $
- ./
- bin
- /
- nodetool
- -
- h localhost cfstats
- Keyspace: dev
- Read Count: 1
- Read Latency: 0.897 ms.
- Write Count: 2
- Write Latency: 0.051 ms.
- Pending Tasks: 0
- Column Family: data
- SSTable count: 2
- Space used (live): 9530
- Space used (total): 9530
- Memtable Columns Count: 1
- Memtable Data Size: 26
- Memtable Switch Count: 1
- Read Count: 1
- Read Latency: 0.897 ms.
- Write Count: 2
- Write Latency: 0.020 ms.
- Pending Tasks: 0
- Key cache capacity: 200000
- Key cache size: 2
- Key cache hit rate: 0.0
- Row cache: disabled
- Compacted row minimum size: 51
- Compacted row maximum size: 86
- Compacted row mean size: 73
在引入物化表之前,如果你想要去查询一个列,并且这个列不是分区键的话,那么你只能使用二级索引。如果我们需要从集群中的大多数节点上查询数据(当数据基数很大的时候),又想查询速度很快,我们应该怎么办?我们可以采用物化表。
假设我们用一张球员的得分表,我们有以下几项查询需求想要实现:
1. 通过比赛查询谁是得分最高的球员?
2. 通过比赛和比赛日,查询谁是得分最高的球员?
3. 通过比赛和月份,查询谁是得分最高的球员?
我们首先需要创建表,如清单 9 所示。
- CREATE TABLE scores
- (
- user TEXT,
- game TEXT,
- year INT,
- month INT,
- day INT,
- score INT,
- PRIMARY KEY (user, game, year, month, day)
- )
接下来我们需要创建物化视图用于呈现所有时间的最高得分。物化视图实际上是预先对数据进行排序,所以我们这里要选择合适的排序方式,这里选择的是 score 列,如清单 10 所示。
- CREATE MATERIALIZED VIEW alltimehigh AS
- SELECT user FROM scores
- WHERE game IS NOT NULL AND score IS NOT NULL AND user IS NOT NULL AND year IS NOT NULL AND month IS NOT NULL AND day IS NOT NULL
- PRIMARY KEY (game, score, user, year,
- month, day)
- WITH CLUSTERING ORDER BY (score desc)
相应地,为了满足另外两个查询需求,我们也分别创建了物化视图,如清单 11 所示。
- CREATE MATERIALIZED VIEW dailyhigh AS
- SELECT user FROM scores
- WHERE game IS NOT NULL AND year IS NOT NULL AND month IS NOT NULL AND day IS NOT NULL AND score IS NOT NULL AND user IS NOT NULL
- PRIMARY KEY ((game, year, month, day),
- score, user)
- WITH CLUSTERING ORDER BY (score DESC)
- CREATE MATERIALIZED VIEW monthlyhigh AS
- SELECT user FROM scores
- WHERE game IS NOT NULL AND year IS NOT
- NULL AND month IS NOT NULL AND score IS NOT NULL AND user IS NOT NULL AND day IS NOT
- NULL
- PRIMARY KEY ((game, year, month), score,
- user, day)
- WITH CLUSTERING ORDER BY (score DESC)
我们尝试插入一些数据,如清单 12 所示。
- INSERT INTO scores
- (
- user
- ,
- game
- ,
- year
- ,
- month
- ,
- day
- ,
- score
- )
- VALUES
- (
- 'pcmanus'
- ,
- 'Coup'
- ,
- 2015
- ,
- 05, 01, 4000)
- INSERT INTO scores (user, game, year, month, day, score) VALUES ('jbellis', 'Coup', 2015,
- 05, 03, 1750)
- INSERT INTO scores (user, game, year, month, day, score) VALUES ('yukim', 'Coup', 2015, 05, 03, 2250)
- INSERT INTO scores (user, game, year, month, day, score) VALUES ('tjake', 'Coup', 2015, 05,
- 03, 500)
- INSERT INTO scores (user, game, year, month, day, score) VALUES ('jmckenzie', 'Coup', 2015,
- 06, 01, 2000)
- INSERT INTO scores (user, game, year, month, day, score) VALUES ('iamaleksey', 'Coup',
- 2015, 06, 01, 2500)
- INSERT INTO scores (user, game, year, month, day, score) VALUES ('tjake', 'Coup', 2015, 06,
- 02, 1000)
- INSERT INTO scores (user, game, year, month, day, score) VALUES ('pcmanus', 'Coup', 2015,
- 06, 02, 2000)
- SELECT user
- ,
- score FROM alltimehigh WHERE game
- =
- 'Coup'
- LIMIT
- 1
- user | score
- -------------------
- pcmanus | 4000
也可以查询每日最高得分,如清单 14 所示。
- SELECT user
- ,
- score FROM dailyhigh WHERE game
- =
- 'Coup'
- AND year = 2015 AND month = 06
- AND day = 01 LIMIT 1
- user | score
- -------------------
- iamaleksey |2500
实现原理
点击查看大图
图 5 中的"base replica"从本地读取数据,然后创建对应的视图。如果视图里的主键(Primary Key)已经在基准表里被更新了,那么这时候会生成一个墓碑(tombstone),以确保旧的数据不会再被呈现在视图里。关于墓碑概念,我会在系列文章中介绍。而确保最终基准表和视图之间一致性的是 batchlog,我也会在其他文章中深入介绍。你可以在 system.built_materializedviews 表里面查询到自己创建的物化视图。
注意点
1. 物化视图没有正常表的那些写入属性。物化视图写入前需要额外的读取动作,并且需要在每个副本上检查数据的一致性,以确保视图里面的数据一并更新;
2. 物化视图实质上是基准表的 CQL 行的对照;
3. 数据量很少情况下不适合使用物化视图;
4. 如果存在大量的分区墓碑,那么物化视图的查询性能可能会受到影响。物化视图在查询过程中一定会针对每一个数据产生一个墓碑(为了确保数据一致性)。
一个组合键由一个或者多个主键字段组成。你可以通过使用括号方式区分组成复合分区键的字段,包含在主键定义里面,但是又在内置的括号以外的字段属于集群列。这些列在一个分区内部形成逻辑集合,便于检索数据,如清单 15 所示。
- CREATE TABLE
- Cats
- (
- block_id uuid,
- breed text,
- color text,
- short_hair boolean,
- PRIMARY KEY ((block_id, breed), color, short_hair)
- );
清单 15 所示的组合分区键是 block_id 和 breed。集群列是 color 和 short_hair。Cassandra 会把具有相同 block_id,但是不太 breed 的数据存储在不同的节点,而这两个字段都相同的数据会被存储在相同的节点上。
我们来设想这么一个场景,我们的网站需要包含用户表和商品表,一个用户可以选择多款商品,同样,一款商品可以被多个用户选择,这就构成了多对多的关系。如果按照关系型数据库的设计方式,如图 6 所示,我们看看下面这张 E-R 图。
点击查看大图
如果我们想在 Cassandra 里同样实现完全一样的功能,就数据库设计方案而言,那么我们可以有以下几种方案:
1.关系模型的精确副本
点击查看大图
这种模型支持通过 user id 查询用户数据,也可以通过 item id 查询 item 数据。但是如果你想查询某个用户喜欢的所有商品,或者所有喜欢某种商品的用户,在 Cassandra 数据库里就不那么简单了。所以,这种设计方式是比较差的一种方式。
2.具有自定义索引的规范化实体
点击查看大图
这种模型方式使 user id 和 item id 的映射关系存储了两次,第一次是通过 item id,第二次是通过 user id。假设我们想要拿到某位购买的商品的名字以及商品 id,我们首先需要查询 Item_By_User 表获取这位用户所有的商品,然后对于每一个 item id,我们需要需要去 item 表获得对应的 item title。针对这种方式的优化方式是针对 Item_by_User 表和 User_by_Item 进行优化,如方案 3 所示。
3.归一化到自定义索引的规范化实体
点击查看大图
采用这种模式,title 和 usernmae 已经被分别包含在 User_By_Item 表和 Item_By_User 表。这样就可以快速地通过用户 id 搜索所有的商品名称,也可以通过商品 id 搜索对应的用户名字。对于这个用例来说,这样的设计比较合理。
4.部分去规范化实体
点击查看大图
方案 4 通过采用 Super Column 方式(已经淘汰,建议采用 composite columns 方式替代)构建了两张大表,这种方式不是说不可以,但是一旦数据量增大后,容易出现查询性能问题。
方案 3 是最佳选择,但是设计时我们应该增加时间戳 column。
默认值是 256。大家知道,Cassandra 的数据存储是由 N 个节点组成的环形,每个节点通过虚拟节点(vnodes)区分,数据被存储在节点上,每个节点随机地被分配一定数量的令牌(tokens)。一个节点有越多的令牌,意味着它也存储了更多的数据。
默认值是 true。这个值是为了设置是否开启或者关闭 hinted handoff 特性。如果你想要针对数据中心进行设置,需要通过这种方式:hinted_handoff_enabled: DC1,DC2。Hinted 特性是为了避免出现数据脏读(数据更新过程中一个节点离线了,等大家都更新完毕后它又活过来了,造成"僵尸"数据的出现),hint 就是为了避免出现这种情况而向 Coordinator 节点写入信息。我会在后续的文章里具体介绍 hinted_handoff 的实现方法及源代码。
针对无响应节点的最大 hint 时间。这段时间结束之后,新的 hints 不会再生成,直到这个离线节点恢复响应。如果这个节点再次宕机,那么新的中断时间开始。这个值的默认值是 3 小时(10800000 微秒)。
这个值的单位是分钟,指的是一个 memtable 应该在写入磁盘前内存中保留多长时间的数据,默认是 24 小时(1440 分钟)。我们会每隔 10 秒钟检查一次 memtable 里面存放的数据。在时间周期范围内,如果列族的主键 memtabl 或者任意的 secondary 索引 memtables 有收到数据,它们需要写入磁盘。
如果我们把这个值设置得很小,那么可能会造成很多个 memtables 同时过期,我们可以通过调整 memtable_flush_writers 和 memtable_flush_queue_size 的值减少对于磁盘的压力,但是依然会造成磁盘 I/O 压力。最好的方式是调整其他 memtable 的触发阈值。
默认值是 org.apache.cassandra.dht.Murmur3Partitioner。通过分区键分布的行横贯整个集群的所有节点。针对这个参数,我们可以配置为 Murmur3Partitioner 、RandomPartitioner、ByteOrderedPartitioner、OrderPreservingPartitioner(已淘汰)中的任意一个。
设置 Cassandra 处理磁盘失败的方式,默认值是 stop,推荐值是 stop 或者 best_effort。配置可选值包括:
记录所有 seed 主机地址。Seed 中文叫种子,它的作用也确实和"种子"差不多,这些"种子"节点会记录整个集群的状态,其他节点可以通过从这些"种子"节点同步状态来了解整个集群的当前实际状况。
用于设置横贯节点的压缩数据总的吞吐量。你越快速地插入数据,你越应该快速地压缩数据,用于保证 SSTable 的数量。推荐值是 16 到 32MB/s。
Cassandra 绑定用于连接到其他节点的 IP 地址,默认值是 localhost(主机名),推荐设置为 IP 地址。
默认值是 org.apache.cassandra.locator.SimpleSnitch。Cassandra 使用 snitches(告密者)锁定节点的位置和路由请求。主要有以下几个值可以被用于设置:
当我们建列族的时候,如果使用这个 WITH COMPACT STORAGE 关键字,并且使用的是复合主键方式,每次存储数据的时候列名也会一起存储,并且使存储在磁盘上的同一个列。如清单 16 示例代码所示。
- CREATE TABLE sblocks(
- block_id uuid,
- subblock_id uuid,
- PRIMARY KEY(block_id, subblock_id)
- )
- WITH COMPACT STORAGE
注意,如果使用的是组合组件,并且也使用了压缩表策略,那么需要指定至少一个 Clustering Column(集群列)。并且,压缩表创建完毕后是不可以增加或者删除列的。
从字面上理解,Bloom_filter_fp_chance 这个参数控制着存储在磁盘上的 SSTables 的过滤精度。说得通俗一些,每次尝试检索数据时,如果我们在内存中已经存放了完整的数据映射表(数据具体在哪个 SSTable 上),那么我们的每次检索耗时会很短,如果我们保存在内存中的精度不高,那会出现我们搜索了很长时间的 SSTable,结果还是没能找到数据。同时,越精准也意味着需要更大的内存。
这个值为 0 表示最大化启用 Bloom Filter,1 表示关闭 Bloom Filter,推荐使用 0.1 这个值。
这个值对应着不同的压缩策略,如果设置为 0.01,那么需要设置压缩策略:SizeTieredCompactionStrategy,如果设置为 0.1,压缩策略:LeveledCompactionStrategy。
本文是"Apache Cassandra3.X"系列文章的第二篇,主要对数据模型进行了深入介绍,包括 KeySpace、Super Column、Column Family、Composite Key 等的设计理念及源代码解释,也针对一个从 RDBMS 到 Cassandra 的数据库设计迁移案例进行了举例说明。接下来介绍了与数据模型相关的各个属性,包括 KeySpace 属性、Column Family 属性,由于属性较多,这里并没有完全举例,只是列举了几个比较常用的,并且需要根据实际情况设定值的属性。
参考 developerWorks 上的 Apache Cassandra,了解更多 Apache Cassandra 知识。
参考书籍 《Cassandra The Definitive Guide 2nd Edition》 Jeff Carpenter&Eben Hewitt
来源: http://www.ibm.com/developerworks/cn/java/j-web-resource-bundle/index.html