Skip to content

Files

Latest commit

c70125a · Jan 8, 2022

History

History
1895 lines (1270 loc) · 95 KB

3.md

File metadata and controls

1895 lines (1270 loc) · 95 KB

三、Cassandra 和 CQL 数据建模

如果你来自关系世界,你需要一些时间来适应 Cassandra 解决问题的多种方式。最难放下的是数据规范化。在关系世界中,数据被分割成多个表,因此数据几乎没有冗余。数据经过逻辑分组和存储,如果我们需要这个数据的某个视图,我们会进行查询并向用户呈现所需的数据。

Cassandra 的故事有点不同。我们必须从一开始就考虑如何查询我们的数据。这导致了对数据库的大量写命令,因为每次我们在 Cassandra 中存储一些东西时,数据经常会被插入到多个表中。这里可能会想到的第一个想法是,这不会有什么反应,也不会很快,但请记住,Cassandra 写得真的很快。

你可能担心的另一个问题是,这可能会存储比我们实际需要的多得多的数据,这一点你是对的;Cassandra 可能会比关系数据库在磁盘上存储更多的数据。当运行查询时,这种繁重的编写是值得的,因为查询已经准备好了,并且没有复杂、耗时的数据连接。整个 Cassandra 哲学可以用磁盘便宜这个前提来概括。

比较关系和 Cassandra 数据存储

要比较经典的关系存储和 Cassandra,我们需要从模型开始。举个例子,我们将使用一个在线二手车市场。让我们从关系数据模型开始。

图 35:在线汽车市场的简单关系模型

图 35 显示了一个在线二手车市场的简单关系模型。图中的每个框都将成为关系数据库中的一个单独的表。框内的元素将成为表中的列。存储的每一行都会将模型中定义的每一列保存到磁盘中。如果表的创建者指定了它,列可以有未定义的值或空值,但它们仍然会存储在磁盘上。当存储图 35 中模型的关系数据时,它可能看起来像下图。

图 36:存储的关系数据的例子

要研究 Cassandra 存储数据的方式,我们首先必须了解数据实际上是如何构造的。

图 37:Cassandra 数据结构

我们已经讨论了键空间和柱族。列族也常常简称为表。Cassandra 中的一行可以定义所有列,也可以只定义部分列。Cassandra 只会将实际值保存到行中。Cassandra 对一行的限制是它必须适合单个节点。行的第二个限制是它最多可以有 20 亿列。在大多数情况下,这已经足够了,如果超过这个数字,您定义的数据模型可能需要一些调整。

上图显示了一行中存储值和时间戳的列。列总是按列名在一行中排序。在接下来的部分中,我们将讨论如何用 Cassandra 建模二手车市场的例子,但是现在,让我们只显示用户的表将如何存储在 Cassandra 中。请注意,我们没有像关系数据建模那样经常使用自动递增键;相反,我们使用了自然密钥用户名。现在,让我们跳过 CQL 的东西,只在抽象层次上看一下存储的数据:

图 38:Cassandra 存储示例用户数据

上图显示了 Cassandra 使用的低级数据存储技术。这个例子与关系数据库存储数据的方式非常相似。现在,让我们看看如果行键是状态,而聚类列是用户名,数据的存储方式。此外,让我们将约翰·q·公共添加到纽约来说明这种差异。

图 39: Cassandra 用状态作为分区键存储示例用户数据

上图中的数据存储目前可能看起来有点奇怪,因为列名在关系数据库存储系统中是相当固定的。它使用用户名将属于单个用户的列分组到一个组中。请记住,列总是按名称排序的。还要注意,用户名没有列,它只存储在列名中。但是,这种存储方法不允许通过用户名直接获取用户。要访问行(分区)中包含的任何数据,我们需要提供一个状态名。

现在我们知道了 Cassandra 是如何在内部处理数据的,让我们在实践中尝试一下。到目前为止,我们已经讨论了很多 Cassandra 概念,以及它们与关系数据库的区别。在下一节中,我们将深入探讨 CQL,这是一种用于与 Cassandra 互动的语言。

CQL

CQL,或 Cassandra 查询语言,不是与 Cassandra 互动的唯一方式。不久前,节俭应用编程接口是与之交互的主要方式。这个应用编程接口的用法、组织和语法面向直接向用户公开 Cassandra 存储的内部机制。

CQL 是官方推荐的与 Cassandra 互动的方式。CQL 目前的版本是 CQL3。CQL3 是对 CQL2 的重大更新。它添加了CREATE TABLE语法以允许多列主键、WHERE子句中除分区键之外的列上的比较运算符、ORDER BY语法等等。与标准 SQL 的主要区别在于,CQL 不支持连接或子查询。事实上,CQL 的主要目标之一是给用户一种熟悉的使用 SQL 的感觉。

CQL 壳牌公司

在前一章中,我们研究了如何安装和运行数据税开发中心。如果不想使用 DataStax DevCenter,请转到安装 Cassandra 的目录,然后转到 bin 目录并运行cqlsh实用程序。

    # ./cqlsh
    Connected to Test Cluster at localhost:9160.
    [cqlsh 4.1.1 | Cassandra 2.0.9 | CQL spec 3.1.1 | Thrift protocol 19.39.0]
    Use HELP for help.
    cqlsh>

代码清单 3

运行 CQL 外壳后,您可以开始向 Cassandra 发出指令。CQL 贝壳适合 Cassandra 的大部分日常工作。事实上,由于它的命令行特性和可用性,它是管理员的首选工具。请记住,它与 Cassandra 捆绑在一起。DevCenter 更适合开发复杂的脚本和提高开发人员的效率。

Keyspace(键空间)

在使用 Cassandra 中的表进行任何工作之前,我们必须为它们创建一个容器,也称为键空间。键空间的主要用途之一是为一组表定义复制机制。我们将首先为我们的二手车市场示例定义一个关键空间。

    CREATE KEYSPACE used_cars
          WITH replication = {
                  'class': 'SimpleStrategy',
                  'replication_factor' : 1};

代码清单 4

创建键空间时,我们必须指定复制。定义复制的两个组件是classreplication_factor。在我们的例子中,我们使用了SimpleStrategy作为类选项;它将数据复制到环上的下一个节点,而没有任何网络感知机制。

目前,我们处于集群中只有一个节点的开发环境中,因此复制因子为 1 就足够了。事实上,如果我们指定更高的复制因子(例如 3),然后在执行选择时使用quorum选项,选择命令将会失败,因为复制因子 3 无法到达quorum,因为有 2 个节点必须响应,而此时我们的群集中只有一个节点。如果我们向集群中添加了更多节点并希望复制数据,我们可以使用ALTER KEYSPACE命令更新复制因子,如下面的代码示例所示。

    ALTER KEYSPACE used_cars
          WITH REPLICATION = {
                  'class' : 'SimpleStrategy',
                  'replication_factor' : 3};

代码清单 5

SimpleStrategy在某些情况下会不够。第一章提到 Cassandra 使用一个名为飞贼的组件来确定网络拓扑,然后使用不同的网络感知复制机制。要在键空间上启用这种行为,我们必须使用NetworkTopologyStrategy复制类,如下例所示。

    CREATE KEYSPACE used_cars
          WITH replication = {
                  'class': 'NetworkTopologyStrategy',
                  'DC1' : 1,
                  'DC2' : 3};

代码清单 6

前面的示例复制了used_cars键空间,其中复制因子 1 在 DC1 中,复制因子 3 在 DC2 中。数据中心名称 DC1 和 DC2 在节点配置文件中定义。我们当前的环境中没有指定任何数据中心,但是该命令仍然会运行,并使用我们指定的选项创建一个键空间。

在某些情况下,我们需要删除完整的键空间。这是通过DROP KEYSPACE命令实现的。这个命令非常简单,只有一个参数:我们要删除的键空间的名称。键空间的移除是不可逆的,发出命令会立即移除键空间及其所有列族和它们的数据。

    DROP KEYSPACE used_cars;

代码清单 7

DROP KEYSPACE命令主要用于开发场景和/或迁移场景。如果删除键空间,数据将从系统中删除,并且只能从备份中恢复。

在 CQL shell 中发出命令时,我们通常必须提供一个键空间和表。以下SELECT命令列出了used_cars键空间中的所有用户。

    SELECT * FROM used_cars.users;

    username  | first_name | last_name | state
    ----------+------------+-----------+-------
         jdoe |       John |       Doe |    NY
       jsmith |       John |     Smith |    CA

代码清单 8

随着时间的推移,这变得有点乏味,并且按顺序发布的大多数操作通常都在一个键空间中。所以,为了减少写入开销,我们可以发出USE命令。

    USE used_cars;

代码清单 9

运行USE命令后,所有后续查询都在指定的键空间内运行。这使得编写查询变得更加容易。

桌子

定义和创建表是构建任何应用程序的基础。虽然 CQL 的语法类似于 SQL,但是在创建表时有一些重要的区别。让我们从存储用户的最基本的表开始。

    CREATE TABLE users (
          username text,
          password text,
          first_name text,
          last_name text,
          state text,
          PRIMARY KEY (username)
    );

代码清单 10

定义表格时需要PRIMARY KEY。它可以有一个或多个组件列,但不能是计数器列。如果PRIMARY KEY只有一列,可以在指定列类型后将PRIMARY KEY放入列定义中。

    CREATE TABLE users (
          username text PRIMARY KEY,
          password text,
          first_name text,
          last_name text,
          state text
    );

代码清单 11

如果我们使用 SQL 和关系数据库来指定PRIMARY KEY中的列,那么所有的列都将得到相同的处理。在前一章中,我们讨论了 Cassandra 如何存储数据,以及所有数据都保存在宽行中。每行由一个标识来标识。在 Cassandra 术语中,这个 ID 被称为分区键。分区键是PRIMARY KEY定义中列列表的第一列。

| | 注意:主键列表中的第一列是分区(行)键。 |

让我们用一个例子来看看PRIMARY KEY列表中第一个位置之后指定的列是怎么回事。当我们比较关系数据存储和 Cassandra 数据存储时,我们将对显示的产品进行建模。我们之前提到过,在使用 Cassandra 时,规范化并不常见。我们使用的关系示例将关于汽车产品的数据存储到五个表中。在这里,我们将只使用一张桌子,至少现在是这样。

    CREATE TABLE offers (
          username text,
          date timestamp,
          price float,
          brand text,
          model text,
          year int,
          mileage int,
          color text,
          PRIMARY KEY (username, date)
    );

代码清单 12

PRIMARY KEY定义由两列组成。列表中的第一列是分区键。分区键后面列出的所有列都是群集键。聚类键影响 Cassandra 在存储级别组织数据的方式。为了简单起见,我们将只关注品牌和颜色,以显示聚类键的功能。查询时,包含报价的表可能如下所示。

    username  | date                     | brand  | color
    ----------+--------------------------+--------+-------
         jdoe | 2014-08-11 17:12:32+0200 | Toyota |  Blue
       jsmith | 2014-09-09 11:35:20+0200 |    BMW |   Red
       jsmith | 2014-09-19 11:35:20+0200 |    BMW | Black

代码清单 13

这看起来非常类似于我们对关系存储中的表的期望。

当涉及到物理存储数据时,情况就有点不同了。聚类键date与其他列的名称组合在一起。这将导致报价在行内按日期排序。

图 40:Cassandra 用日期作为聚类键存储报价数据

