Aerospike:入门与实战——基本操作

542 阅读16分钟

在第 2 章中,我们展示了一个入门示例应用的基本样式,并简要讨论了如何建立连接、检索数据和插入数据。本章中,我们将深入介绍在使用 Aerospike 时会用到的所有基本操作,如创建、读取、更新和删除等。这些操作是日常使用的核心内容,掌握后在每天使用 Aerospike 时都会应用到。

我们还将讨论生存时间(TTL),它有助于数据生命周期管理,进一步了解 WritePolicyPolicy,并探索各种数据类型及其使用方法。特别是,你会看到 Aerospike 客户端接口如何在许多情况下为你处理数据类型,从而简化系统间的数据传输。

CRUD 操作

创建(Create)、读取(Read)、更新(Update)和删除(Delete)(简称 CRUD)是与任何数据库(包括 Aerospike)交互的四大基本操作。要开始使用基本功能,你需要了解这些操作。在深入讨论基本操作之前,我们先了解一下 Aerospike 原生支持的数据类型及其可用的数据生命周期选项。

数据类型

Aerospike 中的记录可以包含一个或多个 bin。创建时,bin 被设定为特定的数据类型。了解各种数据类型非常重要,以便根据其能力、限制和数据大小成本做出明智决策。

Aerospike 客户端驱动的内置转换允许你独立于创建数据或输入数据的语言来创建这些数据类型。换句话说,Aerospike 的数据类型与语言无关,这使得日后在数据上应用新的用例变得更加容易。表 3-1 描述了 Aerospike 的数据类型。

数据类型描述
整数 (Integer)64 位数值,可以是有符号或无符号,用于正负整数(例如,42,-12)。
布尔值 (Boolean)表示 truefalse 的值。
双精度 (Double)用于存储小数或包含小数点的数值,适用于 GPS 坐标、价格、温度等(例如,37.4212,-122.0988)。
字符串 (String)以 UTF-8 编码的字符,存储在不透明的字节数组中。可以表示任意字符组合(例如,“Aerospike is fast”、“Tim”或“Pineapple42 themed backpack”)。
映射 (Map)类似于 HashMap 或字典的数据结构,存储键值对关系。映射中的键是唯一的,不允许重复,因此所有与该键关联的值都集中在一起。映射的键必须是标量数据类型(仅限字符串、整数或 blob),值可以是标量或集合类型,可以嵌套。例如: myMapBin: { "Aerospike": "Fast", "Product1234": "OReilly canned beans", 23: True, 255: [1,2,2,3] }
列表 (List)类似数组的数据结构,与映射相似,它是一个可以操作的集合。不同的是列表没有键值关系,且可以有重复的数据。列表中的条目可以是标量数据类型或集合数据类型,也可以嵌套。例如:pageViews: [1691950585, 1691953456, 1691958589]。列表索引从 0 开始,写入时可配置排序与否。列表元素无需类型相同,因此类似 [1691950585, "Tim", 0.42, true] 的列表是有效的。
Blob二进制数据,原始字节。这种数据类型适合存储语言特定结构的原始字节,例如压缩的序列化字典、布隆过滤器或内部数据类型。
GeoJSON几何对象,用于地理空间匹配(例如,找到在另一点 42 英里范围内的点)。
HyperLogLog概率数据类型,用于统计集合或集合并集中的成员数量(例如,对“Computers”和“Furniture”感兴趣的用户数量)。

数据生命周期

如果数据具有有限的有效期并最终需要删除和清理,可以在写入操作时通过设置 WritePolicy 中的到期值来设置记录的生存时间 (TTL)。

TTL 是服务器到期删除记录的秒数,写入记录时会更新其生存时间。服务器配置参数 nsup-period 控制 Aerospike 清理到期记录的频率。默认值为 0,即不允许设置记录的 TTL。如果服务器不允许 TTL 设置而尝试写入具有到期值的记录,则会出现异常:

WritePolicy writePolicy = new WritePolicy();
writePolicy.expiration = 100;
client.put(writePolicy, key, new Bin("mybinname", "mybinvalue"));

