Contents

Cassandra 二级索引

1. 概述

在本教程中,我们将讨论如何在Apache Cassandra 中使用二级索引。

我们将了解数据在数据库中的分布情况并探索所有索引类型。最后,我们将讨论使用二级索引的一些最佳实践和建议。

2. Cassandra 架构

Cassandra 是一个 NoSQL 分布式数据库,具有完全去中心化的通信模型。

它由具有相同职责的多个节点组成,提供高可用性。它可以在任何云提供商和本地运行,使其与云无关。

我们还可以跨多个云平台同时部署单个 Cassandra 集群。它最适合 OLTP(在线事务处理)查询,其中响应速度至关重要,查询很少更改。

2.1. 主键

主键是唯一标识数据记录的最重要的数据建模选择它由至少一个分区键和零个或多个集群列组成。

分区键定义了我们如何在集群中拆分数据。群集列对磁盘上的数据进行排序以启用快速读取操作。 让我们看一个例子:

CREATE TABLE company (
    company_name text,
    employee_name text,
    employee_email text,
    employee_age int,
    PRIMARY KEY ((company_name), employee_email)
);

在这里,我们将company_name定义为用于在节点之间均匀分布表数据的分区键。接下来,由于我们将employee_email指定为集群列,Cassandra 使用它来保持每个节点上的数据按升序排列,以便有效地检索行。

2.2. 集群拓扑

Cassandra 提供与可用节点数量成正比的线性可扩展性和性能

节点被放置在一个环中,形成一个数据中心,通过连接多个地理分布的数据中心,我们创建了一个集群。

Cassandra 自动对数据进行分区,无需人工干预,从而为大数据做好准备。

接下来,让我们看看 Cassandra 如何通过company_name对我们的表进行分区:

/uploads/cassandra_secondary_indexes/1.png

正如我们所见,company表使用分区键company_name分成多个分区,并分布在各个节点上。我们可以注意到 Cassandra 将具有相同company_name值的行分组并将它们存储在磁盘上的相同物理分区上。因此,我们可以以最低的 I/O 成本读取给定公司的所有数据。

此外,我们可以通过定义复制因子来跨数据中心复制数据。N 的复制因子会将每个数据行存储在集群中的 N 个不同节点上。

我们可以在数据中心级别而不是集群级别指定副本数量。因此,我们可以拥有多个数据中心的集群,每个数据中心具有不同的复制因子。

3. 查询非主键

让我们以之前定义的company表为例,尝试按employee_age进行搜索:

SELECT * FROM company WHERE employee_age = 30;
InvalidRequest: Error from server: code=2200 [Invalid query] message="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"

我们收到此错误消息是因为我们无法查询不属于主键的列,除非我们使用ALLOW FILTERING子句。

但是,即使我们技术上可以,我们也不应该在生产中使用它,因为ALLOW FILTERING既昂贵又耗时。这是因为,在后台,它会在集群中的所有节点上启动全表扫描以获取结果,这会对性能产生负面影响。

但是,我们可以使用它的一个可接受的用例是当我们需要对单个分区进行大量过滤时。在这种情况下,Cassandra 仍然执行表扫描,但我们可以将其限制为单个节点:

SELECT * FROM company WHERE company_name = 'company_a' AND employee_age = 30 ALLOW FILTERING;

因为我们添加了company_name聚类列作为条件,Cassandra 使用它来识别保存所有公司数据的节点。因此,它只对特定节点上的表数据执行表扫描。

4. 二级索引

Cassandra 中的二级索引解决了查询不属于主键的列的需求。

当我们插入数据时,Cassandra 使用一个名为commitlog的仅追加文件来存储更改,因此写入速度很快。同时,数据被写入内存中的键/列值缓存,称为Memtable。Cassandra 会定期以不可变SSTable的形式将Memtable刷新到磁盘。

接下来,让我们看看 Cassandra 中三种不同的索引方式,并讨论它们的优缺点。

4.1. 常规二级索引 (2i)

常规二级索引是我们可以定义的用于对非主键列执行查询的最基本索引。

让我们在employee_age列上定义一个二级索引:

CREATE INDEX IF NOT EXISTS ON company (employee_age);

有了这些,我们现在可以通过employee_age运行查询而不会出现任何错误:

SELECT * FROM company WHERE employee_age = 30; 
## company_name  | employee_email    | employee_age | employee_name 
    company_A | [[email protected]](/cdn_cgi/l/email_protection) |           30 |     employee_1

当我们建立索引时,Cassandra 会在后台创建一个隐藏表来存储索引数据:

CREATE TABLE company_by_employee_age_idx ( 
    employee_age int,
    company_name text,
    employee_email text,
    PRIMARY KEY ((employee_age), company_name, employee_email) 
);

与常规表不同,Cassandra 不使用集群范围的分区器分发隐藏索引表。索引数据与源数据位于同一节点上。

因此,在使用二级索引执行搜索查询时,Cassandra 从每个节点读取索引数据并收集所有结果。如果我们的集群有很多节点,这可能会导致数据传输增加和高延迟。

我们可能会问自己,为什么 Cassandra 不根据主键跨节点对索引表进行分区。答案是,将索引数据与源数据一起存储可以减少延迟。此外,由于索引更新是在本地而不是通过网络执行的,因此不会因连接问题而丢失更新操作的风险。此外,如果索引列数据分布不均,Cassandra 会避免创建宽分区。

当我们向附加了二级索引的表中插入数据时,Cassandra 会同时写入索引和基础Memtable。此外,两者都同时刷新到SSTable中。因此,索引数据将具有与源数据不同的生命周期。

当我们根据二级索引读取数据时,Cassandra 首先检索索引中所有匹配行的主键,然后使用它们从源表中获取所有数据。

4.2. SSTable 附加二级索引 (SASI)

SASI 引入了将SSTable生命周期绑定到索引的新思想。执行内存索引,然后使用SSTable将索引刷新到磁盘可减少磁盘使用率并节省 CPU 周期。

让我们看看我们如何定义一个 SASI 索引:

CREATE CUSTOM INDEX IF NOT EXISTS company_by_employee_age ON company (employee_age) USING 'org.apache.cassandra.index.sasi.SASIIndex';

SASI 的优点是标记化文本搜索、快速范围扫描和内存索引。另一方面,一个缺点是它会生成大的索引文件,尤其是在启用文本标记化时。

最后,我们应该注意到DataStax Enterprise (DSE) 中的 SASI 索引是实验性的。DataStax 不支持用于生产的 SASI 索引。

4.3. 存储附加索引 (SAI)

Storage-Attached Indexing 是一种高度可扩展的数据索引机制,可用于 DataStax Astra 和 DataStax Enterprise 数据库。我们可以在任何列上定义一个或多个 SAI 索引,然后使用范围查询(仅限数字)、CONTAIN的语义和过滤查询。

SAI 为每一列存储单独的索引文件,并包含一个指向SSTable中源数据偏移量的指针。一旦我们将数据插入索引列,它将首先写入内存。每当 Cassandra 将数据从内存刷新到磁盘时,它都会将索引与数据表一起写入。

这种方法通过减少写入的开销,在 2i 上将吞吐量提高了 43%,延迟提高了 230%。与 SASI 和 2i 相比,它用于索引的磁盘空间要少得多,故障点更少,并且具有更简化的架构。

让我们使用 SAI 定义我们的索引:

CREATE CUSTOM INDEX ON company (employee_age) USING 'StorageAttachedIndex' WITH OPTIONS = {'case_sensitive': false, 'normalize': false};

normalize 选项将特殊字符转换为其基本字符。例如,我们可以将德语字符ö规范化为常规 o,从而无需键入特殊字符即可进行查询匹配。因此,例如,我们可以通过简单地使用“schon”作为条件来搜索术语“schön”。

4.4. 最佳实践

首先,当我们在查询中使用二级索引时,建议添加分区键作为条件。因此,我们可以将读取操作减少到单个节点(根据一致性级别加上副本):

SELECT * FROM company WHERE employee_age = 30 AND company_name = "company_A";

**其次,我们可以将查询限制为分区键列表,**并限制获取结果所涉及的节点数:

SELECT * FROM company WHERE employee_age = 30 AND company_name IN ("company_A", "company_B", "company_C");

第三,如果我们只需要结果的一个子集,我们可以为查询添加一个限制。这也减少了读取路径中涉及的节点数量:

SELECT * FROM company WHERE employee_age = 30 LIMIT 10;

此外,我们必须避免在基数非常低的列(性别、真/假列等)上定义二级索引,因为它们会产生影响性能的非常宽的分区。

同样,具有高基数的列(社会保险号、电子邮件等)将导致索引具有非常精细的分区,在最坏的情况下,这将迫使集群协调器命中所有主副本。

最后,**我们必须避免在频繁更新的列上使用二级索引。**这背后的基本原理是 Cassandra 使用不可变数据结构,频繁更新会增加磁盘上的写入操作次数。