现在的问题是,如果我们不把date加到PRIMARY KEY列表的第二位,导致它成为聚类键,会怎么样?如果您仔细看,上图中的列名是日期和列名的组合,如品牌和颜色。如果我们去掉date作为聚类键,每个用户将只有一个二手车优惠,仅此而已。date将成为另一个专栏,我们将失去为每个用户增加更多优惠的可能性。

有时候数据对于一行来说太大了。在这种情况下,我们将行标识与其他数据结合起来,将数据进一步分割成更小的块。例如,如果我们的网站变得非常受欢迎,如果所有汽车零售商都在我们的网站上提供他们的汽车,我们将不得不分割数据。或许最合理的方法是按照汽车品牌来划分数据。

    CREATE TABLE offers_by_brand (
          username text,
          date timestamp,
          price float,
          brand text,
          model text,
          year int,
          mileage int,
          color text,
          PRIMARY KEY ((username, brand), date)
    );

代码清单 14

请注意,如果一行是用户名和品牌的组合,则在提取该行时,如果不指定用户名和品牌,我们将无法访问优惠。有时当使用这种技术时,应用程序必须组合数据以将其呈现给用户。由多列组成的分区(行)键称为复合分区键。复合分区键是通过在括号中列出列来定义的。请记住,只有列表中的第一列是分区键。如果我们想让更多的列进入分区键,我们必须把它们放在括号里。

CQL 数据类型

至此,我们已经使用了不同的类型来指定列数据,但是还没有讨论它们。下表简要概述了 Cassandra 中的数据类型。

表 1: CQL 数据类型

类型 描述
美国信息交换标准码 美国 ASCII 字符串
比吉斯本 64 位有符号长
一滴 任意字节(无验证),在 CQL shell 中表示为十六进制。开发中心只显示<>。
布尔 对还是错
计数器 64 位分布式计数器值
小数 可变精度小数
两倍 64 位 IEEE-754 浮点
漂浮物 32 位 IEEE-754 浮点
inet IP 地址字符串(支持 IPV4 和 IPV6 格式)
(同 Internationalorganizations)国际组织 32 位有符号整数
目录 有序元素的集合
地图 关联数组
设置 元素的无序集合
文本 UTF 8 编码字符串
时间戳 日期和时间,编码为从 1.1.1970 开始的 8 字节整数
uuid 标准格式的 UUID
timeuuid UUID 的价值带有时间印记
可变长字符串 UTF 8 编码字符串
varint 任意精度整数

列类型是通过在列名后指定来定义的。如果类型兼容,可以更改列类型。如果类型不兼容,查询将返回一个错误。还要注意,应用程序可能会停止运行,因为这些类型在应用程序级别可能不兼容。不可能更改群集列或其上定义了索引的列。例如,不允许将year列类型从int更改为text,这将导致以下错误。

    ALTER TABLE offers_by_hand ALTER year TYPE text; Bad Request: Cannot change year from type int to type text: types are incompatible.

代码清单 15

向 Cassandra 表中添加列是一种常见且标准的操作。

    ALTER TABLE offers ADD airbags int;

代码清单 16

前面的代码清单向表中添加了一个名为airbags的整数类型列。在我们的示例中,该列存储了车辆的安全气囊数量。通过ALTER TABLE命令中的DROP子命令删除该列。

    ALTER TABLE offers DROP airbags;

代码清单 17

为了完成一个表的生命周期,我们必须再运行两个命令。第一个是清空表中的所有数据。

    TRUNCATE offers;

代码清单 18

最后一个命令是删除整个表。

    DROP TABLE offers;

代码清单 19

表属性

除了列名和类型,CQL 还可以用来设置表的属性。一些属性,如注释,用于更容易的维护和开发,一些深入到 Cassandra 的内部工作。

表 2: CQL 表属性

财产 描述
bloom_filter_fp_chance 表布隆过滤器的假阳性概率。该值的范围从产生最大可能的布隆过滤器的 0 到禁用布隆过滤器的 1.0。推荐值为 0.1。默认值取决于压缩策略。大小压缩的默认值为 0.01,级别压缩的默认值为 0.1。
贮藏 缓存优化。可用的级别有“全部”、“仅键”、“仅行”和“无”。应谨慎使用 rows _ only 选项,因为当该选项启用时,Cassandra 会将大量数据放入内存。
评论 主要由管理员和开发人员用来对表进行注释和注释。
压紧 设置表格的压缩策略。有两种:默认的 SizeTieredCompactionStrategy 和 LeveledCompactionStrategy。大小当表超过某个限制时,分层触发压缩。这个策略的积极方面是它不会降低写性能。负面影响是,它偶尔会使用两倍于磁盘的数据大小,并且读取性能可能很差。分级压缩有多级表。最低级别有 5 MB 的表。随着时间的推移,这些表被合并成一个大 10 倍的表;这导致非常好的读取性能。
压缩 确定如何压缩数据。用户可以选择速度或节省空间。速度越大,节省的磁盘空间就越少。按最快到最慢的顺序,压缩依次为 Lz4 压缩程序、SnappyCompressor、放气压缩程序。
dclocal_read_repair_chance 调用读取修复的概率。
gc _ grace _ 秒 等待删除带有墓碑的数据的时间。默认值为 10 天。
填充 _io_cache_on_flush 默认情况下,此值被禁用;仅当您希望所有数据都适合内存时,才启用此选项。
读取修复机会 介于 0 和 1.0 之间的数字,指定未达到仲裁时修复数据的概率。默认值为 0.1
写时复制 这仅适用于计数器表。设置后,复制副本将写入所有受影响的复制副本,忽略指定的一致性级别。

用注释定义表非常容易,并且代表了维护和管理数据库的积极实践。

    CREATE TABLE test_comments (
          a text,
          b text,
          c text,
          PRIMARY KEY (a)
    ) WITH comment = 'This is a very useful comment';

代码清单 20

大多数选项都很容易定义。添加注释是这种选项的一个很好的例子。另一方面,压缩和压缩选项具有子属性。子属性的定义是在类似 JSON 的语法的帮助下完成的。我们将很快看一个例子。

表 3: CQL 表压缩选项

财产 描述
表压缩 指定要使用的压缩算法。上表中列出了可用的算法:ly4 压缩器、SnappyCompressor 和放气压缩器。要禁用压缩,只需使用空字符串。
区块长度 表按块压缩。较大的值通常会提供更好的压缩率,但会增加读取的数据大小。默认情况下,此选项设置为 64KB。
crc _ check _ 偶然性 Cassandra 中的所有压缩数据都有一个校验和块。该值用于检查数据是否损坏,以便不会发送到其他副本。默认情况下,此选项设置为 1.0,以便每次读取数据时,节点都会检查校验和值。将该值设置为 0 将禁用校验和检查,将其设置为 0.33 将导致每三次读取数据时检查一次校验和。

操作压缩选项可以带来显著的性能提升,许多 Cassandra 解决方案都将压缩选项设置为非默认值。事实上,调整后的压缩选项有时对于成功的 Cassandra 部署非常重要,但大多数情况下,尤其是如果您刚刚开始使用 Apache Cassandra,使用默认的SnappyCompressor设置作为压缩选项会很好。

压缩也有许多子属性。

表 4: CQL 表压缩选项

财产 描述
使能够 确定是否在表上运行压缩。默认情况下,所有表都启用了压缩。
墓碑 _ 阈值 从 0 到 1 的比率值,指定多少列必须用墓碑标记才能开始压缩。默认值为 0.2。
墓碑 _ 压缩 _ 间隔 表创建后开始压缩的最短等待时间,但仅在达到 tombstone_threshold 后。默认设置为一天。
未选中 _ 墓碑 _ 压缩 启用主动压缩,即使表未达到阈值,也按检查的时间间隔运行压缩。默认情况下,该值设置为 false。
最小表大小 与 SizeTieredCompactionStrategy 一起使用。此选项用于防止将表分组为过小的块。默认设置为 50MB。
最小阈值 可在 SizeTieredCompactionStrategy 中获得。表示开始一个小型压缩过程所需的最小表数。默认设置为 4。
最大阈值 仅在 SizeTieredCompactionStrategy 中可用。设置小压缩处理的最大表格数。默认设置为 32。
桶 _ 低 仅适用于 SizeTieredCompactionStrategy。检查大小差异低于组平均值的表。默认值为 0.5,这意味着只有大小相差最大 50%的表。
桶 _ 高 仅适用于 SizeTieredCompactionStrategy。检查大小大于组平均值的表上的压缩情况。默认设置为 1.5,这意味着所有表都比组平均值大 50%。
stable_size_in_mb 仅在 LeveledCompactionStrategy 中可用。表示目标表大小,但该大小可能稍大或稍小,因为行数据从不在两个表之间拆分。默认设置为 5MB。

压缩选项和压缩选项是用类似 JSON 的语法定义的。

    CREATE TABLE inventory (
      id uuid,
      name text,
      color text,
      count varint,
      PRIMARY KEY (id)
    ) WITH
          compression = {
               'sstable_compression' : 'DeflateCompressor',
               'chunk_length_kb' : 64
          }
        AND
           compaction = {
               'class' : 'SizeTieredCompactionStrategy',
               'min_threshold' : 6
           };

代码清单 21

对数据进行聚类排序

之前我们注意到磁盘上一行中的列是经过排序的。当从表中的物理行提取数据时,我们得到由指定的聚类键预分类的数据。默认情况下,如果数据按升序排列,那么按降序取数据会导致性能问题。为了防止这种情况发生,我们可以指示 Cassandra 用CLUSTERING ORDER BY以降序将数据保存在表的一行中。

    CREATE TABLE latest_offers (
          username text,
          date timestamp,
          price float,
          brand text,
          model text,
          year int,
          mileage int,
          color text,
          PRIMARY KEY (username, date)
    ) WITH CLUSTERING ORDER BY (date DESC);

代码清单 22

重要的是要记住,前面的CREATE TABLE示例保持数据按降序排序,但仅在行内。这意味着从具有用户名的特定用户行中选择数据将显示该用户的排序数据,但是如果您从表中选择所有数据而不提供行键(username),则数据不会按username排序。

| | 提示:聚类对分区内的数据进行排序,而不是分区。 |

但是为了更好地记住这一切,让我们考虑一个例子。让我们回到我们的offers桌。默认情况下offers表是按升序排序的,但是这对于演示上一个技巧的全部内容来说是很好的。首先,我们将为jsmith用户选择优惠。

    SELECT username, date, brand, color
        FROM offers WHERE username = 'jsmith';

    username  | date                     | brand | color
    ----------+--------------------------+-------+-------
       jsmith | 2014-09-09 11:35:20+0200 |   BMW |   Red
       jsmith | 2014-09-19 11:35:20+0200 |   BMW | Black
       jsmith | 2014-09-20 17:12:32+0200 |  Audi | White

代码清单 23

请注意,这些报价按升序排序,最早的记录排在第一位。如果数据按date排序,这就是我们所期望的,因为这个列是聚类的关键。但是一些 Cassandra 用户在不提供行键(username)的情况下试图访问整个表时可能会感到困惑:

    SELECT username, date, brand, color
        FROM offers;

    username  | date                     | brand  | color
    ----------+--------------------------+--------+--------
         jdoe | 2014-08-11 17:12:32+0200 | Toyota |   Blue
         jdoe | 2014-08-25 11:13:22+0200 |   Audi | Orange
       jsmith | 2014-09-09 11:35:20+0200 |    BMW |    Red
       jsmith | 2014-09-19 11:35:20+0200 |    BMW |  Black
       jsmith | 2014-09-20 17:12:32+0200 |   Audi |  White
         adoe | 2014-08-26 10:11:10+0200 |     VW |  Black