Exception com.aerospike.client.AerospikeException: Error 
22,1,0,30000,1000,0,BB9A328EA05C062 127.0.0.1 3101: Operation not allowed 
at this time

如果希望设置正数的 WritePolicy 到期值,需要在 Aerospike 服务器中将 nsup-period 设置为非零值。如果不确定起点,建议将 nsup-period 设置为 120 秒。

为记录设置 TTL 是可选的,每次写入操作时可以指定。也有特殊标志可以防止在写入过程中扩展 TTL。表 3-2 列出了 Aerospike 客户端驱动 WritePolicy 到期值及其效果的摘要。

WritePolicy 到期值效果
−2写入时不更改 TTL。
−1永不过期。
0使用服务器配置的命名空间 default-ttl
大于 0 的值TTL 秒数:记录在 Aerospike 中的生存时间。

某些数据集和应用可能更喜欢通过外部逻辑(例如批处理或协调清理过程)控制数据的删除。如果需要完全控制数据删除,或不希望自动清理,可以将 WritePolicy 到期值设置为 -1。默认服务器配置 nsup-period=0 禁用过期,以确保数据自动清理和删除为选择性行为,除非明确定义,否则不会删除数据。

警告

将客户端的 WritePolicy 到期值设置为 0 表示数据将在服务器参数 default-ttl 定义的秒数后到期。但是,将服务器的 default-ttl 设置为 0 表示“永不过期”。尽管命名相似,但这是不同的配置。可以在服务器配置参考中找到 default-ttl 的文档,客户端 API 文档中也有 WritePolicy 的说明。

创建记录

可以使用 put 客户端方法在 Aerospike 中创建记录。要使用 put 方法需要一个 AerospikeClient 对象,如第 2 章所述。AerospikeClient 对象支持多种不同语言:

Key myKey = new Key("test", "item_set", 10012);
Bin myBin = new Bin("description", "Stylish Couch");

client.put(null, myKey, myBin);

在 Python 中:

key = ('test', 'item_set', 10012)
bin = {'description': 'Stylish Couch'}
client.put(key, bin)

如果获取此记录,你将看到类似:

(gen:1),(exp:0),(bins:(description:Stylish Couch))

此示例未包含 WritePolicyWritePolicy 控制写入的许多方面,例如定义数据到期值、超时时间以及记录已存在时的操作。正如所见,获取的记录显示 exp:0,意味着记录永不过期。如果希望设置 WritePolicy 以指定到期时间,且已配置非零的 nsup-period,则必须在 put 命令中传递该 WritePolicy

WritePolicy writePolicy = new WritePolicy();
writePolicy.expiration = 100;
Key myKey = new Key("test", "item_set", 10012);
Bin myBin = new Bin("description", "Stylish Couch");
client.put(writePolicy, myKey, myBin);

在 Python 中:

write_policy = {'expiration': 100}
key = ('test', 'item_set', 10012)
bin = {'description': 'Stylish Couch'}
client.put(key, bin, policy=write_policy)

参考你的客户端 API 文档了解 WritePolicy 的完整选项列表。

在写入记录时,可能希望使用特定数据类型存储它。Aerospike 客户端驱动简化了这一过程。Aerospike 的驱动将数据类型分配给匹配源的 bin 并插入数据。如果 bin 分配了字典数据类型,例如 Java 中的 HashMap 或 Python 中的字典,客户端会将其写为 Aerospike 的映射数据类型。类似地,Java 或 Python 的列表将被分配为 Aerospike 的列表数据类型。

让我们在 Aerospike 中编写一个映射数据类型的示例并打印出来。首先,在 Java 中定义 HashMap 或在 Python 中定义字典并填充数据。

接着创建一个 Aerospike bin,而不指定数据类型,然后将记录写入 Aerospike。Aerospike 客户端驱动的内置转换会自动匹配数据类型,使这些集合类型的存储独立于创建数据或输入数据的语言。

然后从 Aerospike bin 中检索数据并打印,显示往返后数据保持不变。

