Cassandra使用总结,高度可伸缩的、一致的
简单介绍
Cassandra是一个高可靠的大规模分布式存储系统。高度可伸缩的、一致的、分布式的结构化key-value存储方案,集Google BigTable的数据模型与Amazon Dynamo的完全分布式的架构于一身。Cassandra使用了Google BigTable的数据模型,与面向行的传统的关系型数据库不同,这是一种面向列的数据库,列被组织成为列族(Column Family),在数据库中增加一列非常方便。Cassandra的系统架构与Dynamo一脉相承,是基于O(1)DHT(分布式哈希表)的完全P2P架构,与传统的基于Sharding的数据库集群相比,Cassandra可以无缝地加入或删除节点,非常适于对于节点规模变化比较快的应用场景。
Cassandra的数据会写入多个节点,来保证数据的可靠性,在一致性、可用性和网络分区耐受能力(CAP)的折衷问题上,Cassandra比较灵活,用户在读取时可以指定要求所有副本一致(高一致性)、读到一个副本即可(高可用性)或是通过选举来确认多数副本一致即可(折衷)。这样,Cassandra可以适用于有节点、网络失效,以及多数据中心的场景。Cassandra是一套开源分布式NoSQL数据库系统,设计思想采用了google的BigTable的数据模型和Amazon的Dynamo的完全分布式架构,因而它具有很好的扩展性且不存在单点故障。
数据模型
Cassandra将数据存储在表中,每个表由行和列组成。CQL用户查询存储在表中的数据。Cassandra数据模型基于查询针对查询进行了优化。Cassandra不支持用户关系数据库的关系数据建模。
### 查询驱动建模
数据建模是识别实体及其关系的过程。
在关系型数据库中,数据被放置在规范化表中,外检用于引用其他表中的相关数据。应用程序进行的查询由表的结构驱动,相关数据作为表连接进行查询。
在cassandra中,数据建模是查询驱动的,数据访问模式和应用程序查询决定了数据的结构和组织,然后用于设计数据库表。数据围绕特定查询建模,查询最好设计为访问单个表,这意味着查询中设计的所有实体必须在同一个表中,才能非常快速的访问数据,数据被建模以最合适一个查询或一组查询,一张表以有一个或多个最适合查询的实体。由于实体之间通常确实存在关系,并且查询可能涉及实体之间存在关系,因此单个实体可能包含在多个表中。
目标
主键和分区键的选择对于在集群中均匀分布数据很重要,将查询读取的分区数量保持在最低水平也很重要,因为不同的分区可能位于不同的节点上,并且协调器需要向每个节点发送请求,这会增加请求开销和延迟,即使查询中涉及的不同分区在同一个节点上,更少的分区也能提高查询效率。
分区
Cassandra 是一个分布式数据库,用于跨节点集群存储数据。分区键用于在节点之间对数据进行分区,Cassandra使用一致性散列的变体来对存储节点上的数据进行分区以进行数据分发。散列是一种用于映射给定键的数据的技术,散列函数生成存储在散列表中的散列值。分区键是从主键的第一个字段生成的。使用分区键将数据分区到哈希表中以实现快速查找。用于查询的分区越少,查询的响应时间就越快。
作为划分的一个例子,考虑表t中id是在主键的唯一字段。
CREATE TABLE t ( id int, k int, v text, PRIMARY KEY (id) );
分区键是从主键生成的,id用于跨集群中的节点分发数据。
考虑一个表的变体,t它具有构成主键的两个字段以构成复合或复合主键。
CREATE TABLE t ( id int, c text, k int, v text, PRIMARY KEY (id,c) );
对于t具有复合主键的表,第一个字段id用于生成分区键,第二个字段c是用于在分区内排序的集群键。使用聚类键对数据进行排序可以更有效地检索相邻数据。
通常,主键的第一个字段或其它主键被散列以生成分区键,其余字段或其它主键是用于对分区内的数据进行排序的集群建。对数据进行分区,提高了读写效率。其它非主键字段可能会被单独索引以进一步提高查询性能。
如果将多个字段分组为主键的第一个主键,则可以从多个字段生成分区键,作为table的另一个变体t,请考虑一个表,其中主键的第一个主键由使用括号分组的两个字段组成。
CREATE TABLE t ( id1 int, id2 int, c1 text, c2 text k int, v text, PRIMARY KEY ((id1,id2),c1,c2) );
对于前面的表 t 构成的主键的第一个主键字段id1和id2用于生成分区键和其余字段c1和c2是用于在分区内排序聚类主键。
与关系型数据库比较
关系型数据库将数据存储在使用外键和其他表有关系的表中,关系型数据库的数据建模方法是以表为中心的。查询必须使用表连接从多个表中获取数据,这些表之间存在关系。Cassandra没有外键或关系完整性的概念。Cassandra的数据模型是基于设计高效查询;不涉及多个表的查询,关系数据库规范化数据以避免重复。相比之下,Cassandra通过以查询为中心的数据模型在多个表中复制数据来反规范化数据。如果Cassandra数据模型不能完全集成特定查询的不同实体之间关系的复杂性,则可以使用应用程序代码中处理。
数据建模示例
例如,一个magazine 数据集由具有杂志 ID 、杂志名称、出版频率、出版日期和出版商等属性的杂志数据组成。杂志数据的基本查询 (Q1) 是列出所有杂志名称,包括其出版频率。由于并非Q1需要所有数据属性,因此数据模型仅包含id(用于分区键)、杂志名称和出版频率,如下所示
另一个查询(Q2) 是按照出版商列出所有杂志名称。对于Q2,数据模型将由publisher 分区键的附件属性组成。该id会成为一个分区内排序聚集键。如下所示
Schema 设计
在创建数据模型之后,可为查询设计schema,Q1 schema 设计如下
CREATE TABLE magazine_name (id int PRIMARY KEY, name text, publicationFrequency text)
对于Q2,schema定义包含用于排序的聚类键
CREATE TABLE magazine_publisher (publisher text,id int,name text, publicationFrequency text, PRIMARY KEY (publisher, id)) WITH CLUSTERING ORDER BY (id DESC)
CQL(Cassandra Query Language)
CQL提供一套类似sql的查询驱动
数据类型
Data type
ddl
Data Definition
数据操作
SELECT
select_statement ::= SELECT [ JSON | DISTINCT ] ( select_clause | '*' ) FROM table_name [ WHERE where_clause ] [ GROUP BY group_by_clause ] [ ORDER BY ordering_clause ] [ PER PARTITION LIMIT (integer | bind_marker) ] [ LIMIT (integer | bind_marker) ] [ ALLOW FILTERING ] select_clause ::= selector [ AS identifier ] ( ',' selector [ AS identifier ] ) selector ::= column_name | term | CAST '(' selector AS cql_type ')' | function_name '(' [ selector ( ',' selector )* ] ')' | COUNT '(' '*' ')' where_clause ::= relation ( AND relation )* relation ::= column_name operator term '(' column_name ( ',' column_name )* ')' operator tuple_literal TOKEN '(' column_name ( ',' column_name )* ')' operator term operator ::= '=' | '<' | '>' | '<=' | '>=' | '!=' | IN | CONTAINS | CONTAINS KEY group_by_clause ::= column_name ( ',' column_name )* ordering_clause ::= column_name [ ASC | DESC ] ( ',' column_name [ ASC | DESC ] )*
例:
SELECT name, occupation FROM users WHERE userid IN (199, 200, 207); SELECT JSON name, occupation FROM users WHERE userid = 199; SELECT name AS user_name, occupation AS user_occupation FROM users; SELECT time, value FROM events WHERE event_type = 'myEvent' AND time > '2011-02-03' AND time <= '2012-01-01' SELECT COUNT (*) AS user_count FROM users;
Group_by_clause
group by 字段只能是partition key 或者聚类主键
Allowing filtering
默认情况下,我们所有的查询条件都是主键,也是no filtering的;当非主键使用allow filtering查询时,查询结果和性能是不可预测的,请谨慎使用!
例如:如下birth_year字段带有二级索引
CREATE TABLE users ( username text PRIMARY KEY, firstname text, lastname text, birth_year int, country text ) CREATE INDEX ON users(birth_year);
那么如下查询是有效的:
SELECT * FROM users; SELECT * FROM users WHERE birth_year = 1981;
这两个查询语句,Cassandra都保证性能和返回的数据量成正比。
但是,如下查询将被拒绝
SELECT * FROM users WHERE birth_year = 1981 AND country = 'FR';
因为Cassandra不能保证即使这些查询的结果很小也不会扫描大量数据。通常,它会扫描birth_year=1981所有索引条目,即使实际上只有少数country=‘FR’。但是如果你知道“know what you are doing”,可以强制使用 ALLOW FILTERING ,以下语句是合法的
SELECT * FROM users WHERE birth_year = 1981 AND country = 'FR' ALLOW FILTERING;
总结
查询条件尽量使用主键(分区主键和者聚类主键),表设计时充分考虑查询需求;少用二级索引(测试过数据量千万级,二级索引效率明显降低,查询慢);不用 ALLOW FILTERING
INSERT
insert_statement ::= INSERT INTO table_name( names_values| json_clause) [ IF NOT EXISTS ] [ USING update_parameter( AND update_parameter)* ] names_values ::= namesVALUES json_clause ::= JSON [ DEFAULT ( NULL | UNSET ) ] names ::= '(' ( ',' )* ')' tuple_literal stringcolumn_namecolumn_name
例如:
INSERT INTO NerdMovies (movie, director, main_actor, year) VALUES ('Serenity', 'Joss Whedon', 'Nathan Fillion', 2005) USING TTL 86400; INSERT INTO NerdMovies JSON '{"movie": "Serenity", "director": "Joss Whedon", "year": 2005}';
总结:与 SQL 不同,INSERT默认情况下不检查该行的先前存在:如果之前不存在该行,则创建该行,否则更新该行
UPDATE
update_statement ::= UPDATE table_name [ USING update_parameter( AND update_parameter)* ] SET assignment( ',' assignment)* WHERE where_clause [ IF ( EXISTS | condition( AND condition)*) ] update_parameter ::= ( TIMESTAMP | TTL ) ( integer| bind_marker) assignment ::= simple_selection'=' term | column_name'=' column_name( '+' | '-' ) term | column_name'=' list_literal'+' simple_selection ::= | '[' ']' | '.' `字段名称 column_name column_namecolumn_nametermcolumn_name条件 ::= simple_selection operator term
例如
UPDATE NerdMovies USING TTL 400 SET director = 'Joss Whedon', main_actor = 'Nathan Fillion', year = 2005 WHERE movie = 'Serenity'; UPDATE UserActions SET total = total + 2 WHERE user = B70DE1D0-9908-4AE3-BE34-5573E5B09F14 AND action = 'click';
总结:更新条件字段必须是primary key;UPDATE默认情况下不检查该行的先前存在(除了使用IF):如果之前不存在该行,则创建该行,否则进行更新。此外,无法知道是否发生了创建或更新。
DELETE 和 Tombstone
delete_statement ::= DELETE [ simple_selection ( ',' simple_selection ) ] FROM table_name [ USING update_parameter ( AND update_parameter )* ] WHERE where_clause [ IF ( EXISTS | condition ( AND condition )*) ]
例如:
DELETE FROM NerdMovies USING TIMESTAMP 1240003134 WHERE movie = 'Serenity'; DELETE phone FROM Users WHERE userid IN (C73DE1D3-AF08-40F3-B124-3FF3E5109F22, B70DE1D0-9908-4AE3-BE34-5573E5B09F14);
tombstone(墓碑问题):
cassandra是分布式的,数据删除相比较关系型数据库要复杂的多;当删除一条数据时,会被写入一个被称作墓碑(tombstone)的标记,用于记录一个删除操作,表示之前的值被删除了;等到执行合并(compact,gc_grace_seconds默认864000秒,即10天)时,就利用这些墓碑数据删除SSTable中对应的数据。
问题点:Cassandra会将同一份数据存放到多个副本不同的节点上,如果某个节点A收到了删除指令,删除了本地的记录,并尝试将删除逻辑传递给包含该记录副本的其他节点B,如果对应节点B并没有进行相应,即该节点无法将数据立即删除,且该节点上仍然拥有删除前的数据。如果在节点B重新恢复到集群中之前,其他节点上的该数据已经被清除,则节点B上的该数据会被认为是新的记录,进行同步。就会导致删除操作并不能生效,这种记录被称为僵尸(zombie)。
总结:
- 删除可以是一行,也可以是某一列
- 尽量使用id删除
- 避免大量数据删除,可通过删表重建的方式
BATCH
batch_statement ::= BEGIN [未记录| COUNTER ] BATCH [ USING update_parameter( AND update_parameter)* ] modification_statement( ';' modification_statement)* APPLY BATCH modify_statement ::= | | insert_statementupdate_statementdelete_statement
例如:
BEGIN BATCH INSERT INTO users (userid, password, name) VALUES ('user2', 'ch@ngem3b', 'second user'); UPDATE users SET password = 'ps22dhds' WHERE userid = 'user3'; INSERT INTO users (userid, password) VALUES ('user4', 'ch@ngem3c'); DELETE name FROM users WHERE userid = 'user1'; APPLY BATCH;
总结:
- 在批处理多个更新时,它可以节省客户端和服务器之间(有时是服务器协调器和副本之间)的网络往返
- BATCH语句只能包含UPDATE,INSERT和DELETE语句
- 默认情况下,批处理中的所有操作都按记录执行,以确保所有更改最终完成(或不会完成)
Cassandra Tools
cassandra提供了很多工具,比如CQL shell、Nodetool、SSTable Tools,这里简单介绍下使用Nodetool备份还原数据,其它工具可参考官网介绍
Nodetool 备份还原数据
Cassandra Tools
备份:./bin/nodetool snapshot -t 备份文件夹名 备份的keyspace cassandra数据存储目录查看cassandra.ymal文件的hints_directory配置 scylladb数据存储目录查看scylla.ymal文件的hints_directory配置 备份完成后数据会存在数据目录 bak_dir(../data/keyspacename/tablename-uuid/snapshots/备份文件夹名/时间戳-tablename/) 还原 将备份的表数据bak_dir里文件直接拷贝至目标table目录下 ../data/keyspacename/tablename-uuid/ 执行./bin/nodetool refresh -- keyspacename tablename
java集成
java集成有很多方式,可以直接使用cassandra提供的驱动,也可以使用spring-data-cassandra,下面介绍下使用spring-data-cassandra方式集成
引入依赖(maven方式)
<dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-cassandra</artifactId> </dependency> <dependency> <groupId>com.datastax.cassandra</groupId> <artifactId>cassandra-driver-core</artifactId> <version>3.9.0</version> </dependency>
配置
在spring配置文件中添加一下配置
spring.data.cassandra.contact-points=10.192.77.203 spring.data.cassandra.port=9042 spring.data.cassandra.cluster-name=Test Cluster
使用(查询和删除)
下面使用的CassandraTemplate实现查询和删除
@Repository @Slf4j public class Demo { @Autowired private CassandraTemplate cassandraTemplate; /** * 查询 * @return */ public List<Map> select(){ String sql = "select * from demo where id = '1794871'"; List<Map> maps = cassandraTemplate.select(sql, Map.class); if(CollectionUtils.isEmpty(maps)){ return null; } return maps; } /** * 删除 * @return */ public boolean delete(){ String sql = "DELETE FROM demo WHERE id='127198' "; boolean execute = cassandraTemplate.getCqlOperations().execute(sql); return execute; } }
CassandraTemplate类包含较全的CQL相关方法,基本能满足使用,如果没有想要的方法,可以使用cassandraTemplate.getCqlOperations().execute 执行自定义cql。
参考资料
cassandra官方文档