代码清单 24

请注意,数据没有按username(行标识,分区键)列排序。同样,数据在用户名内排序,而不是在用户名之间排序。

操纵数据

到目前为止,我们主要集中在数据结构以及组织存储数据的各种选项和技术上。我们已经介绍了基本的数据读取,但是我们很快会看到更高级的数据检索示例。插入数据看起来很像在标准的 SQL 数据库中;我们定义表名和列,然后指定值。最简单的例子之一是将数据输入users表。

    INSERT INTO
        users (username, password, first_name, last_name, state)
          VALUES ('jqpublic', 'hello1',  'John', 'Public', 'NY');

代码清单 25

前面的代码展示了如何将数据插入到users表中。它只有文本列,所以所有插入的值都是字符串。一个更复杂的例子是将值插入offers表,如下例所示。

    INSERT INTO
          offers (username, date, price, brand, model, year,
                mileage, color)
          VALUES ('jsmith', '2014-09-19 11:35:20', 6000, 'BMW', '120i',
                   2010, 40000, 'Black');

代码清单 26

处理字符串和数字非常简单。数字或多或少是简单的;十进制分隔符始终是句点,因为插入参数用逗号分隔。数字不能以加号开头,只能以减号开头。数字也可以用科学符号来输入,所以在指定浮点数时,“E”和“E”都是有效的。请注意,DataStax DevCenter 将使用“e”定义的数字标记为不正确的语法用法,而 CQL shell 将这两个符号都解释为有效。在指定指数的“e”后面,可以跟一个加号或减号。下面的代码示例插入一辆价格为 8000 美元的汽车,该汽车用科学符号表示,前缀为负指数。

    INSERT INTO
          offers (username, date, price, brand, model, year,
                mileage, color)
            VALUES ('jsmith', '2014-05-11 01:22:11', 80000.0E-1, 'FORD',
                   'Orion', 206, 200000, 'White');

代码清单 27

字符串总是放在单引号内。如果数据中需要单引号,请在将它放入字符串之前用另一个单引号转义它,如下例所示。

    INSERT INTO test_data(stringval) VALUES ('O''Hara');

代码清单 28

除了数字和字符串,前面的查询还包含日期。日期在 Cassandra 中被定义为timestamp类型。A timestamp可以简单的输入一个整数,表示从 1970 年 1 月 1 日 00:00:00 GMT 开始经过的毫秒数。在 Cassandra 中用于输入timestamp数据的字符串以下列格式输入。

表 5:时间戳字符串文字格式

| 格式 | 例子 | | yyyy-mm-dd HH:mm | '2014-07-24 23:23' | | yyy-mm-dd HH:mm:ss | '2014-07-24 23:23:40' | | yyyy-mm-dd HH:mmZ | '2014-07-24 23:23+0200' | | yyyy-mm-dd HH:mm:ssZ | '2014-07-24 23:23:40+0200' | | 年-月-日'时:毫米 | 2014-07-24T23:23 ' | | yyy-mm-dd'T'HH:mmZ | 2014-07-24T23:23+0200 ' | | yyy-mm-dd'T'HH:mm:ss | 2014-07-24T23:23:40 ' | | yyyy-mm-dd'T'HH:mm:ssZ | 2014-07-24T23:23:40+0200 ' | | yyyy-mm-dd | '2014-07-24' | | yyyy-mm-ddZ | '2014-07-24+0200' |

这种格式在许多编程语言和数据库环境中是非常标准的。除了使用空格分隔日期和时间部分之外,有时还会用 t 分隔。时区是以四位数的格式指定的,并且在上表的格式示例中用字母 Z 表示。时区可以以加号或减号开始,具体取决于时区相对于格林尼治标准时间的位置。前两位数字代表小时的差异,后两位数字代表分钟的差异。

与格林尼治时间相比,大多数时区都是整数差。一些地区有额外的半小时补偿,如斯里兰卡、阿富汗、伊朗、缅甸、纽芬兰、委内瑞拉、尼泊尔、查塔姆群岛和澳大利亚的一些地区。如果未指定时区,则使用来自协调器节点的时区。大多数文档建议指定时区,而不是依赖协调器节点时区。如果未指定时区,则假设您想要输入“00:00:00”作为时区。

在 CQL 外壳中显示timestamp值的默认格式是“yyyy-mm-dd HH:mm:ssZ”。该格式向用户显示所有关于timestamp值的可用信息。

有时您会想要更新已经写入的数据。与标准的 SQL 数据库系统一样,可以更新记录,但是 Cassandra 中UPDATE命令的内部工作方式有点不同。让我们改变用户jdoe2014-08-11上提供的产品的品牌和型号。

    UPDATE offers SET
        brand = 'Ford', model = 'Mustang'
      WHERE
        username = 'jdoe' AND date='2014-08-11 17:12:32+0200';

代码清单 29

更新该值实际上会在该行中添加一个带有新时间戳的新列,并用墓碑标记旧列。墓碑不是马上立的,数据只是写的。随着第一次读取或压缩过程的开始,Cassandra 将比较这两列。如果列名相同,时间戳较新的列将获胜。其他列用墓碑标记。这在下图中更容易可视化。

图 41:更新列:旧的列得到墓碑,新的列被添加新的值

在操作数据时,比较 Cassandra 和 SQL 解决方案还有另一个重要的区别。让我们回顾一下当前的表数据。

    SELECT username, date, brand, model FROM offers;

    username  | date                     | brand | model
    ----------+--------------------------+-------+---------
         jdoe | 2014-08-11 17:12:32+0200 |  Ford | Mustang
         jdoe | 2014-08-25 11:13:22+0200 |  Audi |      A3
       jsmith | 2014-05-11 01:22:11+0200 |  Ford |   Orion
       jsmith | 2014-09-09 11:35:20+0200 |   BMW |    118d
       jsmith | 2014-09-19 11:35:20+0200 |   BMW |    120i
       jsmith | 2014-09-20 17:12:32+0200 |  Audi |      A6
         adoe | 2014-08-26 10:11:10+0200 |    VW |    Golf

代码清单 30

现在,让我们尝试插入来自jdoe用户的第一个报价,但使用不同的品牌和型号。

    INSERT INTO offers (
          username, date, price, brand, model,
          year, mileage, color)
          VALUES (
                  'jdoe', '2014-08-11 17:12:32',
                  7000, 'Toyota', 'Auris 2.0d', 2012, 15000, 'Blue');

代码清单 31

你认为之前的INSERT声明会有什么结果?嗯,如果这是一个经典的 SQL 数据库,我们可能会得到一个错误,因为系统已经有相同用户名和日期的数据。Cassandra 有点不同。如前所述,INSERT增加了新的列,就是这样。新的栏目将与旧的栏目名称相同,但它们将有新的时间戳。所以在读取数据时,Cassandra 会返回最新的列。在运行了前面的INSERT语句之后,运行与代码清单 30 中相同的查询将返回以下内容。

    SELECT username, date, brand, model FROM offers;

    username  | date                     | brand  | model
    ----------+--------------------------+--------+------------
         jdoe | 2014-08-11 17:12:32+0200 | Toyota | Auris 2.0d
         jdoe | 2014-08-25 11:13:22+0200 |   Audi |         A3
       jsmith | 2014-05-11 01:22:11+0200 |   Ford |      Orion
       jsmith | 2014-09-09 11:35:20+0200 |    BMW |       118d
       jsmith | 2014-09-19 11:35:20+0200 |    BMW |       120i
       jsmith | 2014-09-20 17:12:32+0200 |   Audi |         A6
         adoe | 2014-08-26 10:11:10+0200 |     VW |       Golf

代码清单 32

在某些情况下,您会想要删除数据。这是通过DELETE语句完成的。通常的 SQL 系统允许调用DELETE语句,而不指定要删除哪些行。这将删除表中的所有行。有了 Cassandra,你必须提供DELETE声明的WHERE部分,它才能工作,但是有一些选择。我们总是可以在WHERE部分指定分区键。这将删除整行内容,实际上也将删除用户提供的所有内容。

    DELETE FROM offers WHERE username = 'maybe';

代码清单 33

前面的语句相当危险,因为它删除了整个行,并且是用我们示例中没有的用户名完成的。通常,在删除数据时,会指定所有的主键列,而不仅仅是分区(第一个)键。

    DELETE FROM offers 
       WHERE username = 'jdoe' AND date = '2014-08-11 17:12:32+0200';

代码清单 34

数据操纵角案例

在前一节中,我们使用了一个INSERT语句,该语句使用了相同的分区和聚类键,在列中设置了新的数据,而不是更新行。这种技术被称为向上插入。

向上插入是 Cassandra 非常流行的技术,实际上是更新数据的推荐方式。它还使系统维护和实现更加容易,因为我们不必设计和实现额外的更新查询。

| | 提示:尽可能使用旧密钥插入新数据,从而更新数据。 |

在 Cassandra 中,大多数时候应该做的是向上插入数据,但是让我们将其与插入进行比较。让我们假设,出于某种原因,用户可以随时更改我们示例中的所有优惠。我们将分析这些字段,看看有什么变化是可能的。

让系统更改报价上的username是不明智的。如果留给任何用户处理,这实际上是一个严重的安全问题。为了进行比较,我们假设系统管理员能够更改username。让我们试着改变它。

    UPDATE offers
        SET username = 'maybe'
            WHERE username = 'jdoe'
                AND date = '2014-08-11 17:12:32+0200';

    Bad Request: PRIMARY KEY part username found in SET part

代码清单 35

好吧,这没用,但是当我们在做的时候,让我们试着改变报价date并用UPDATE命令将其设置为报价日期后的一天。date字段不是分区键。实际上,它是一个群集键,所以它可能只是工作。

    UPDATE offers
        SET date = '2014-08-12 17:12:32+0200'
            WHERE username = 'jdoe'
                AND date = '2014-08-11 17:12:32+0200';
     Bad Request: PRIMARY KEY part username found in SET part

代码清单 36

好吧,这也没用。date也是主键的一部分。您可能已经预料到了,因为在第一次尝试中收到了错误消息。不可能更新主键列。

这个例子很重要,要记住。Cassandra 不允许更新任何主键字段。

| | 注意:Cassandra 不允许更新主键字段。 |

如果您有一些可能需要这种更新的数据,并且这些数据发生了很大的变化,您可能需要考虑其他存储技术,或者将这种逻辑转移到应用程序级别。这可以分两步完成。步骤可以互换;选择留给系统设计者。让我们考虑一个例子,其中第一步是插入新数据,第二步是删除旧数据。

    INSERT INTO offers (
          username, date, price, brand, model,
          year, mileage, color)
          VALUES ('maybe', '2014-08-11 17:12:32',
               7000, 'Toyota', 'Auris 2.0d', 2012, 15000, 'Blue');

    DELETE FROM offers
        WHERE username = 'jdoe' AND date = '2014-08-11 17:12:32+0200';

代码清单 37

DELETE语句发出之前,数据库会处于不一致的状态,但是如果我们颠倒一下步骤,情况会是一样的。如果INSERT发生在DELETE之前,系统中可能会出现两个报价。如果DELETE发生在INSERT之前,报价可能会丢失。如前所述,这是由系统设计者来决定两个邪恶中哪个更小。