// 定义我们的产品为映射
Map<String, Object> product = new HashMap<>();
product.put("name", "stylish couch");
product.put("productid", 123);
product.put("purchasable", true);

// 将映射转换为 Aerospike Bin
Bin productBin = new Bin("product", product);

// 将记录写入 Aerospike
client.put(null, key, productBin);

// 从数据库中读取记录
Record record = client.get(null, key);

// 从记录中获取映射 bin,存储为映射
Map retrievedMap = record.getMap("product");

// 打印 bin 的值
System.out.println(retrievedMap);

// 输出:
// {productid=123, name=stylish couch, purchasable=true}

在 Python 中:

# 定义示例字典
product = {
"name": "stylish couch",
"productid": 123,
"purchasable": True
}

# 定义要写入的 bin
record = {
"product": product
}

# 将记录写入 Aerospike
client.put(key, record)

# 从数据库中读取记录
(key_, metadata, bins) = client.get(key)

# 打印
print(bins)

# 输出:
# {'product': {'name': 'stylish couch', 'productid': 123, 'purchasable': True}}

在第 4 章中,我们还将讨论如何发送操作和表达式,以帮助你在不将数据下载到应用中进行操作和检查的情况下与这些数据类型交互。这可以大大提高应用的效率、简便性和线程安全性。

读取操作

可以使用 get 客户端方法读取记录。策略(Policy)是可选的,可用于控制超时、重试和一致性级别等。还可以指定要检索的 bin,以便仅在需要部分记录时节省资源。

注意

如第 2 章所述,Policy 类是其他策略类(如 WritePolicyBatchPolicy)的超类,因此它们都继承了超时、重试等设置。put 操作使用的是 WritePolicy,而 get 操作仅使用 Policy,而非 ReadPolicy

可以创建包含多个 bin 的记录:

Key myKey = new Key("test", "item_set", 10012);
Bin myBin = new Bin("description", "Stylish Couch");
Bin mySecondBin = new Bin("price", 23.04);
client.put(null, myKey, myBin, mySecondBin);

然后仅检索数据库中存储的一个 bin:

Record myRecord = client.get(null, myKey, "price");
System.out.printf("Got record: %s\n", myRecord);

结果如下:

Got record: (gen:1),(exp:0),(bins:(price:23.04))

读取整个记录将传输更多数据:

Record myRecord = client.get(null, myKey);
System.out.printf("Got record: %s\n", myRecord);

此调用传输了所有 bin,因为没有指定特定的 bin:

Got record: (gen:1),(exp:0),(bins:(description:Stylish Couch),(price:23.04))

当前 Aerospike Python 驱动程序不支持 bin 选择功能,因此需要使用 client.select 或在本地检索所有数据后从记录字典中选择:

(record_key, metadata, bins) = client.select(key, ['price'])

从 Aerospike 读取记录时,应用会从服务器接收记录的元数据。元数据显示了记录的 generation(代数)和到期时间。generation 是服务器上的一个计数器,expiration 显示读取记录的当前 TTL。

可以使用 generation 进行冲突解决,通常会使用 generation-敏感的写入模式进行“检查并设置”(CAS)写入。这是一种在分布式或多线程应用中避免多个进程修改相同数据的方法。generation 计数器表示记录被修改的次数,即使仅修改了 TTL。对于确保记录自上次读取以来未被其他线程或应用修改,这非常有用。

注意

generation 计数器有上限,达到上限后会重置为 1。

CAS 模式利用读取返回的元数据和 WritePolicyGenerationPolicy 设置。当使用简单的 get 语句读取记录时,将整数变量设为记录的 Generation 属性值。然后在应用中本地修改数据,并将 WritePolicy 设置为仅在当前 generation 等于上次读取数据时的值时写入数据。将当前 generation 设为变量中的值,然后写入数据:

// 读取要更新的记录
Record record = client.get(null, key);

// 获取记录的当前 generation
int currentGeneration = record.generation;

// 根据要求修改数据
// ...

// 执行具有期望 generation 的写入操作
WritePolicy writePolicy = new WritePolicy();
writePolicy.generationPolicy = GenerationPolicy.EXPECT_GEN_EQUAL;
writePolicy.generation = currentGeneration; // 设置期望的 generation

