Cassandra学习笔记
准备
按照Cassandra集群部署搭建两台测试机,环境信息如下:
名称 | IP | 数据中心名称 |
---|---|---|
node-01 | 192.168.198.130 | datacenter1 |
node-02 | 192.168.198.131 | datacenter1 |
Keyspace
创建Keyspace
1 | create_keyspace_statement ::= CREATE KEYSPACE [ IF NOT EXISTS ] keyspace_name WITH options |
示例:
1 | ## 使用SimpleStrategy复制策略 |
使用Keyspace
1 | use_statement ::= USE keyspace_name |
修改Keyspace(replication factor)
1 | alter_keyspace_statement ::= ALTER KEYSPACE keyspace_name WITH options |
示例:
1 | ALTER KEYSPACE excelsior |
查看Keyspace
1 | DESCRIBE KEYSPACE <keyspace name>; |
使用该语句查看创建的键空间是否正确:
1 | DESCRIBE KEYSPACE excelsior; |
删除Keyspace
1 | drop_keyspace_statement ::= DROP KEYSPACE [ IF EXISTS ] keyspace_name |
1 | DROP KEYSPACE excelsior; |
Table
创建Table
1 | create_table_statement ::= CREATE TABLE [ IF NOT EXISTS ] table_name |
创建Table必须指定主键,主键是用于在表中唯一标识某一行,可以是一列或多列。
示例,在excelsior
键空间创建一张名为excelsior_alt_stats
的表:
1 | CREATE TABLE excelsior.excelsior_alt_stats ( |
cassandra还支持collection(map, set, 或者 list)类型作为列:
1 | CREATE TABLE excelsior.whimsey ( |
甚至是嵌套的元组类型(tuple):
1 | CREATE TABLE excelsior.route ( |
更多数据类型请参阅下一节Cassandra数据结构
静态列
某些列可以在表定义中声明为STATIC。静态的列将由属于同一分区(具有相同分区键)的所有行“共享”。例如:
1 | CREATE TABLE t ( |
修改Table
1 | alter_table_statement ::= ALTER TABLE table_name alter_table_instruction |
示例:
1 | ALTER TABLE addamsFamily ADD gravesite varchar; |
修改Table可以:
- 向表中添加新列(通过ADD指令)。请注意,无法更改表的主键,因此新添加的列将不会成为主键的一部分。
- 从表中删除列。这会丢弃列及其所有内容。
- 更改一些表选项(通过WITH指令)。支持的选项与创建表时相同(在创建后无法更改的COMPACT STORAGE和CLUSTERING ORDER之外)。
删除Table
1 | drop_table_statement ::= DROP TABLE [ IF EXISTS ] table_name |
截断Table(清空表数据)
1 | truncate_statement ::= TRUNCATE [ TABLE ] table_name |
由于表是唯一可以在当前截断的对象,因此可以省略TABLE关键字。
截断表会永久删除表中的所有现有数据,但不会删除表本身。
Cassandra数据结构
CQL是一种类型化语言,支持丰富的数据类型集,包括本地类型,集合类型,用户定义类型,元组类型和自定义类型:
1 | cql_type ::= native_type | collection_type | user_defined_type | tuple_type | custom_type |
本地类型(Native Types)
类型 | 常量支持 | 说明 |
---|---|---|
ascii | string |
ASCII字符串 |
bigint |
integer |
64位无符号整数 |
blob |
blob |
任意字节(无验证) |
boolean |
boolean |
true 或false |
counter |
integer |
计数器列(64位有符号值) |
date |
integer , string |
日期(没有相应的时间值) |
decimal |
integer , float |
十进制可变精度 |
double |
integer float |
64位IEEE-754浮点 |
duration |
duration |
持续时间(纳秒精度) |
float |
integer , float |
32位IEEE-754浮点 |
inet |
string |
IP地址,IPv4(4字节长)或IPv6(16字节长) |
int |
integer |
32位无符号整数 |
smallint |
integer |
16位有符号整数 |
text |
string |
UTF8编码的字符串 |
time |
integer , string |
具有纳秒精度的时间(没有相应的日期值) |
timestamp |
integer , string |
时间戳(日期和时间),精度为毫秒 |
timeuuid |
uuid |
UUID(版本1),通常用作“无冲突”时间戳 |
tinyint |
integer |
8位有符号整数 |
uuid |
uuid |
一个UUID(任何版本) |
varchar |
string |
UTF8编码的字符串 |
varint |
integer |
任意精度整数 |
其中需要注意的是时间类型:
timestamps
时间戳类型的值被编码为64位有符号整数,表示自标准基准时间(称为纪元:1970年1月1日格林威治标准时间00:00:00)以来的毫秒数。
1299038700000
'2011-02-03 04:05+0000'
'2011-02-03 04:05:00+0000'
'2011-02-03 04:05:00.000+0000'
'2011-02-03T04:05+0000'
'2011-02-03T04:05:00+0000'
'2011-02-03T04:05:00.000+0000'
例如:
1 | SELECT * |
或者
1 | SELECT * |
其中,+0000
是RFC 822 4-digit时区规范,+0000
指GMT。美国太平洋标准时间为-0800
,中国北京标准时间为+8000
,官方建议每次插入查询都带上时区,不加的话,默认是使用Cassandra节点配置的时区,可能会出现时区不一致导致的查询失败问题。
dates
日期类型的值被编码为32位无符号整数,表示在该范围的中心处具有“纪元”的天数(2^31)。大纪元是1970年1月1日。
至于时间戳,日期可以作为整数或使用日期字符串输入。在后一种情况下,格式应为yyyy-mm-dd
(例如’2011-02-03’)。
times
时间类型的值被编码为64位有符号整数,表示自午夜以来的纳秒数。
对于时间戳,可以以整数或表示时间的字符串的形式输入时间。在后一种情况下,格式应为hh:mm:ss [.fffffffff]
(其中亚秒精度是可选的,如果提供,则可以小于纳秒)。例如,以下是一段时间内的有效输入:
'08:12:54'
'08:12:54.123'
'08:12:54.123456'
'08:12:54.123456789'
durations
持续时间类型的值被编码为3个有符号整数的可变长度。这是因为一个月的天数可以改变,一天可以有23或25小时,具体取决于夏令时。
第一个整数表示月数(32位整数)
第二个表示天数(32位整数)
第三个表示纳秒数(64位整数)
- 支持的单位:
y
: 年(12 月)mo
: 月 (1 月)w
: 周(7 天)d
: 天(1 天)h
: 小时(3,600,000,000,000 纳秒)m
: 分钟(60,000,000,000 纳)s
: 秒(1,000,000,000 纳)ms
: 毫秒(1,000,000 纳)us
orµs
: 微妙(1000 纳)ns
: 纳秒(1 纳)
- ISO 8601格式:
P[n]Y[n]M[n]DT[n]H[n]M[n]S or P[n]W
- ISO 8601替代格式:
P[YYYY]-[MM]-[DD]T[hh]:[mm]:[ss]
插入示例:
1 | INSERT INTO RiderResults (rider, race, result) VALUES ('Christopher Froome', 'Tour de France', 89h4m48s); |
持续时间列不能作为主键。这是由于无法精确确认持续时间。如果没有日期上下文,实际上不可能知道1个月是否大于29天。
1天的持续时间也不等于24h,因为持续时间类型需要支持夏令时。
集合类型(Collections)
cassandra支持三种类型的集合:Maps, Sets and Lists
1 | collection_type ::= MAP '<' cql_type ',' cql_type '>' |
可以这样输入集合类型的数据:
1 | collection_literal ::= map_literal | set_literal | list_literal |
Maps
Maps是一组(有序)键值对,其中键是唯一的,并且按其键排序。
1 | CREATE TABLE users ( |
另外,Maps还具有一些高级特性:
- 更新或插入一个或多个元素
1 | UPDATE users SET favs['author'] = 'Ed Poe' WHERE id = 'jsmith'; |
- 删除一个或多个元素(如果一个元素不存在,删除它是一个无效操作但不会抛出错误)
1 | DELETE favs['author'] FROM users WHERE id = 'jsmith'; |
Sets
Sets是唯一值的(已排序)集合。
1 | CREATE TABLE users ( |
另外,Sets也具有一些高级特性:
- 添加一个或多个元素(因为这是一个集合,插入一个已存在的元素是一个无效操作)
1 | UPDATE images SET tags = tags + { 'gray', 'cuddly' } WHERE name = 'cat.jpg'; |
- 删除一个或多个元素(如果一个元素不存在,删除它是一个无效操作但不会抛出错误)
1 | UPDATE images SET tags = tags - { 'cat' } WHERE name = 'cat.jpg'; |
Lists
Lists是非唯一值的(已排序)集合,其中元素按列表中的位置排序。它与Sets的区别就在于是否是唯一值。
1 | CREATE TABLE plays ( |
另外,Lists同样也具有一些高级特性:
- 在列表头或尾添加元素
1 | UPDATE plays SET players = 5, scores = scores + [ 14, 21 ] WHERE id = '123-afde'; |
💡该操作不是幂等的,特别是在其中一个操作超时时,重试操作是不安全的,可能会导致同一数据插入两次。
- 在列表中指定下标处设置值。该列表必须长度大于此下标,否则将抛出列表太小的错误
1 | UPDATE plays SET scores[1] = 7 WHERE id = '123-afde'; |
- 通过列表指定下标删除元素。该列表必须长度大于此下标,否则将抛出列表太小的错误。此外,当操作从列表中删除元素时,列表大小将减1,从而改变此下标之后所有元素的位置
1 | DELETE scores[1] FROM plays WHERE id = '123-afde'; |
- 删除列表中指定下标之间的所有元素
1 | UPDATE plays SET scores = scores - [ 12, 21 ] WHERE id = '123-afde'; |
💡以上2,3,4操作会出现内部的 read-before-write
,会比通常的更新消耗更多的资源,所以尽量使用Sets代替Lists。
用户自定义类型(User-Defined Types)
CQL支持用户定义类型(以下简称UDT)。可以使用下面
create_type_statement
,alter_type_statement
和drop_type_statement
创建,修改和删除此类型。
1 | user_defined_type ::= udt_name |
创建
1 | create_type_statement ::= CREATE TYPE [ IF NOT EXISTS ] udt_name |
UDT有一个名称(用于声明该类型的列),是一组命名和类型字段。字段名称可以是任何类型,包括集合或其他UDT。例如:
1 | CREATE TYPE phone ( |
💡注意
- 尝试创建现有类型时请使用
IF NOT EXISTS
选项,否则将会抛出错误。 - UDT本质上绑定到创建它的键空间,并且只能在该键空间中使用。在创建时,如果类型名称以键空间名称为前缀,则在该键空间中创建它。否则,它将在当前键空间中创建。
- 从Cassandra 4.0开始,在大多数情况下必须冻结UDT,因此在上面的表定义中冻结了
<address>
。有关详细信息,请参阅冻结部分。
修改
1 | alter_type_statement ::= ALTER TYPE udt_name alter_type_modification |
修改一个UDT,可以:
- 在类型中添加一个新字段
1 | ALTER TYPE address ADD country text |
请注意:新添加的字段在之前的记录中,都将被置为NULL。
- 重命名该类型的字段
1 | ALTER TYPE address RENAME zip TO zipcode |
删除
1 | drop_type_statement ::= DROP TYPE [ IF EXISTS ] udt_name |
使用
1 | udt_literal ::= '{' identifier ':' term ( ',' identifier ':' term )* '}' |
使用UDT有点像Maps,例如
1 | INSERT INTO user (name, addresses) |
元组(Tuples)
CQL还支持元组和元组类型(元素可以是不同类型),类似于匿名的UDT或者是Scala的Tuple类型。
1 | tuple_type ::= TUPLE '<' cql_type ( ',' cql_type )* '>' |
例如:
1 | CREATE TABLE durations ( |
自定义类型(Custom Types)
自定义类型主要是为了兼容老项目,不建议使用。使用已有的类型加上用户自定义类型(UDT)就够了。
1 | custom_type ::= string |
数据增删改查(CRUD)
SELECT
1 | select_statement ::= SELECT [ JSON | DISTINCT ] ( select_clause | '*' ) |
示例:
1 | SELECT name, occupation FROM users WHERE userid IN (199, 200, 207); |
Allowing filtering
默认情况下,CQL仅允许不涉及“过滤”服务器端的选择查询,原因是那些“非过滤”查询具有可预测的性能,因为它们的查询性能与Limit成比例。
举个例子:
1 | CREATE TABLE users ( |
以下两种查询是不需要添加ALLOW FILTERING
的:
1 | SELECT * FROM users; |
因为在这两种情况下,Cassandra都保证这些查询性能与返回的数据量成正比。
而下面的这个查询,则需要强制添加:
1 | SELECT * FROM users WHERE birth_year = 1981 AND country = 'FR' ALLOW FILTERING; |
👉🏼 关于如何定义可预测的列,可参考Cassandra中的索引
INSERT
1 | insert_statement ::= INSERT INTO table_name ( names_values | json_clause ) |
示例:
1 | INSERT INTO NerdMovies (movie, director, main_actor, year) |
💡请注意
与SQL不同,INSERT默认情况下不检查行的先前存在:如果之前不存在,则创建行,否则更新。此外,没有办法知道发生了哪些创建或更新。
如果要做到存在则不更新,可以使用
IF NOT EXISTS
条件。但请注意,使用IF NOT EXISTS
将导致不可忽略的性能成本(内部使用Paxos),因此应谨慎使用。INSERT的所有更新都以原子方式单独应用。
UPDATE
1 | update_statement ::= UPDATE table_name |
示例:
1 | UPDATE NerdMovies USING TTL 400 |
💡请注意
与SQL不同,UPDATE默认情况下不检查行的先前存在(除非通过IF):如果之前不存在,则创建行,否则更新。此外,没有办法知道是否发生了创建或更新。
可以通过IF在某些列上使用条件,在这种情况下,除非满足条件,否则不会更新行。但请注意,使用IF条件会产生不可忽视的性能成本(内部使用Paxos),因此应谨慎使用。
在UPDATE语句中,同一分区键中的所有更新都以原子方式单独应用。
此外,UPDATE操作针对某些数据类型有强制性要求:
c = c + 3
用于递增/递减计数器。
‘=’符号后面的列名称必须与’=’符号前面的列名相同。请注意,仅在计数器上允许递增/递减,并且是计数器上允许的唯一更新操作。
id = id + <some-collection>
和id[value1] = value2
用于集合。
id.field = 3
在非冻结的用户定义类型上设置字段的值。
DELETE
1 | delete_statement ::= DELETE [ simple_selection ( ',' simple_selection ) ] |
示例:
1 | DELETE FROM NerdMovies USING TIMESTAMP 1240003134 |
💡请注意
WHERE子句指定要删除的行。使用IN运算符可以使用一个语句删除多行。可以使用不等运算符(例如>=)删除一系列行。
在DELETE语句中,同一分区键中的所有删除都以原子方式单独应用。
DELETE操作可以通过使用IF子句来条件化,类似于UPDATE和INSERT语句。但是,与INSERT和UPDATE语句一样,这将导致不可忽略的性能成本(内部,将使用Paxos),因此应谨慎使用。
批处理
批处理只允许包含UPDATE,INSERT和DELETE语句。
批处理节省客户端和服务器之间的网络资源消耗。
1 | batch_statement ::= BEGIN [ UNLOGGED | COUNTER ] BATCH |
示例
1 | BEGIN BATCH |
💡请注意
- 属于给定分区键的BATCH中的所有更新都是单独执行的。
- 默认情况下,批处理中的所有操作都按记录执行,以确保所有变更都最终完成(或不执行任何操作)。类似于SQL事务,但不完全等同于SQL事务。
UNLOGGED batches
默认情况下,Cassandra使用批处理日志来确保所有变更都最终完成(或不执行任何操作)【请注意,操作仅在单个分区中隔离】。
批处理跨越多个分区时,批处理在性能上会有所损失。可以使用UNLOGGED选项来跳过批处理日志,不过,如果批处理失败,可能会造成批处理中的任务部分成功部分失败,请谨慎选择。
COUNTER batches
使用COUNTER选项进行批量计数器更新。
与Cassandra中的其他更新不同,计数器更新不是幂等的。