前一种情况在大多数情况下都可以正常工作。尽管如此,有时由于各种并发技术,前面的查询可能不会按照我们计划的顺序运行。从 shell 或 DevCenter 发出前面的命令将总是按照这个顺序运行,但是当应用程序发出多个请求时,可能会导致不一致。

此外,有些系统的时钟可能不是 100%同步的。例如,如果时钟甚至在毫秒范围内有偏移,那么命令必须一个接一个地发出,这一点非常重要。只需将USING TIMESTAMP <integer>添加到您想要以特定顺序运行的语句中。Cassandra 集群将使用提供的USING TIMESTAMP代替他们收到命令的时间。在大多数情况下,在应用程序级别使用此选项会阻止对准备好的语句进行缓存,因此请明智地使用它。此外,请注意,如果用户通过键入按顺序给出命令,通常不会发出此选项。

使用TIMESTAMP选项可能有点棘手。例如,如果由于某种原因,我们发出了一个DELETE,那就是在未来,我们可能会正常地处理一个条目,然后想知道为什么它在没有发出DELETE命令的情况下就消失了。

无论如何,下面的代码示例使用TIMESTAMP选项发出前面的INSERTDELETE语句组合。

    INSERT INTO offers (
          username, date, price, brand, model,
          year, mileage, color)
          VALUES ('jdoe', '2014-08-11 17:12:32+0200',
                  7000, 'Toyota', 'Auris 2.0d', 2012, 15000, 'Blue')
                  USING  TIMESTAMP 1406489822417000;

    DELETE FROM offers USING TIMESTAMP 1406489822417001
        WHERE username = 'jdoe' AND date = '2014-08-11 17:12:32+0200';

代码清单 38

INSERT语句在末尾指定了TIMESTAMP选项。在DELETE声明中,它被指定在声明的WHERE部分之前。TIMESTAMP选项很少使用,但它包含在这里,因为了解这个选项可能会在 Cassandra 中处理数据时遇到奇怪的问题时为您节省一些时间。

询问

到目前为止,我们已经花了很多时间来探索数据操作,因为要处理查询,我们必须在数据库中创建一些数据。检索数据的命令叫做SELECT,它出现在本书前面几节的几个例子中。对于那些对经典 SQL 数据库有经验的人来说,适应命令的语法不会有任何问题,但是命令的行为可能会看起来有点奇怪。记住,对于 Cassandra,重点是可伸缩性。

将 CQL 与 SQL 进行比较时,有两个主要区别:

  • 不支持JOIN操作。
  • COUNT外,没有其他聚合函数可用。

使用经典的 SQL 解决方案,可以将任意多的表连接在一起,然后组合数据以生成各种视图。在 Cassandra 中,由于可伸缩性,避免了连接。对于开发人员和架构师来说,预先考虑他们想要查询什么,然后在编写阶段相应地填充表是一个很好的做法。这将导致大量的数据冗余,但这实际上是 Cassandra 的常见模式。此外,我们在前一章中提到,在使用 Cassandra 构建应用程序时,磁盘被认为是最便宜的资源。

唯一允许的聚合函数是COUNT,在实现分页解决方案时经常用到。使用COUNT函数要记住的重要一点是,它在某些情况下不会返回实际的计数值,并且该函数受到限制。最常见的限制值是 10000,因此如果预期的计数较大,但函数恰好返回 10000,则可能需要增加限制才能获得确切的值。同时,要非常小心,记住这可能会导致性能问题。

在前面的部分中,我们讨论了基本的SELECT语句,并限制自己显示特定的列,因为没有足够的空间来很好地显示所有的表列。构建查询很容易,因为我们知道表中的键空间名、表名和列名。由于图形界面,开发中心使这变得更加容易,但是对于那些使用 CQL 外壳的人来说,这种情况需要几个命令来解决。最有用的命令是DESCRIBE。一旦您使用 CQL shell 连接到 Cassandra,您就可以发出以下命令。

    cqlsh> DESCRIBE keyspaces;

    system  used_cars  system_traces

    cqlsh>

代码清单 39

现在我们看到我们的used_car键空间是可用的,我们可以在上面发出USE命令。现在我们在used_cars键空间,如果我们能看到哪些表是可用的,那就太好了。我们可以使用带有tables参数的DESCRIBE命令来实现这一点。

    cqlsh> DESCRIBE tables;

    Keyspace system
    ---------------
    IndexInfo                hints        range_xfers            
    NodeIdInfo               local        schema_columnfamilies
    batchlog                 paxos        schema_columns       
    compaction_history       peer_events  schema_keyspaces    
    compactions_in_progress  peers        schema_triggers      
    sstable_activity

    Keyspace used_cars
    ------------------
    offers_by_brand  test_comments        offers
    test_data        users   

    Keyspace system_traces
    ----------------------
    events  sessions

    cqlsh>

代码清单 40

使用带有tables参数的DESCRIBE命令,我们得到了一个表格列表。前面列表中的大多数表都是系统表;我们通常不会通过 CQL 与他们有任何直接的互动。现在我们在used_cars键空间中有了一个表的列表,看到表中的列和它们的类型将会非常有趣。让我们查一下offers表。

    cqlsh:used_cars> DESCRIBE TABLE offers;

    CREATE TABLE offers (
      username text,
      date timestamp,
      brand text,
      color text,
      mileage int,
      model text,
      price float,
      year int,
      PRIMARY KEY ((username), date)
    ) WITH
      bloom_filter_fp_chance=0.010000 AND
      caching='KEYS_ONLY' AND
      comment='' AND
      dclocal_read_repair_chance=0.100000 AND
      gc_grace_seconds=864000 AND
      index_interval=128 AND
      read_repair_chance=0.000000 AND
      replicate_on_write='true' AND
      populate_io_cache_on_flush='false' AND
      default_time_to_live=0 AND
      speculative_retry='99.0PERCENTILE' AND
      memtable_flush_period_in_ms=0 AND
      compaction={'class': 'SizeTieredCompactionStrategy'} AND
      compression={'sstable_compression': 'LZ4Compressor'};

    cqlsh:used_cars>

代码清单 41

上一个命令最有趣的结果是列名及其类型。如果您想要优化和调整表的行为,您可能会使用这个命令来检查一个设置是否成功地应用于表。让我们检查一下offers表中的当前状态。

    SELECT username, date, brand, model
        FROM offers;

    username  | date                     | brand | model
    ----------+--------------------------+-------+-------
         jdoe | 2014-08-25 11:13:22+0200 |  Audi |    A3
       jsmith | 2014-05-11 01:22:11+0200 |  Ford | Orion
       jsmith | 2014-09-09 11:35:20+0200 |   BMW |  118d
       jsmith | 2014-09-19 11:35:20+0200 |   BMW |  120i
       jsmith | 2014-09-20 17:12:32+0200 |  Audi |    A6
         adoe | 2014-08-26 10:11:10+0200 |    VW |  Golf

代码清单 42

如果我们只看到特定用户的报价,那将会非常有趣。既然用户jsmith的优惠最多,那我们就来关注一下。选择他们的报价将通过WHERE条款和指定jsmith用户名来完成。

    SELECT username, date, brand, model
        FROM offers WHERE username = 'jsmith';

    username  | date                     | brand | model
    ----------+--------------------------+-------+-------
       jsmith | 2014-05-11 01:22:11+0200 |  Ford | Orion
       jsmith | 2014-09-09 11:35:20+0200 |   BMW |  118d
       jsmith | 2014-09-19 11:35:20+0200 |   BMW |  120i
       jsmith | 2014-09-20 17:12:32+0200 |  Audi |    A6

代码清单 43

您可能希望为多个用户筛选offers表,并构建一个查询,返回用户jsmithadoe的所有报价。

    SELECT username, date, brand, model
        FROM offers WHERE username = 'jsmith' OR username = 'adoe';

    Bad Request: line 1:74 missing EOF at 'OR'

代码清单 44

OR运算符在 Cassandra 中不存在。如果我们想从用户中选择多个报价,我们必须使用IN操作符。

    SELECT username, date, brand, model
        FROM offers WHERE username IN ('jsmith', 'adoe');

    username  | date                     | brand | model
    ----------+--------------------------+-------+-------
       jsmith | 2014-05-11 01:22:11+0200 |  Ford | Orion
       jsmith | 2014-09-09 11:35:20+0200 |   BMW |  118d
       jsmith | 2014-09-19 11:35:20+0200 |   BMW |  120i
       jsmith | 2014-09-20 17:12:32+0200 |  Audi |    A6
         adoe | 2014-08-26 10:11:10+0200 |    VW |  Golf

代码清单 45

我们将再次关注jsmith用户的报价,并选择用户在特定日期做出的报价。请记住,要约由要约的用户名和日期信息的组合来标识。如果您回到上一节,您会发现用户名和日期信息构成了一个主键。该查询是:

    SELECT username, date, brand, model FROM offers
        WHERE username= 'jsmith'
            AND date = '2014-09-09 11:35:20+0200';

    username  | date                     | brand | model
    ----------+--------------------------+-------+-------
       jsmith | 2014-09-09 11:35:20+0200 |   BMW |  118d

代码清单 46

如果我们想从表中获取用户的两个特定订单,我们可以再次使用IN运算符,如下例所示。

    SELECT username, date, brand, model
        FROM offers WHERE username= 'jsmith'
    AND date IN ('2014-09-09 11:35:20+0200', '2014-09-19 11:35:20+0200');

    username  | date                     | brand | model
    ----------+--------------------------+-------+-------
       jsmith | 2014-09-09 11:35:20+0200 |   BMW |  118d
       jsmith | 2014-09-19 11:35:20+0200 |   BMW |  120i

代码清单 47

但是列出从用户那里获得报价的所有日期并不是查询数据的最有效方式。想一想;为了得到列表,我们实际上必须知道列表中的每一条数据。一个更面向实践的查询将显示用户自某个时间以来做出的所有报价或者在一段时间内做出的所有报价。让我们看看 9 月份的报价。

    SELECT username, date, brand, model
        FROM offers WHERE username= 'jsmith'
            AND date > '2014-09-01' AND date < '2014-10-01';

    username  | date                     | brand | model
    ----------+--------------------------+-------+-------
       jsmith | 2014-09-09 11:35:20+0200 |   BMW |  118d
       jsmith | 2014-09-19 11:35:20+0200 |   BMW |  120i
       jsmith | 2014-09-20 17:12:32+0200 |  Audi |    A6

代码清单 48

有些人可能会将结束日期设置为“2014-09-30”,但这不会返回当月最后一天的报价,因为默认情况下,CQL 会将小时、分钟和秒的值设置为零。因此,指定第 30 个实际上会忽略午夜之后的所有报价,或者换句话说,忽略当天的所有报价。

最简单的方法是用小于运算符指定下个月的第一天作为结束日期。实际上,前面的查询不是 100%正确的。午夜钟声敲响到第一毫秒结束之间的报价将不包括在结果中。为了涵盖这种情况,只需在大于号后添加等号。比较运算符在 Cassandra 中并不总是可行的。Cassandra 将只允许在能够顺序检索数据的情况下使用比较运算符。