// 更新记录
client.put(writePolicy, key, updatedBins);

在 Python 中:

# 读取要更新的记录
(_, metadata, record) = client.get(key)

# 获取记录的当前 generation
current_generation = metadata.generation

# 根据要求修改数据
# ...

# 执行具有期望 generation 的写入操作
write_policy = {'gen': aerospike.POLICY_GEN_EQ}
write_policy['gen_value'] = current_generation # 设置期望的 generation

# 更新记录
client.put(key, updated_bins, policy=write_policy)

如果在数据库读取数据和应用写回修改数据之间,数据被另一个线程或应用修改,则操作将失败,并抛出异常,可以在代码中处理:

Exception com.aerospike.client.AerospikeException: Error 
3,1,0,30000,1000,0,BB9F6DDDD0EC32E 127.0.0.1 3100: Generation error

这样可以防止不同线程或应用意外修改相同数据。

更新操作

更新使用与写入相同的方法,即 put。当对现有记录执行 put 操作时,默认行为是执行 UPSERT,这意味着如果记录不存在,Aerospike 将创建记录;如果记录已存在,Aerospike 会保留记录中的现有数据,除了操作涉及的 bin。如果记录中已有一些不需要修改的 bin,且使用默认的更新策略,这些 bin 将保持不变。

此行为通过调用 Java 中 put 时指定的 WritePolicy.recordExistsAction 来控制。在 Python 客户端中有类似的选择,但不属于特定的枚举。可以参考表 3-3 或 API 文档中“aerospike”部分的“存在策略选项”(Existence Policy Options)。尽管具体实现有所不同,但所有客户端驱动程序中都存在此行为修改器。

JavaPython行为
RecordExistsAction.CREATE_ONLYaerospike.POLICY_EXISTS_CREATE仅在记录不存在时创建记录。如果记录已存在,则抛出异常。
RecordExistsAction.REPLACEaerospike.POLICY_EXISTS_CREATE_OR_REPLACE如果记录存在,则完全替换记录;否则创建记录。
RecordExistsAction.UPDATEaerospike.POLICY_EXISTS_IGNORE如果记录存在,则更新记录;否则创建记录。这是所有客户端驱动程序的默认行为。
RecordExistsAction.REPLACE_ONLYaerospike.POLICY_EXISTS_REPLACE仅在记录存在时完全替换记录。如果记录不存在,则抛出异常。
RecordExistsAction.UPDATE_ONLYaerospike.POLICY_EXISTS_UPDATE仅在记录存在时更新记录。如果记录不存在,则抛出异常。

为说明如何使用这些策略以及异常的示例,以下是使用 CREATE_ONLY 的示例。

首先,将策略设置为仅创建新记录,如果记录已存在则抛出错误。在 Java 中,这是 RecordExistsAction.CREATE_ONLY 设置。在 Python 中,这是 aerospike.POLICY_EXISTS_CREATE。然后写入数据。如果记录不存在,将会正常写入数据且不会发生错误:

// 创建 WritePolicy 并设置为仅在记录不存在时创建记录。
WritePolicy wp = new WritePolicy();
wp.recordExistsAction = RecordExistsAction.CREATE_ONLY;

// 写入数据
client.put(wp, myKey, myBin);

在 Python 中:

# 创建 WritePolicy 并设置为仅在记录不存在时创建记录。
wp = {
'exists': aerospike.POLICY_EXISTS_CREATE
}

# 使用 WritePolicy 写入数据
client.put(my_key, my_bin, policy=wp)

如果使用相同的键运行此示例程序两次,第二次写入时会因为记录已存在而失败,并抛出异常。异常如下所示:

Exception in thread "main" com.aerospike.client.AerospikeException: Error 
5,1,0,30000,1000,0,BB9020011AC4202 127.0.0.1 3000: Key already exists

警告

