Cassandra系列文章的第二篇。
在说CQL之前,得先说一下BigTable。说BigTable前,得说下NoSQL。在说NoSQL之前,得说下关系型数据库。
关系型数据库是什么,其实我也说不好,甚至搜了一圈定义也没有搜到一个恰当的中文定义,好在搜到了个英文的定义觉得比较恰当:
Relational database management systems (RDBMS) support the relational (=table-oriented) data model. The schema of a table (=relation schema) is defined by the table name and a fixed number of attributes with fixed data types. A record (=entity) corresponds to a row in the table and consists of the values of each attribute. A relation thus consists of a set of uniform records.
也就是说,关系型数据库的数据模型是基于“表”的。表作为一种二维结构,每行是一条记录,并且每条记录都互不相同;表的每列是这条记录的一个属性。此外,关系型数据库还应该支持多种操作,除了基本的CRUD,还应该支持集合操作(取并、交、差)以及Join——一种将多个表组合连接的查询操作。针对关系数据库的模型,有一套特殊的语言来对数据库进行操作,也就是Structured Query Language,SQL。
似乎只要不是关系型数据库,都可以称之为NoSQL。NoSQL的出现道理很简单,就是关系型数据库太复杂,而很多数据存储场景中对关系模型的需求没那么大,简化了模型之后不仅实现简单而且性能也能好一些。还是那句话,不谈需求场景只谈利弊是耍流氓,所以这两者应该是互为补充的关系,不是替代关系。除非到了某天,性能完全不再是瓶颈(虽然分布式数据库理论上可以加机器提高性能,但是还有个成本问题,所以本质上还是性能问题),那么NoSQL的简化只剩下了使用上,而使用上可以靠ORM把SQL变成NoSQL,那个时候倒是关系型数据库可以一统天下……
因为“不符合RDBMS模型的数据库都是NoSQL”,因而NoSQL的数据模型也不尽相同。最简单的NoSQL模型就是K-V模型,一个key对应一个value。或者说整个数据库就是一个Map,HashMap还是SortedMap不一定。像Memcache就是典型的kv数据库。一些数据库能稍微高级点,value不一定只是一个单纯的string或bytes,还可以在数据库层面支持多种数据类型,甚至是集合类型。Redis就因为其支持多种类型的value所以属于在kv数据库中比较强大而方便的。
比kv数据库稍微强大一点的模型主要有两种,一种是MongoDB为代表的“文档数据库”,这种数据库最大的特点是“schema-free”,每行数据可以有多个字段的同时又不需要提前定义好字段的schema(也就是不需要提前create table),并且每行数据的字段名可以不尽相同、随意设置,甚至支持嵌套。说白了,你可以理解为每条数据是一个类似json的结构,这种数据库相当于有处理json的能力,如按某个字段建索引等功能。如果不用这些功能,本质上相当于把json序列化然后放在kv数据库的value中。而因为这种数据库没有定义schema,同时也缺少了一些SQL操作,所以不属于关系型数据库。
第二种是BigTable采用的“Wide Column”。这种模型在每行数据内嵌套了一层kv的map,相当于整个数据模型是二维map。虽然RDBMS的表也是二维的,但是任何行中的每列都是提前定义、而后一直固定且相同的,相当于不断的增删数据增加了二维表的行数而列数是永远不变的。而Wide Column的行数和列数都是会随着数据的增删而变化的,并且列名(也就是第二维map的key)是不固定的,因而也算“schema-free”。但MongoDB支持嵌套,只限制最外层一条记录的大小,而Wide Column确定了row key和column key之后就确定了value,value只能是基本类型不能无限嵌套(有限的嵌套,或者说value是个集合类型还是可以的),这应该也算是两者的最大区别,而且两者的实现原理应该是完全不同的(其实我不知道MongoDB是怎么实现的……)。虽然BigTable还支持第三维map,其key为时间戳,但本质上更多维是没有区别的,因为这只是相当于map的key有多个、排序时按顺序逐个比较而已(只是时间戳作为特殊的一维,数据库会自动只保留最新的若干个版本,剩下的自动删掉)。实际上在HBase的实现中,是把所有维压扁变成一个大的SortedMap的(内存里为跳表)来存储的,row key、column qualifier(column key)、timestamp共同作为key并且按顺序比较。唯一的区别是row key作为一行数据的唯一标识,而row作为最小的存储单位,一个row无论有多少个column,一定都属于一个region进而属于一个节点,不会出现一个row key的前若干个column qualifier和后若干个被分开的情况。因而无论Wide Column模型中有多少个key,最终row key是一维、其他key共同是一维,形成一个二维的关系。此外,其实很容易理解,Wide Column只会在分布式数据库中出现,因为单机数据库的全部数据都是连续存储,多维map的每一维是没任何区别的,相当于K-V模型把Key搞复杂而已。
讲完了BigTable,终于可以将主角Cassandra了。Cassandra虽然在分布式模型上继承了Dynamo,但Dynamo是kv模型,Cassandra并没有直接使用简单的kv模型而是从BigTable中学了Wide Column。其实原因很简单,那就是kv模型过于简单了。大多数常见的场景很难做到一个实体只有一个字段,因而kv数据库只能要么把这个实体的多个字段共同存储在一个value内,要么把这个实体的id加上字段名组成一个key从而把一个实体拆成多行。前者导致如果只想查找一个实体的其中一个字段也不得不全部读出来、反序列化,如果只修改一个字段也不得不全部读出来、反序列化、修改、再序列化、再写入,甚至如果想并发修改一个实体的不同字段也不得不对整行数据加锁;后者导致如果想读写一个实体的多个字段就得读写多次(如果key是有序的还好点,可以扫,性能比读多次好,但Dynamo和Cassandra因为起原理导致key很难有序,原因下一篇文章会讲)。总之单纯的kv都远不如Wide Column的多维关系好用。
Cassandra在最开始使用thrift作为client的交互接口。每个集群有多个keyspace,每个keyspace有多个column family,每个cf的数据独立存储。CF下面是一行行数据,每行数据有一个key和若干个column,每个column是一个由 name, value, timestamp, ttl组成的元组。其中column name也就是column key。虽然Cassandra不支持多版本,同name写多个数据就会是新的覆盖旧的,但是如系列文章的第一篇所讲的,Cassandra判断新旧的唯一标准就是时间戳,因而每个数据的写入时间戳都要永远保留。而ttl是数据的有效期,如果设置,到期自动删除(只是读不到而已,物理上依然会保留一段时间,以后会说为啥)。如果Cassandra只发展到这里,实际上用起来跟HBase挺像,只是row key非有序、不支持多版本而已。于此同时,thrift也基本上暴露了其存储引擎的存储方式。
于是本文的主角终于出场了,Cassandra从1.2开始引入CQL——Cassandra Query Language,来作为查询语言,是目前版本的首选方式,并且未来会彻底废除thrift。同时因为CQL是专门为了Cassandra定义的语言,无论通信协议、解析复杂度上都优于thrift,因而CQL的性能会比thrift好不少。并且诸多最新功能都只能在CQL上用,因此新接触Cassandra的话只关注CQL就可以了。此外随着CQL的进化,Cassandra的数据存储格式也在变化,最开始的CQL是完全兼容thrift的,但从CQL3开始数据格式已经不兼容thrift了。
首先,CQL在模型定义上参考了关系型数据库,也使用二维表格将数据schema确定化,但是除此之外没有任何相似之处。CQL在定义列时也需要指定主键,而主键也可以有多个。并且主键分为partition key和clustering column两种,前者就是row key,而后者就是column key。以前的thrift模型中,row key和column name是一个确定的字段,没有也不需要“字段名”,就像kv数据库的两个列相当于就叫key和value不能自己起名字一样。而CQL中可以给row key和column key分别起名,并且分别都可以有多个,甚至column key可以没有,变成一个纯kv模型。这个名字并不会存储在每行数据中,直接存储在meta信息中即可。如SQL一样,primary key无论partition key还是clustering column都不能为null,而非主键字段可以有多个,是可以为null的。换句话说就是thrift模型下是一个row key+一个column key定位一个value;而CQL模型下是一到多个row key+零到多个column key确定零到多个value。
CQL模型中的行,也叫一个CQL row,其实本质上是Cassandra中Wide Column模型的一个列,只是这个列的key可能不只是一个无名字段而可能是多个带名字的字段,而且value可以有多个。CQL支持多个value,本质上是每列数据在存储多个value时用额外的空间把每个value对应的字段名也存进去了,因而CQL table的定义中可以定义很多个CQL column,如果某个CQL row的数据很稀疏,很多CQL column都是null,是不会多占用空间的。CQL的语法也支持创建旧格式兼容thrift的表,加with compact storage,这样的表只支持一个非主键的列,并且不能动态增减新列,也更省空间。
Cassandra虽然因为架构原因不方便让row key有序存储,但是clustering column是有序的,并且是按照CQL定义的顺序分别比较大小排序。所以在查询的时候是支持范围查询连续的数据的,只能查询连续的数据意味着必须在where语句中限制全部partition key的相等关系、前x个clustering column的相等关系和第x+1个clustering column的大小或相等关系。其中0<=x<n,n为clustering column的个数。举个例子就是,假如有这样一个表:
create table orders(
userid string,
date string,
order_id int,
comment string,
primary key ((userid), date, order_id));
不搞语法教程了,先知道这个表中userid是partition key、date和order_id是clustering column,comment是普通column就行。insert和update的时候(cassandra完全不区分这俩,可以insert存在的数据或者update不存在的)必须指定这三个字段,这三个字段完全一样就会被认作是同一个CQL row进而会覆盖之前写的数据。在读的时候,可以select * from orders where userid=xx and date = yy and order_id=zz,这样只会返回1 row或者0 row;也可以select * from orders where userid=xx and date = yy and order_id>zz,这样返回0到多行数据;也可以select * from orders where userid=xx and date = yy,完全不限制order_id这样就返回全部(xx,yy)对应下的数据;还可以select * from orders where userid=xx and (date, order_id) > (yy, zz) 来查询从(xx,yy,zz)开始的数据并且不限制date一定是yy。但是,你不能select * from orders where userid=xx and order_id=zz,因为不限制date只限制order_id的结果必然是不连续的,而Cassandra只支持连续的范围查询。
能意识到CQL的这个限制,并且知道这个限制的内在原理,就基本上懂Cassandra的数据模型了。在懂这个之前,是没有能力甚至说没有资格用Cassandra的。因此实际上我个人觉得Cassandra搞的CQL虽然比较强大并且可以把数据schema化,但如果不仔细看文档是容易入坑的,而很多码农说实话没有仔细看文档的意识和能力……所以如果有公司用Cassandra,在新手设计schema的时候一定要有老手来review防止进坑。一些高级功能虽然可以绕过这个限制,搞得“非常像SQL”,比如secondary index、materialized view,但都和关系型数据库实现类似功能有本质区别,并不是随便能乱用的,可能因使用不当造成性能问题,甚至修改数据的同时修改这些辅助数据可能是不保证隔离性的因而导致读到脏数据。
Cassandra因为其NoSQL的本质,对跨行(CQL row)查询的支持比较弱,至于跨wide-column row的能力就更弱,更别说跨行事务了,因为随便一个跨wide-column row的查询都需要扫整个表(暂不考虑3.0新增的materialized view,毕竟3.0连正式版都还没发),扫整个表就意味着扫整个集群,开销非常大。也因此CQL是不太需要什么“慢查询log”的,因为应该连用都不用,杜绝一切可能慢的查询。
总而言之,如果说NoSQL是Not only SQL,那CQL就是——Cassandra does not have SQL。
CQL当然也有他的好处,最大的好处就是为NoSQL带来了schema。用刘奇的话说,数据库的schema有点像编程语言中的静态类型,约束更严格,少了一些自由,项目越大可能优势越明显。同时CQL绕过thrift的束缚从下到上完整造了轮子后,更容易优化性能,表达能力更强也方便加新功能。同时只要配合各种client的driver,就可以简化代码不需要写裸的CQL语句,用起来不比thrift麻烦。
发表回复