在前面的例子中,来自用户的所有报价都是由 Cassandra 根据日期自动排序的。这就是为什么我们可以使用大于和小于运算符。我们在主键的聚类部分做了这个。现在,就一会儿,让我们后退一小步,尝试与主键的第一部分进行某种比较。为此,我们将创建一个全新的表格,以显示数字的情况:

    CREATE TABLE test_comparison_num_part (
        a int,
        b text,
        PRIMARY KEY (a)
    );

    INSERT INTO test_comparison_num_part (a, b) VALUES (1, 'A1');
    INSERT INTO test_comparison_num_part (a, b) VALUES (2, 'A2');
    INSERT INTO test_comparison_num_part (a, b) VALUES (3, 'A3');
    INSERT INTO test_comparison_num_part (a, b) VALUES (4, 'A4');

    SELECT * FROM test_comparison_num_part WHERE a >= 3;

    Bad Request: Only EQ and IN relation are supported on the partition key (unless you use the token() function)

代码清单 49

正如前面的错误消息所说,主键的第一部分只能用等号和一个IN运算符来检索。这是 Cassandra 在查询数据时的局限性之一。让我们看看主键的其他部分。我们将创建一个新的独立的例子来展示 Cassandra 在这种情况下的行为。

    CREATE TABLE comp_num_clustering (
          a text,
          b int,
          c text,
          PRIMARY KEY (a, b)
    );

    INSERT INTO comp_num_clustering (a, b, c) VALUES ('A', 1, 'A1');
    INSERT INTO comp_num_clustering (a, b, c) VALUES ('A', 2, 'A2');
    INSERT INTO comp_num_clustering (a, b, c) VALUES ('A', 3, 'A3');
    INSERT INTO comp_num_clustering (a, b, c) VALUES ('A', 4, 'A4');

    SELECT * FROM comp_num_clustering WHERE a = 'A' AND b > 2;

    a  | b | c
    ---+---+----
    A  | 3 | A3
    A  | 4 | A4

代码清单 50

之前的结果看起来还不错。我们指定了要读取的行,并提供了范围。Cassandra 可以找到该行,对其进行顺序读取,然后将其返回给我们。这是可能的,因为来自列b的值实际上变成了列。所以,在第A行,我们没有叫b的列,但是我们有叫1:c2:c等的列。,这些列中的值为A1A2等。

我正在回顾 Cassandra 是如何存储表的,因为为了尽可能高效地使用它们,了解 Cassandra 的内部是非常重要的。

图 42:代码清单 50 中表的 Cassandra 存储

范围一切正常,但是如果我们在多行上尝试这种方法会发生什么?如果我们想要所有b大于某个数字的数据,比如说 3,会怎么样?

    SELECT * FROM comp_num_clustering WHERE b >= 3;

    Bad Request: 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

代码清单 51

这似乎有点出乎意料,尤其是对来自关系世界的人来说。数字是有序的,那么为什么我们没有把所有的数据都拿出来呢?上一个表可能有多行。为了找到所有的数据,客户端必须潜在地联系其他节点,等待它们的结果,遍历它收到的每一行,然后从中读取数据。这个简单的查询很容易联系到集群中的每个节点,并导致严重的性能下降。在《Cassandra》中,一切都面向表演。因此,这种查询是不可能的,甚至在某些情况下是不允许的。使这种查询成为可能的一种方法是使用ALLOW FILTERING选项。下面的查询将返回我们所期望的结果。

    SELECT * FROM comp_num_clustering WHERE b >= 3 ALLOW FILTERING;

    a  | b | c
    ---+---+----
    A  | 3 | A3
    A  | 4 | A4

代码清单 52

ALLOW FILTERING将让您运行一些可能需要过滤的查询。极其小心地使用此选项。它会显著降低性能,应该在生产环境中避免使用。过滤的主要缺点是,它通常涉及对其他节点的大量查询,如果在大表上进行,可能会导致非常长的处理时间,这是非常不确定的。在此过程中,让我们尝试一下c列的过滤选项。

    SELECT * FROM comp_num_clustering WHERE c = 'A4' ALLOW FILTERING;

    Bad Request: No indexed columns present in by-columns clause with Equal operator

代码清单 53

指数

过滤选项不允许我们根据c列中的数据搜索表格。错误消息警告我们没有索引列,所以让我们在表中为c列创建一个索引。

    CREATE INDEX ON comp_num_clustering(c);

    SELECT * FROM comp_num_clustering WHERE c = 'A4';

    a  | b | c
    ---+---+----
    A  | 4 | A4

代码清单 54

一旦我们在列上创建了索引,前面的查询就会返回结果。创建索引似乎是向 Cassandra 添加搜索功能的简单方法。理论上,我们可以在任何需要搜索任何类型数据的时候在表上创建索引,但是创建索引有一些潜在的危险的缺点。

Cassandra 存储索引就像存储其他表一样。索引值成为分区键。通常,唯一值越多,索引性能越好。另一方面,如果行数增加,维护该索引将是系统的一个巨大开销,因为每个节点都必须拥有其他节点存储的大部分信息。另一种可能是,我们有某种二进制索引,它有某种真假值。这将是一个具有极少数唯一值的索引。这种索引很快会在几行中获得许多列,并且随着时间的推移变得没有响应,因为它在两个非常大的行中进行搜索。重要的一点是,如果节点出现故障,而我们恢复数据,则必须从头开始重建索引。根据经验,索引的最佳用途是在相对较小的表中,这些表中的查询返回几十或几百个结果,但不会更多。

我们在前面的示例中创建的索引返回了一个自动名称,因为我们只是忘记了为它指定一个名称。这是一个非常常见的错误。每次写入 Cassandra 时都会更新索引,并且有一些技术可以消除对索引的需求,因此如果我们在某个时间点决定删除索引,我们将会遇到麻烦,因为我们必须知道它的名称才能删除它。下面的查询使我们能够找到索引名称以及与识别它相关的其他信息。

    SELECT column_name, index_name, index_type
       FROM system.schema_columns
         WHERE keyspace_name='used_cars'
          AND columnfamily_name='comp_num_clustering';

    column_name  | index_name                | index_type
    -------------+---------------------------+------------
               a |                      null |       null
               b |                      null |       null
               c | comp_num_clustering_c_idx | COMPOSITES

代码清单 55

我们已经在comp_num_clustering表的c列中建立了一个索引,因此我们要查找的索引必须是运行上一个查询后显示的表中的最后一个索引。下面的示例显示了如何移除索引。

    DROP INDEX comp_num_clustering_c_idx;

代码清单 56

以前的索引名称似乎很合理,从名称中我们可以确定这个索引的全部内容。如果您不喜欢 Cassandra 自动命名的方式,或者您有一些必须强制执行的特殊命名约定,那么您可以使用以下命令来命名索引。

    CREATE INDEX comp_num_clustering_c_idx ON comp_num_clustering(c);

代码清单 57

这样,您可以在其他节点或开发环境的初始化脚本中保留索引名称,并避免依赖自动索引命名,这种命名在未来版本的 Cassandra 中可能会改变。

现在您已经知道了 Cassandra 是如何获取数据的,并且不可能总是获取任何列值的数据,让我们看看ORDER BY子句。使用这个子句并不总是可能的。大多数关系数据库允许在几乎所有查询中组合多个列。Cassandra 为了提高性能而限制了这一点。ORDER BY只能在一列上指定,该列必须是主键规范中的第二个键。在comp_num_clustering桌子上,是b柱;在offers中,是date柱。两个可能的订单是ASCDESC。例如,我们将及时反转jsmith用户的报价,并按降序排列。它们默认是升序的,因为 Cassandra 是按列名排序的,所以不需要在offers表上使用ASC

    SELECT username, date, brand, model
        FROM offers WHERE username= 'jsmith' ORDER BY date DESC;

    username  | date                     | brand | model
    ----------+--------------------------+-------+-------
       jsmith | 2014-09-20 17:12:32+0200 |  Audi |    A6
       jsmith | 2014-09-19 11:35:20+0200 |   BMW |  120i
       jsmith | 2014-09-09 11:35:20+0200 |   BMW |  118d
       jsmith | 2014-05-11 01:22:11+0200 |  Ford | Orion

代码清单 58

ORDER BY不常用。更常见的做法是在创建表时指定聚类选项,以便按照系统设计者希望的顺序自动检索数据。大多数情况下,排序是基于某种基于时间的列,如传感器、用户活动、天气或其他一些读数。

收集

Cassandra 中集合的主要用途是存储少量非规范化数据以及基本信息集。集合最常见的用途是存储电子邮件、存储设备读数的属性、存储随事件变化的事件的属性,等等。Cassandra 将集合中的元素数量限制为 65,535 个条目。您可以插入超过此限制的数据,但 Cassandra 只能在该限制内保留和管理数据。有三种基本的收集类型:

  • 地图
  • 设置
  • 目录

这些结构在当今大多数编程语言中都很常见,大多数读者至少会对它们有一个基本的了解。我们将用几个例子展示每个结构,当我们在做的时候,我们会稍微扩展一下我们的数据模型,并利用集合。

我们将从二手车示例中的地图集合开始。到目前为止,我们已经定义了offers表,并指定了一些基本的优惠属性,如日期、品牌、颜色、里程、型号、价格和年份。现在,我们知道不同的汽车有非常不同的配件。一些汽车可能使用不同的燃料,有不同的变速器,或者有不同数量的车门。如果我们在offers表中指定一个map类型的列,我们就可以覆盖所有这些额外的属性,而无需在我们的offers表中添加新的列,如下例所示。

    ALTER TABLE offers ADD equipment map<text, text>;

代码清单 59

现在我们已经有了报价上的equipment地图,让我们来谈谈地图集合的属性。长话短说,映射是一组类型化的键值对。地图中的键总是唯一的。当存储在 Cassandra 中时,映射中的值的一个有趣的属性是键总是被排序的。

有两种方法可以定义映射键值对。一种是只更新地图中的特定键。另一种是一次定义整个地图。这两种方式都可以通过UPDATE命令实现,但是当使用INSERT时,您可以只指定整个地图。让我们看看 T2 会是什么样子。

    INSERT INTO offers (
          username, date, price, brand, model,
          year, mileage, color, equipment)
          VALUES ('adoe', '2014-09-01 12:02:52',
                  12000, 'Audi', 'A4 2.0T', 2008, 99000, 'Black',
               {
            'transmission' : 'automatic',
            'doors' : '4',
            'fuel' : 'petrol'
            });

代码清单 60

定义地图的语法与非常流行的 JSON 格式非常相似。逗号分隔键-值对,键与值之间用冒号分隔。保存的行如下所示。

    SELECT equipment FROM offers WHERE username= 'adoe';

    equipment
    ---------------------------------------------------------------
    {'doors': '4', 'fuel': 'petrol', 'transmission': 'automatic'}

代码清单 61

请注意,存储的映射值是排序的,它们不依赖于我们定义它们的顺序。如果我们决定在不丢弃旧值的情况下更改地图中的值或添加新值,我们可以使用以下UPDATE命令。

    UPDATE offers SET
          equipment['doors'] = '5',
          equipment['turbo'] = 'yes'
      WHERE username = 'adoe'
            AND date='2014-09-01 12:02:52';

代码清单 62

先前的查询将更新地图中的doors键,并将向其添加新的turbo键。这种语法在更新时使用,因为我们不必关心密钥是否被创建。我们只需指定它的值,如果 Cassandra 能够找到它,它就会用新值更新密钥,如果找不到它,它就会定义一个新密钥。让我们看看目前的数据是什么样子。

    SELECT equipment FROM offers WHERE username= 'adoe';

    equipment
    ---------------------------------------------------------------
    {'doors': '5', 'fuel': 'petrol', 'transmission': 'automatic', 'turbo': 'yes'}