从表 3-3 可以看到,有一个选项叫 replace。此选项将删除特定键上不由客户端重写的任何现有数据。如果记录上有四个 bin,然后使用 replace 策略写入三个 bin,第四个 bin 将被删除,因为你是在“替换”整个记录。可以将 replace 视为“用此写入替换整个记录”。

删除操作

删除整个记录的主要方法是使用 delete(Java)或 remove(Python)客户端方法:

client.delete(null, myKey);

在 Python 中:

client.remove(my_key);

删除操作可以选择使用 WritePolicy 进行持久删除,这是 Aerospike 企业版的功能,提供了一种标记删除(tombstone delete)的方式。标记删除记录表明曾经存在该记录,防止在意外事件(如断电)中记录被意外恢复。

轻量级操作

尽管可以通过在 bin 中写入一些数据来刷新记录的 TTL,但可以选择更高效的方法。如果只需要刷新记录的 TTL,则无需重写整个记录。同样,如果仅需要检查记录是否存在,也无需获取整个记录。这些功能可以通过客户端驱动程序提供的 touchexists 方法以更轻量的方式实现。

touch 操作是一种轻量级方式,用于更新记录的元数据,如 TTL 或 generation。主要用于读取时刷新记录,以确保记录保持有效。touch 操作仅向服务器发送元数据和 WritePolicy,对客户端来说是非常轻量的操作。

在以下示例中,TTL 被延长了一天(86,400 秒):

WritePolicy writePolicy = new WritePolicy();
writePolicy.expiration = 86400;
client.touch(writePolicy, myKey);

在 Python 中:

client.touch(my_key, 86400);

exists 函数类似,它仅在网络上传输元数据到客户端。用于在客户端应用中获取布尔值 True 或 False,以确定记录是否存在:

client.exists(null, myKey);

在 Python 中:

client.exists(my_key);

注意

需要注意的是,exists 返回的是调用执行时服务器中是否包含该记录。在客户端接收到响应时,由于可能存在多个客户端实例,值可能已不准确。例如,一个线程执行 exists 调用以检查记录是否存在,只有在记录不存在时才创建该记录。API 调用执行时服务器上该记录不存在,因此返回 False。然而,几毫秒后,另一个客户端创建了该记录。原始客户端认为可以安全创建记录,实际上却不能。

因此,exists 很少使用;通常情况下,RecordExistsAction 可以执行相同功能,但带有原子保证。在这种情况下,正确的处理方式是使用 RecordExistsAction.CREATE_ONLY

批量操作

如果需要一次读取或写入多个记录,批量操作可能最合适。要执行批量读取操作,需要创建要读取的键的列表或数组,然后将其传递给 client.get 方法。

在此示例中,首先创建一个要读取的键数组,然后一次性读取它们。注意在 Python 中,该命令不同,为 get_many 而不是简单的 get

Key[] keys = new Key[] {
    new Key("test", "testset", "key1"),
    new Key("test", "testset", "key2"),
    new Key("test", "testset", "key3")
};

Records[] records = client.get(null, keys);

在 Python 中:

keys = [
    ('test', 'testset', 'key1'),
    ('test', 'testset', 'key2'),
    ('test', 'testset', 'key3')
]

records = client.get_many(keys);

执行此操作后,检索到的记录可在 Records 数组中按位置找到。例如,在此示例中,对于 key1,预期 Records[0] 包含其数据。

在 Java 中,如果找不到记录,则位置 0 将为 null,而在 Python 中,列表中将看到 None 条目。执行此操作并找到记录后,可以像在单记录读取操作中一样与这些记录交互。

也有批量写入操作和删除操作的方法,我们将在第 4 章中介绍。

总结

在本章中,你学习了如何执行 CRUD 操作来读取、写入、更新和删除记录。你还探索了如何使用各种数据类型,包括如何创建大多数数据类型(除了 GeoJSON 和 HyperLogLog 数据类型,这些类型需要更深入的探讨,不在本章介绍的范围内)。

你了解了写入操作的行为,以及如何根据记录是否存在来控制行为,还学习了如何设置和更新记录的生存期和代数。在下一章中,你将学习更多高级操作和与 Aerospike 交互的技术。