代码清单 63

如果我们想重新定义完整的地图,我们只需将equipment设置为等于一个全新的地图定义,如代码清单 60 所示。尽管如此,有时我们还是想从地图中删除特定的键。这是通过DELETE命令完成的。

    DELETE equipment['turbo'] FROM offers
        WHERE username = 'adoe'
            AND date='2014-09-01 12:02:52+0200';

代码清单 64

删除地图中不存在的密钥不成问题。您可以任意多次运行前面的示例,但它只会在第一次运行时删除turbo键。我们还没有涵盖生存时间的概念。就目前而言,请记住,Cassandra 可以在数据已经存在一段指定的时间后自动删除数据。例如,如果使用生存时间参数插入或更新键值,数据的实际生存时间仅指已更改的插入键值对。所有其他数据都有自己的生存时间。稍后我们将更深入地讨论 TTL 概念。目前,知道集合在元素级别而不是行或列级别指定 TTL 就足够了。

我们要深入探讨的下一个收藏类型是布景。集合是唯一值的集合。Cassandra 总是按价值排序。创建一个集合列是通过set关键字,后跟带角度括号的关键元素类型来完成的。器械包最常见的用途之一是保存用户联系数据,如电子邮件地址和电话号码。让我们给users表添加一组电子邮件。

    ALTER TABLE users ADD emails set<text>;

代码清单 65

现在users表可以为每个用户存储更多的电子邮件。以下示例显示了插入新用户。

    INSERT INTO users (
       username, password, first_name, last_name, state, emails)
        VALUES ('jqpublic', 'secret',  'John', 'Public', 'NY',
                 {'j@example.com', 'p@example.com'});

代码清单 66

与地图相比,集合的区别在于没有关键元素,只有值。如果在以前的电子邮件列表中添加重复项,重复的值将不会存储在 Cassandra 中。集合中的值总是像地图中的键一样进行排序。

更新语法基于加号和减号运算符。如果我们想先从集合中删除一个电子邮件地址,然后添加一个新地址,我们可以通过以下查询来完成。

    UPDATE users SET
      emails = emails - {'j@example.com'} WHERE username = 'jqpublic';

    UPDATE users SET
      emails = emails + {'jq@example.com'} WHERE username = 'jqpublic';

代码清单 67

之前的查询会为用户产生以下电子邮件jqpublic

    SELECT username, emails
      FROM users WHERE username = 'jqpublic';

    username  | emails
    ----------+-------------------------------------
    jqpublic  | {'jq@example.com', 'p@example.com'}

代码清单 68

Cassandra 的最后一种收藏类型是列表。列表是非唯一值的类型化集合。列表中的值按其位置排序。映射和设置值总是排序的。列表元素始终保留在我们放置它们的位置。最基本的例子是待办事项清单。我们将在接下来的示例中展示如何使用该列表。在对列表做任何事情之前,我们必须定义一个。

    CREATE TABLE list_example (
          username text,
          to_do list<text>,
          PRIMARY KEY (username)
    );

代码清单 69

地图和集合用大括号表示,但列表用括号表示。除此之外,该列表与前面两种集合类型非常相似。让我们看看在向表中插入数据时如何定义列表。

    INSERT INTO list_example (username, to_do)
        VALUES ('test_user', ['buy milk', 'send mail', 'make a call']);

代码清单 70

前面的INSERT语句为用户生成了一个简单的待办事项列表,其中包含三个元素。让我们看看桌子里有什么。

    SELECT * FROM list_example;

    username   | to_do
    -----------+------------------------------------------
    test_user  | ['buy milk', 'send mail', 'make a call']

代码清单 71

当引用列表中的元素时,我们是根据它们的位置来引用的。列表中的第一个元素的位置为 0,第二个为 1,依此类推。以下示例显示如何更新用户待办事项列表中的第一个元素。

    UPDATE list_example
        SET to_do[0] = 'buy coffee'
            WHERE username = 'test_user';

代码清单 72

在某些情况下,我们希望向列表中添加元素。

    UPDATE list_example
        SET to_do = to_do + ['go to a meeting', 'visit parents']
            WHERE username = 'test_user';

代码清单 73

Cassandra 还可以从列表中删除出现的项目。

    UPDATE list_example
        SET to_do = to_do - ['send mail', 'make a call']
            WHERE username = 'test_user';

代码清单 74

也可以删除列表中具有特定索引的元素。

    DELETE to_do[1]
        FROM list_example
            WHERE username = 'test_user';

代码清单 75

列表中之前的所有更改应该会导致以下结果。

    SELECT * FROM list_example;

    username   | to_do
    -----------+---------------------------------
    test_user  | ['buy coffee', 'visit parents']

代码清单 76

现在我们知道如何使用 Cassandra 的收藏了。收藏在 Cassandra 是一个相对较新的概念。它们在许多日常情况下非常有用。因为它们非常容易使用,所以有时可能会被过度使用。非常小心,不要用可能在表中的数据填充集合列。所有集合类型限于 65,535 个元素;拥有更多肯定是你做错了什么的迹象。

Cassandra 的时间序列数据

了解事物如何随时间变化总是很重要的。通过获得某种现象的历史数据,我们可以对这种现象的内部运作得出结论,然后预测它未来的表现。当谈论历史数据时,它通常由测量值和测量时间的时间戳组成。在统计学中,这些测量被称为数据点。如果数据点是按顺序排列的,并且如果测量值以均匀的时间间隔隔开,那么我们谈论的是时间序列数据。

时间序列数据在人类感兴趣的许多领域中非常重要。它常用于:

  • 性能指标
  • 车队跟踪
  • 传感器数据
  • 系统日志记录
  • 用户活动和行为跟踪
  • 金融市场
  • 科学实验

在当今互联设备越来越智能的世界里,时间序列数据的重要性越来越大,因此产生的数据也越来越多。时间序列数据通常与物联网等术语相关联。每天都有更多的设备产生更多的数据。直到最近,大部分时间序列数据都是在非常专业和昂贵的机器上收集的,这些机器或多或少只支持垂直扩展。没有太多的存储解决方案可以处理大量数据,同时允许水平扩展。作为一种存储技术,Cassandra 的性能非常堪比昂贵且专业的解决方案;它甚至在某些领域超过了他们。

到目前为止,我们已经深入研究了 Cassandra 的一些概念,我们知道 Cassandra 行中的列总是按列名排序的。我们还了解到,在某些情况下,Cassandra 使用逻辑行值来为将要存储的数据形成列名。Cassandra 通常存储预先导出的时间序列数据,并在硬盘上很少搜索的情况下获取这些数据。

我们也在我们的网上二手车市场看到了建模的例子。然而,当与时间序列数据交互时,大多数教程使用气象站数据。我们还将使用气象站来引入时间序列数据,因为它们使用易于理解的概念。气象站通常测量气压和降雨量,但是我们将所有的例子限制在温度上,只是为了简单起见。

基本时间序列

Cassandra 可以在一行中支持多达 20 亿列。如果你有一个每日或每小时的温度测量数据,每个气象站一行就足够了。该表中的每一行都可能标有某种气象站标识。列名是测量时间的时间戳,数据是温度。

让我们将时间序列示例放入一个单独的键空间中。我们将在后面的部分回到二手车。

    CREATE KEYSPACE weather
          WITH replication = {
                         'class': 'SimpleStrategy',
                         'replication_factor' : 1};

代码清单 77

以最简单的形式,基本的气象站数据可以用下表来收集。

    CREATE TABLE temperature (
       weatherstation_id text,
       measurement_time timestamp,
       temperature float,
       PRIMARY KEY (weatherstation_id,  measurement_time)
    );

代码清单 78

如果每年每小时收集一次数据,一个气象站将生成大约 8760 列数据。即使我们在前一个表中添加了额外的数据,在对系统预期寿命的最乐观预测中,列号也不会超过 Cassandra 的行长度限制。将温度读数添加到该表中如下例所示。

    INSERT INTO temperature
        (weatherstation_id, measurement_time, temperature)
            VALUES ('A', '2014-09-12 18:00:00', 26.53);

代码清单 79

请随意在temperature表中添加任意多的读数。所有插入的数据将保存在一个气象站行下。列名是测量时间戳,温度是存储值。让我们看看 Cassandra 是如何在内部存储这些数据的。

图 43:单个气象站数据

在我们的温度示例中,Cassandra 将根据测量时间戳自动对所有插入的数据进行排序。这使得读取数据非常快速有效。当加载数据时,Cassandra 按顺序读取磁盘上的部分,这是大部分效率和速度的来源。

迟早,我们需要对时间序列数据进行某种分析。在我们的例子中,我们将一站一站地获取温度测量数据。从气象站 A 获取所有温度读数将通过以下查询完成。

    SELECT * FROM temperature WHERE weatherstation_id = 'A';

    weatherstation_id  | measurement_time         | temperature
    -------------------+--------------------------+-------------
                     A | 2014-09-12 18:00:00+0200 |       26.53
                     A | 2014-09-12 19:00:00+0200 |       26.68
                     A | 2014-09-12 20:00:00+0200 |       26.98
                     A | 2014-09-12 21:00:00+0200 |       22.11

代码清单 80

如果应用程序长时间收集数据,一行中的数据量可能会变得不切实际,无法及时进行分析。此外,在大多数情况下,Cassandra 主要用于存储和组织数据。几乎任何类型的分析都必须在应用程序级别完成,因为除了COUNT之外,Cassandra 没有任何聚合函数。实际上,大部分分析都不需要你获取气象站记录的所有数据。事实上,大多数分析将针对特定时间段,例如一天、一周、一个月或一年。要将结果限制在指定的时间段内,请使用以下查询。

    SELECT measurement_time, temperature
        FROM temperature
            WHERE weatherstation_id = 'A'
                AND measurement_time >= '2014-09-12 18:00:00'
                AND measurement_time <= '2014-09-12 20:00:00';

    weatherstation_id  | measurement_time         | temperature
    -------------------+--------------------------+-------------
                     A | 2014-09-12 18:00:00+0200 |       26.53
                     A | 2014-09-12 19:00:00+0200 |       26.68
                     A | 2014-09-12 20:00:00+0200 |       26.98

代码清单 81

请注意,如果我们在没有大于或等于运算符的情况下运行前面的查询,并且仅使用大于运算符,则我们不会包括查询条件中指定的时间戳读数。有时这可能会导致错误的分析或意想不到的结果。

我们的气象站示例将处理最基本的时间序列用法。正如您可能已经注意到的,Cassandra 非常容易处理时间序列数据,运行它可以实现水平扩展和合理的运行成本。

逆序时间序列

在某些情况下,应用程序将面向最近的数据,获取历史数据并不那么重要。我们知道 Cassandra 总是对列进行排序。因此,如果我们只从气象站一行中选择几个结果,我们将获得气象站有史以来记录的第一个结果。这在我们只想看到最新结果的类似仪表板的应用程序中不是很有用。此外,旧数据甚至可能与我们无关,我们希望将其从存储中完全删除。

Cassandra 很好地处理了这种情况,因为在定义表时,我们可以借助CLUSTERING指令来影响 Cassandra 在表中存储和排序行的方式。我们在本书的前几节中讨论了集群,我们也看了它的例子。聚类对于创建逆序时间序列非常重要,因为它有助于提高效率,尤其是在数据保留时间较长的情况下。在我们的例子中,我们只需要在temperatures表上定义一个降序聚类。

    CREATE TABLE latest_temperatures (
       weatherstation_id text,
       measurement_time timestamp,
       temperature float,
       PRIMARY KEY (weatherstation_id, measurement_time)
    ) WITH CLUSTERING ORDER BY (measurement_time DESC);

代码清单 82

插入的数据与temperatures表中的数据相同。

    INSERT INTO latest_temperatures
        (weatherstation_id, measurement_time, temperature)
            VALUES ('A', '2014-09-12 18:00:00', 26.53);

代码清单 83

当我们检索这些值时,最新的数据将自动出现在顶部。

    SELECT * FROM latest_temperatures WHERE weatherstation_id = 'A';

    weatherstation_id  | measurement_time         | temperature
    -------------------+--------------------------+-------------
                     A | 2014-09-12 21:00:00+0200 |       22.11
                     A | 2014-09-12 20:00:00+0200 |       26.98
                     A | 2014-09-12 19:00:00+0200 |       26.68
                     A | 2014-09-12 18:00:00+0200 |       26.53

代码清单 84

从该站返回最新温度读数的查询如下所示。

    SELECT *
        FROM latest_temperatures
            WHERE weatherstation_id = 'A'
                LIMIT 1;

    weatherstation_id  | measurement_time         | temperature
    -------------------+--------------------------+-------------
                     A | 2014-09-12 21:00:00+0200 |       22.11

代码清单 85

我们使用LIMIT选项只获取一个结果。如果我们对一个以上的最新读数感兴趣,我们会调整极限。所有其他查询与基本时间序列中的查询相同。我们甚至可以使用基本的时间序列,然后使用ORDER子句获取最新的结果。以下查询使用的是temperature表,而不是latest_temperatures

    SELECT * FROM temperature
        WHERE weatherstation_id = 'A'
            ORDER BY measurement_time DESC
                LIMIT 1;

    weatherstation_id  | measurement_time         | temperature
    -------------------+--------------------------+-------------
                     A | 2014-09-12 21:00:00+0200 |       22.11

代码清单 86

与查询集群排序表相比,前面的查询看起来很麻烦。除了繁琐的查询之外,Cassandra 还需要更长的时间来处理这个查询,因为它将从开始开始,然后遍历记录,直到到达最后一个记录。使用集群排序表,Cassandra 将简单地从行中获取第一个值并返回结果。

| | 提示:使用“按列聚类顺序”来反转行中的列。 |

如果我们只对最新的数据感兴趣,我们就不会想用我们永远不会用到的数据来填满数据库。Cassandra 有一个机制,可以在插入时给数据一个以秒为单位的截止日期。

    INSERT INTO latest_temperatures
        (weatherstation_id, measurement_time, temperature)
            VALUES ('A', '2014-09-12 22:00:00', 26.88) USING TTL 20;

    SELECT * FROM latest_temperatures
        WHERE weatherstation_id = 'A';

    weatherstation_id  | measurement_time         | temperature
    -------------------+--------------------------+-------------
                     A | 2014-09-12 22:00:00+0200 |       26.88
                     A | 2014-09-12 21:00:00+0200 |       22.11

    [wait for 20 seconds or more ...]

    SELECT * FROM latest_temperatures
        WHERE weatherstation_id = 'A';

    weatherstation_id  | measurement_time         | temperature
    -------------------+--------------------------+-------------
                     A | 2014-09-12 21:00:00+0200 |       22.11

代码清单 87

在插入具有定义的生存时间(TTL)的数据后,您只需等待指定的秒数,Cassandra 会用墓碑标记该数据,并在下一个压缩过程中删除它。在经典的关系数据库中,我们必须编写复杂的作业,在后台运行并删除数据。这通常是一个非常消耗资源的过程,因为数据库经常需要重新组织它们的索引等。,因此大多数组织会在夜间或系统负载较轻时进行。

TTL 的定义总是以秒为单位,所以如果我们需要一个以月或年为单位的 TTL,我们必须做一点计算。在大多数情况下,Cassandra 中的数据将从多个应用程序源写入,或者由多个操作员手动更改。实际上,有时我们无法确定某些数据的 TTL 有多长。Cassandra 提供了两个非常有用的功能,用于检查数据必须存在多长时间以及数据何时被写入:ttlwritetime

    INSERT INTO latest_temperatures
        (weatherstation_id, measurement_time, temperature)
            VALUES ('A', '2014-09-12 22:00:00', 26.88) USING TTL 20;

    SELECT weatherstation_id AS w_id,
          temperature AS temp,
          ttl(temperature) AS ttl,
          writetime(temperature) AS wt
        FROM latest_temperatures WHERE weatherstation_id = 'A';

    w_id  | temp  | ttl  | wt
    ------+-------+------+------------------
        A | 26.88 |    7 | 1407072070093000
        A | 22.11 | null | 1407063783604000

代码清单 88

上一个查询的ttl列将有一个整数值,实际上是一个倒计时定时器。以null值作为 TTL 的数据将永远保留在那里。前一个查询的第一行有 7 秒钟,直到 Cassandra 自动删除它。有时写时间也很重要,因为测量时间和数据进入 Cassandra 的时间可能不匹配,当搜索问题时,数据进入 Cassandra 的时间可能非常重要。

无论如何,肯定会有一些时候,我们希望将数据保留的时间比其当前 TTL 指定的时间更长。应用程序的需求可能会改变,我们的应用程序中可能有一些错误的配置,我们插入了带有无效 TTl 的值,或者我们可能会发现我们存储的一些数据突然变得对我们的操作至关重要。我们可以通过更新数据和设置新的 TTL 值来非常容易地改变 TTL。有时候我们会想完全去掉 TTL 这在 CQL 也是可能的。

    INSERT INTO latest_temperatures
        (weatherstation_id, measurement_time, temperature)
            VALUES ('A', '2014-09-12 22:00:00', 26.88)
                USING TTL 20;

    SELECT weatherstation_id as w_id,
          temperature as temp,
          ttl(temperature) as ttl,
          writetime(temperature) as wt
        FROM latest_temperatures
          WHERE weatherstation_id = 'A';

    w_id  | temp  | ttl  | wt
    ------+-------+------+------------------
        A | 26.88 |   11 | 1407073690443000
        A | 22.11 | null | 1407063783604000

    UPDATE latest_temperatures
        USING TTL 60
            SET temperature = 26.88
                WHERE weatherstation_id = 'A'
                    AND measurement_time = '2014-09-12 22:00:00';

    [run the previous select again ...]

    w_id  | temp  | ttl  | wt
    ------+-------+------+------------------
        A | 26.88 |   55 | 1407073766175000
        A | 22.11 | null | 1407063783604000

    UPDATE latest_temperatures
        USING TTL 0
            SET temperature = 26.88
                WHERE weatherstation_id = 'A'
                    AND measurement_time = '2014-09-12 22:00:00';

    [run the previous select again ...]

    w_id  | temp  | ttl  | wt
    ------+-------+------+------------------
        A | 26.88 | null | 1407073798438000
        A | 22.11 | null | 1407063783604000

代码清单 89

| | 提示:如果您希望特定数据自动过期,请使用 TTL。 |

处理大规模时间序列

Cassandra 每行 20 亿列的限制看起来似乎很多,事实也确实如此。大多数日常使用都可以接受这个限制,但是当我们开始以毫秒为单位生成数据时,20 亿并不是很多。事实上,如果某个传感器以毫秒的速度生成数据,Cassandra 中的行将在大约一个月的时间内填满。无论我们做什么应用程序,它都可能需要运行一个多月。

解决这个问题的方法是将数据拆分成更多的行。分割这些数据的最常见方法是为日期和来源标识(在我们的例子中是气象站标识)的每个组合创建一行。数据随后在应用程序级别进行组装。让我们看看如果气象站以毫秒的速度生成数据,我们的温度测量气象站会是什么样子。

    CREATE TABLE temperature_by_day (
        weatherstation_id text,
        date text,
        measurement_time timestamp,
        temperature float,
        PRIMARY KEY ((weatherstation_id, date), measurement_time)
    );

代码清单 90

有了这个模型,气象站监测的每一天都存储在单独的一行中。前面的查询显示分区键是气象站 ID 和数据的组合。使用以下查询插入数据。

    INSERT INTO temperature_by_day
        (weatherstation_id, date, measurement_time, temperature)
            VALUES ('A', '2014-09-12', '2014-09-12 18:00:00', 26.53);

代码清单 91

date列通常在应用程序级别自动生成。应用程序通常花费测量时间,然后确定我们要插入的数据将进入哪个分区。在前面的例子中,我们使用了text列来制作分区。此text列设置为测量时间的日期部分。在分区中插入测量时间没有限制。前一天、后一天或任意一天的时间可以插入到一个分区中,如果直接与 CQL 一起完成的话;这只是一个惯例,使我们的应用程序能够扩展。除了文本日期列,我们还可以使用一个简单的整数列来表示自过去某一天以来的天数,如 1970-01-01 或当前年份中的某一天,如果我们不需要超过一年的数据。这完全取决于具体情况,因系统而异。

当插入数据时,这种分区在应用程序端非常简单,但它使处理变得容易得多。甚至可能会出现在亚毫秒级上收集数据的情况,这在大多数情况下是科学实验特有的。在这种情况下,我们需要制作比白天还要小的分区。

一个好的做法是每小时对数据进行一次分区。我们甚至可能遵循以下经验法则:我们期望的分区越大,分区键的粒度就越小。让我们看看 Cassandra 如何存储之前插入的数据。

图 44:单个气象站数据的日级分区

把数据取出来不会像用基本的例子那样容易。如前图所示,单个气象站的数据行现在按照读数的日期进行了划分。要访问特定日期的数据,我们必须指定日期。

    SELECT weatherstation_id AS w_id, date,
          measurement_time AS t,
          temperature AS temp
        FROM temperature_by_day
            WHERE weatherstation_id = 'A' AND date = '2014-09-12';

    w_id  | date       | t                        | temp
    ------+------------+--------------------------+-------
        A | 2014-09-12 | 2014-09-12 18:00:00+0200 | 26.45
        A | 2014-09-12 | 2014-09-12 18:00:01+0200 | 26.53
        A | 2014-09-12 | 2014-09-12 18:00:02+0200 | 26.68
        A | 2014-09-12 | 2014-09-12 18:00:03+0200 | 26.64
        A | 2014-09-12 | 2014-09-12 18:00:05+0200 | 26.77

代码清单 92

在本例中,列名稍长一些,所以在显示列时使用了别名,以便该表可以适合前面的代码列表。别名语法相对简单。要更改输出中的列名,只需在列名或函数名后指定AS关键字,然后提供获取结果时要在表中显示的名称。与基本示例一样,可以通过指定分区内的边界测量时间(在我们的示例中是一天)来浏览读数。语法与基本用例非常相似。

    SELECT weatherstation_id as w_id,
          date,
          measurement_time as t,
          temperature as temp
        FROM temperature_by_day
            WHERE weatherstation_id = 'A'
                AND date = '2014-09-12'
                AND measurement_time >= '2014-09-12 18:00:01'
                AND measurement_time <= '2014-09-12 18:00:03';

    w_id  | date       | t                        | temp
    ------+------------+--------------------------+-------
        A | 2014-09-12 | 2014-09-12 18:00:01+0200 | 26.53
        A | 2014-09-12 | 2014-09-12 18:00:02+0200 | 26.68
        A | 2014-09-12 | 2014-09-12 18:00:03+0200 | 26.64

代码清单 93

到目前为止的例子实际上已经精确到秒级。我们使用格式化的时间戳使一切更容易理解。CQL shell 提供了一种在毫秒级别插入时间戳的简单方法。我们将要使用的数字不容易被人类理解,所以建议使用某种从秒和毫秒到时间戳的转换器,反之亦然。在网上搜索“在线转换时间戳”之类的东西已经足够容易了。

我们将插入发生在“2014-09-12 18:00:00”的气象站B的毫秒级时间戳测量值——这将是毫秒值为 1410537600000 的时间戳。让我们一个接一个地插入三个读数。

    INSERT INTO temperature_by_day
        (weatherstation_id, date, measurement_time, temperature)
            VALUES ('B', '2014-09-12', 1410537600000, 30.00);

    INSERT INTO temperature_by_day
        (weatherstation_id, date, measurement_time, temperature)
            VALUES ('B', '2014-09-12', 1410537600001, 30.01);

    INSERT INTO temperature_by_day
        (weatherstation_id, date, measurement_time, temperature)
            VALUES ('B', '2014-09-12', 1410537600002, 30.02);

代码清单 94

让我们来看看气象站 b 的“2014-09-12”分区。要访问它,我们需要将SELECTWHERE部分指定到所需的分区。

    SELECT weatherstation_id as w_id, date,
          measurement_time as t, temperature as temp  
        FROM temperature_by_day
            WHERE weatherstation_id = 'B'
                AND date = '2014-09-12';

    w_id  | date       | t                        | temp
    ------+------------+--------------------------+-------
        B | 2014-09-12 | 2014-09-12 18:00:00+0200 |    30
        B | 2014-09-12 | 2014-09-12 18:00:00+0200 | 30.01
        B | 2014-09-12 | 2014-09-12 18:00:00+0200 | 30.02

代码清单 95

按照目前的格式,似乎所有的温度读数都发生在同一时间点。我们知道这样一个事实,我们在读数之间插入了一毫秒间隔的温度,并且我们为每个温度读数增加了少量的温度,以便我们以后能够识别它们。Cassandra 根据测量时间戳自动对读数进行排序,因此从这个角度来看,前面的列表似乎没问题。尽管如此,我们还是希望看到毫秒而不是时间戳,只是为了确保数据实际上以我们要求的时间精度存储。

为此,我们将使用一个小技巧。Cassandra 处理二进制数据非常好;为此,它配备了许多功能,能够将任何种类的 Cassandra 本机类型转换为其二进制表示,并将二进制数据转换为任何基本类型。在 Cassandra 文档中,这些函数通常被称为 blob 函数。您可能还记得,Cassandra 中的时间戳保存为 64 位有符号整数,表示自 1970 年 1 月 1 日午夜以来经过的毫秒数。我们可以将时间戳转换为它的二进制表示,然后将这个二进制表示转换回一个数字,如下例所示。

    SELECT weatherstation_id as w_id, date,
          blobAsBigint(timestampAsBlob(measurement_time)) as t,
          temperature as temp  
        FROM temperature_by_day
            WHERE weatherstation_id = 'B'
                AND date = '2014-09-12';

    w_id  | date       | t             | temp
    ------+------------+---------------+-------
        B | 2014-09-12 | 1410537600000 |    30
        B | 2014-09-12 | 1410537600001 | 30.01
        B | 2014-09-12 | 1410537600002 | 30.02

代码清单 96

在前面的例子中,我们获取了时间戳列,并用timestampAsBlob函数将其转换为二进制值。之后,我们将这个二进制值转换成整数,然后显示在t栏中,所有这些都是借助于blobAsBigint函数。当您不确定列中实际存储的时间戳值时,了解时间戳的毫秒值非常有用。

例如,如果在我们进行SELECT语句或类似语句时,某个结果不在查询范围内,我们会想为什么该行没有显示在结果中。如果我们看毫秒级别,我们可能会看到时间戳实际上比零大几毫秒,因此它不在预期范围内。然后,我们可以调整查询以覆盖省略的结果,或者固定行上的时间戳值。在 Cassandra 中,很多事情可以并且将会接连发生,秒级的默认时间戳精度可能不够。我们将在下一节进一步讨论这个问题。

毫秒是很长的时间

随着信息系统发展得越来越多,事情开始在越来越短的时间内发生。如今,系统比以往任何时候都多,数百个事件在同一毫秒内发生。大多数时候,很难将信息系统的时钟同步到超过一毫秒的精度。因此,大多数设备、组件和系统不会在亚毫秒级上跟踪事件。即使它们有,计时器也可能会有某种偏移,因为时钟可能不会完全同步。

现在假设我们正在使用 Cassandra 来收集来自多个设备的测量值。当我们在做的时候,想象一下有成千上万的信息源向 Cassandra 发送信息,这些信息源产生的信息以毫秒的速度出现。假设在同一毫秒内收到十条信息,会发生什么?

最初的答案可能是,我们需要提高时钟的分辨率,然后简单地使测量机制更加精确,但请记住,亚毫秒级的同步可能会显示出非常不切实际。如果这个解决方案是不可能的,那么我们也许可以在我们保存的时间戳上添加额外的字节,并用一些伪随机值填充这些额外的字节,这样我们就可以在一毫秒内有效地压缩更多的事件。本质上,Cassandratimeuuid类型正是这样做的。事实上,timeuuid类型可以在同一毫秒内压缩如此多的数据,以至于如果我们在 100 年内每秒进行 10 亿次写入,我们只有 50%的机会获得单个timeuuid值冲突。

| | 提示:当同一毫秒内发生多个事件时,请使用 timeuuid 类型。 |

有多种 CQL 函数使我们能够更轻松地使用timeuuid:

  • now()
  • dateOf()
  • unixTimestampOf()
  • minTimeuuid() and maxTimeuuid()

我们将通过例子描述如何使用这些函数。我们的天气测量不是每一毫秒都在变化。为了使示例更加面向实践,我们将使用一个监控大量股票交易的示例表。让我们从定义一个新的键空间开始。

    CREATE KEYSPACE high_volume_trading
          WITH replication = {
                         'class': 'SimpleStrategy',
                         'replication_factor' : 1};

代码清单 97

股票价格通常由交易价格和时间戳组成,如下例所示。

    CREATE TABLE stocks_ticks (
        symbol text,
        day int,
        time timeuuid,
        details text,
        PRIMARY KEY ((symbol, day), time)
    ) WITH CLUSTERING ORDER BY (time DESC);

代码清单 98

现在我们已经设置了带有timeuuid列的表格,让我们看看timeuuid功能是如何使用的。让我们从插入数据开始。timeuuid有 32 个十六进制数字。人类很难生成这些值,因此实际上,我们甚至无法将数据插入表中。要插入当前的timeuuid,我们使用now功能。该功能使用当前时间戳创建timeuuid。如果我们想插入一个股票勾号,我们会按照下面的代码清单来做。

    INSERT INTO stocks_ticks (symbol, day, time, details)
        VALUES ('KTCG', 220, now(), 'BUY:2000');

    INSERT INTO stocks_ticks (symbol, day, time, details)
        VALUES ('KTCG', 220, now(), 'SELL:2001');

    INSERT INTO stocks_ticks (symbol, day, time, details)
        VALUES ('KTCG', 220, now(), 'BUY:2000');

    INSERT INTO stocks_ticks (symbol, day, time, details)
        VALUES ('KTCG', 220, now(), 'BUY:2002');

代码清单 99

要从timeuuid中提取日期,我们使用dateOf功能。

    SELECT time, dateOf(time) FROM stocks_ticks;

    time                                 | dateOf(time)
    -------------------------------------+--------------------------
    24f13590-1f06-11e4-b22e-55bb596001d5 | 2014-08-08 16:13:17+0200
    24f0c060-1f06-11e4-b22e-55bb596001d5 | 2014-08-08 16:13:17+0200
    24f02420-1f06-11e4-b22e-55bb596001d5 | 2014-08-08 16:13:17+0200
    24efaef0-1f06-11e4-b22e-55bb596001d5 | 2014-08-08 16:13:17+0200

代码清单 100

这是一个每毫秒都非常重要的例子,我们可能想从timeuuid.中提取精确的毫秒数,为此,我们将使用unixTimestampOf函数。

    SELECT time, unixTimestampOf(time) FROM stocks_ticks;

    time                                  | unixTimestampOf(time)
    --------------------------------------+-----------------------
    24f13590-1f06-11e4-b22e-55bb596001d5  |        1407507197801
    24f0c060-1f06-11e4-b22e-55bb596001d5  |        1407507197798
    24f02420-1f06-11e4-b22e-55bb596001d5  |        1407507197794
    24efaef0-1f06-11e4-b22e-55bb596001d5  |        1407507197791

代码清单 101

虽然上一个清单中的毫秒值不一样,因为我使用的客户端无法足够快地插入它们,但是很容易发生毫秒部分完全相同的情况。当检索结果时,这意味着在同一毫秒内可能有多个结果。CQL 提供了两个功能,有效地使我们能够获得具有相同毫秒时间戳的所有数据。它们是minTimeuuidmaxTimeuuid,它们返回任何给定时间戳的最小和最大timeuuid值。例如,要在一秒钟内获取所有股票价格,我们将使用以下查询。

    SELECT * FROM stocks_ticks
        WHERE symbol = 'KTCG' AND day = 220
           AND time > minTimeuuid('2014-08-0816:13:17')
           AND time < maxTimeUUID('2014-08-0816:13:18');
    symbol | day | time                                 | details
    -------+-----+--------------------------------------+-----------
      KTCG | 255 | 24f13590-1f06-11e4-b22e-55bb596001d5 |  BUY:2002
      KTCG | 255 | 24f0c060-1f06-11e4-b22e-55bb596001d5 |  BUY:2000
      KTCG | 255 | 24f02420-1f06-11e4-b22e-55bb596001d5 | SELL:2001
      KTCG | 255 | 24efaef0-1f06-11e4-b22e-55bb596001d5 |  BUY:2000

代码清单 102

总结

这是书中最长的一章,我们在里面涉及了很多东西。CQL 是与 Cassandra 互动的最重要的方式,所以我们有很多日常用法要介绍。我们首先对关系世界中的二手车市场进行建模,然后研究 Cassandra 如何处理相同的数据,以及使用 Cassandra 处理关系方法建模的数据的最佳实践是什么。

接下来,我们描述了 Cassandra 是如何成为一个相当特定的存储引擎的,因此我们仔细观察了 Cassandra 是如何物理存储数据的。我们讨论了表及其属性,并介绍了 Cassandra 数据类型。在解释这些概念时,我们介绍了日常使用中的常见情况。我们还提到 Cassandra 的搜索能力非常有限,至少是开箱即用的,所以我们描述了索引以及如何使用它们。

由于 Cassandra 经常用于时间序列数据,我们讨论了生存时间(TTL)的概念,因为在大多数系统中,时间序列数据通常被压缩或分配一个到期时间。我们还研究了在亚毫秒级上处理数据。CQL 和数据建模是任何基于 Cassandra 的应用程序的核心,所以这一章是书中最重要的一章。