概念
Amazon DynamoDB 是一种全托管 NoSQL 数据库服务,提供快速而可预测的性能,能够实现无缝扩展。
在 DynamoDB 中,表、项目和属性是其核心组件。表是项目的集合,而项目是属性的集合。
DynamoDB 使用主键来唯一标识表中的每个项目,并使用二级索引来提供更具灵活性的查询。
DynamoDB 还提供 Streams 用于捕获 DynamoDB 表中的数据修改事件。
优势
DynamoDB 可以免除操作和扩展分布式数据库的管理工作负担,因而无需担心硬件预置、设置和配置、复制、软件修补或集群扩展等问题。此外,DynamoDB 提供了加密静态,可以消除在保护敏感数据时涉及的操作负担和复杂性。
使用 DynamoDB 可以创建一个数据库表来存储和检索任意量级的数据,并支持任何级别的请求流量。DynamoDB 可以在不停机或性能下降的情况下扩展或缩小表的吞吐容量。使用 AWS Management Console 可以监控资源利用率和性能指标。
DynamoDB 还提供了按需备份功能。它允许我们创建表的完整备份以进行长期保留和存档,从而满足监管合规性需求。DynamoDB 可以启用时间点恢复。时间点恢复有助于保护数据表免遭意外写入或删除操作。使用时间点恢复,可以将该表还原到最近 35 天中的任何时间点。DynamoDB 还能自动从表中删除过期项目,以减少存储使用量。
核心组件
以下是基本的 DynamoDB 组件:
-
表(Table):类似于其他数据库系统,DynamoDB 将数据存储在表中。表是数据的集合。例如,下文中展示的名为 People 的示例表,该表可用于存储任何有关人的数据。
-
项目(Item):每个表包含零个或更多个项目。项目是一组属性,具有不同于所有其他项目的唯一标识。在 People 表中,每个项目表示一位人员。DynamoDB 对表中可存储的项目数没有限制。
-
属性(Attribute):每个项目包含一个或多个属性。属性是基础的数据元素,无需进一步分解。例如,People 表中的一个项目包含名为 PersonID、LastName、FirstName 等的属性。DynamoDB 中的属性在很多方面都类似于其他数据库系统中的字段或列。
案例:People 表
例如,以下是 People 表中的不同项目,每个项目都有自己的属性:
{
"PersonID": 101,
"LastName": "Smith",
"FirstName": "Fred",
"Phone": "555-4321"
}
{
"PersonID": 102,
"LastName": "Jones",
"FirstName": "Mary",
"Address": {
"Street": "123 Main",
"City": "Anytown",
"State": "OH",
"ZIPCode": 12345
}
}
{
"PersonID": 103,
"LastName": "Stephens",
"FirstName": "Howard",
"Address": {
"Street": "123 Main",
"City": "London",
"PostalCode": "ER3 5K8"
},
"FavoriteColor": "Blue"
}
从这个 People 表中,我们可以得出以下注意事项:
-
表中的每个项目都有一个唯一的标识符或主键,用于将项目与表中的所有其他内容区分开来。在 People 表中,主键就是 PersonID 属性。
-
与关系型数据库不同,People 表本身是无架构的,这表示除了主键之外,其余属性及其数据类型都不需要预先定义,每个项目都能拥有其自己的独特属性。
-
大多数属性是标量类型的,这表示它们只能具有一个值。常见的标量类型有字符串和数字。
-
某些项目具有嵌套属性(例如示例中的 Address 属性)。DynamoDB 支持高达 32 级深度的嵌套属性。
案例:Music 表
下面,再来看另一个 Music 表里的不同项目:
{
"Artist": "No One You Know",
"SongTitle": "My Dog Spot",
"AlbumTitle": "Hey Now",
"Price": 1.98,
"Genre": "Country",
"CriticRating": 8.4
}
{
"Artist": "No One You Know",
"SongTitle": "Somewhere Down The Road",
"AlbumTitle": "Somewhat Famous",
"Genre": "Country",
"CriticRating": 8.4,
"Year": 1984
}
{
"Artist": "The Acme Band",
"SongTitle": "Still in Love",
"AlbumTitle": "The Buck Starts Here",
"Price": 2.47,
"Genre": "Rock",
"PromotionInfo": {
"RadioStationsPlaying": {
"KHCR",
"KQBX",
"WTNR",
"WJJH"
},
"TourDates": {
"Seattle": "20150622",
"Cleveland": "20150630"
},
"Rotation": "Heavy"
}
}
{
"Artist": "The Acme Band",
"SongTitle": "Look Out, World",
"AlbumTitle": "The Buck Starts Here",
"Price": 0.99,
"Genre": "Rock"
}
需要注意的是,与 People 表不同,Music 表的主键包含两个属性(Artist 和 SongTitle)。表中的每个项目都必须同时具有这两个属性。Artist 和 SongTitle 的属性组合用于将表中的每个项目与所有其他内容区分开来。
主键
创建表时,除表名称外,您还必须指定表的主键。主键唯一标识表中的每个项目,因此,任意两个项目的主键都不相同。
DynamoDB 支持两种不同类型的主键:
-
分区键:DynamoDB 使用分区键的值作为内部散列函数的输入。来自散列函数的输出决定了项目将存储到的分区(DynamoDB 内部的物理存储)。
- 分区键也称为其哈希属性。哈希属性一词源自 DynamoDB 中使用的内部哈希函数,以基于数据项目的分区键值实现跨多个分区的数据项目平均分布。
-
排序键:具有相同分区键值的所有项目按排序键值的排序顺序存储在一起。
- 排序键也称为其范围属性。范围属性一词源自 DynamoDB 存储项目的方式,它按照排序键值有序地将具有相同分区键的项目存储在互相紧邻的物理位置。
因此,DynamoDB 支持以下两种构成主键的方式:
-
简单主键(只有分区键):此类型的键只由一个属性组成,这个属性就是分区键。
-
在只有分区键的表中,任何两个项目都不能有相同的分区键值。
-
上文中的 People 表就是只有分区键 PersonID 的。倘若想要访问 People 表中的任何项目,只需提供该项目的 PersonID 值就可以了。
-
-
复合主键(同时具有分区键和排序键):此类型的键由两个属性组成,第一个属性是分区键,第二个属性是排序键。
-
在同时具有分区键和排序键的表中,多个项目可能具有相同的分区键值。但是,这些项目必须具有不同的排序键值。
-
上文中的 Music 表就是具有复合主键(Artist 和 SongTitle)的示例。倘若想要访问 Music 表中的任何项目,需要提供该项目的 Artist 和 SongTitle 值。
-
在查询数据时,复合主键可以提供额外的灵活性。例如,如果仅提供 Artist 的值,则 DynamoDB 将检索该艺术家的所有歌曲。倘若需要仅检索特定艺术家的一部分歌曲,则可以提供一个 Artist 值和一系列 SongTitle 值来实现这个需求。
-
需要注意的是,每个主键属性必须为标量,即只能具有一个值。换句话说,主键属性唯一允许的数据类型是字符串(S)、数字(N)以及二进制(B)。当然,对于其他非主键属性则没有任何此类限制。
二级索引
在 DynamoDB 中,我们可以在一张表上创建一个或多个二级索引。利用二级索引,除了可以对主键进行查询外,还可使用替代键查询表中的数据,方法与从表中读取数据大体相同。
DynamoDB 支持两种索引:
-
全局二级索引:分区键和排序键可与基表中的这些键不同的索引。
- 全局二级索引之所以称为“全局”,是因为索引上的查询可跨过所有分区,覆盖基表的所有数据。全局二级索引存储在其远离基表的分区空间中,并且独立于基表进行扩展。
-
本地二级索引:分区键与基表相同但排序键不同的索引。
- 本地二级索引之所以称为“本地”,是因为索引的每个分区的范围都限定为具有相同分区键值的基表分区。
DynamoDB 中的默认配额是每个表具有 20 个全局二级索引和 5 个本地二级索引。
在前文中的 Music 表示例中,我们可以按照 Artist(分区键)或按照 Artist 和 SongTitle(分区键和排序键)两种方式查询数据项。但如果我们还想要按照 Genre 和 AlbumTitle 查询数据又该怎么办呢?
若要达到此目的,我们便可以在 Genre 和 AlbumTitle 上创建一个二级索引,然后通过与查询 Music 表相同的方式查询该索引。
下面这个表格显示了上文中展示过的一张 Music 表,以及为该表建立的一个名为 GenreAlbumTitle 的二级索引。在这个二级索引中,Genre 是分区键,AlbumTitle 是排序键。
- Music 表
{
"Artist": "No One You Know",
"SongTitle": "My Dog Spot",
"AlbumTitle": "Hey Now",
"Price": 1.98,
"Genre": "Country",
"CriticRating": 8.4
}
- GenreAlbumTitle 索引
{
"Genre": "Country",
"AlbumTitle": "Hey Now",
"Artist": "No One You Know",
"SongTitle": "My Dog Spot"
}
关于 GenreAlbumTitle 索引,需要注意的是:
-
每个索引属于一个表(称为索引的基表)。在上述示例中,Music 是 GenreAlbumTitle 索引的基表。
-
DynamoDB 将自动维护索引。当我们添加、更新或删除基表中的某个项目时,DynamoDB 会自动添加、更新或删除属于该表的任何索引中的对应项目。
-
当我们创建索引时,可以指定哪些属性将从基表复制或投影到索引。DynamoDB 至少会将主键属性从基表投影到索引中。对于 GenreAlbumTitle 也是如此,只不过示例中 Music 表只有主键属性被投影到索引中了,没有额外指定其他属性。
有了 GenreAlbumTitle 索引,我们就可以借助这个索引来查找某个特定流派的所有专辑(例如,所有 Genre 的值为 Rock 的专辑)。我们还可以查询索引以查找特定流派中具有特定专辑名称的所有专辑(例如,AlbumTitle 以字母 H 开头的所有 Genre 的值为 Country 的专辑)。
DynamoDB Streams
DynamoDB Streams 是一项可选功能,用于捕获 DynamoDB 表中的数据修改事件。有关这些事件的数据将以事件发生的顺序近乎实时地出现在流中。
每个事件由一条流记录来表示。如果启用了某张表的流,则每当以下事件之一发生时,DynamoDB 都会写入一条流记录:
-
向表中添加了新项目:流将捕获整个项目的映像,包括其所有属性。
-
更新了项目:流将捕获项目中已修改的任何属性的“之前”和“之后”映像。
-
从表中删除了项目:流将在整个项目被删除前捕获其映像。
每条流记录还包含表的名称、事件时间戳和其他元数据。流记录具有 24 个小时的生命周期,在此时间过后,它们将从流中自动删除。
使用 AWS SDK 操作数据库(以 Java 为例)
创建数据库
public static String createTable(DynamoDbClient ddb, String tableName, String key) {
DynamoDbWaiter dbWaiter = ddb.waiter();
CreateTableRequest request = CreateTableRequest.builder()
.attributeDefinitions(AttributeDefinition.builder()
.attributeName(key)
.attributeType(ScalarAttributeType.S)
.build())
.keySchema(KeySchemaElement.builder()
.attributeName(key)
.keyType(KeyType.HASH)
.build())
.provisionedThroughput(ProvisionedThroughput.builder()
.readCapacityUnits(new Long(10))
.writeCapacityUnits(new Long(10))
.build())
.tableName(tableName)
.build();
String newTable ="";
try {
CreateTableResponse response = ddb.createTable(request);
DescribeTableRequest tableRequest = DescribeTableRequest.builder()
.tableName(tableName)
.build();
// Wait until the Amazon DynamoDB table is created.
WaiterResponse<DescribeTableResponse> waiterResponse = dbWaiter.waitUntilTableExists(tableRequest);
waiterResponse.matched().response().ifPresent(System.out::println);
newTable = response.tableDescription().tableName();
return newTable;
} catch (DynamoDbException e) {
System.err.println(e.getMessage());
System.exit(1);
}
return "";
}
写入项目
public static void putItemInTable(DynamoDbClient ddb,
String tableName,
String key,
String keyVal,
String albumTitle,
String albumTitleValue,
String awards,
String awardVal,
String songTitle,
String songTitleVal){
HashMap<String,AttributeValue> itemValues = new HashMap<>();
itemValues.put(key, AttributeValue.builder().s(keyVal).build());
itemValues.put(songTitle, AttributeValue.builder().s(songTitleVal).build());
itemValues.put(albumTitle, AttributeValue.builder().s(albumTitleValue).build());
itemValues.put(awards, AttributeValue.builder().s(awardVal).build());
PutItemRequest request = PutItemRequest.builder()
.tableName(tableName)
.item(itemValues)
.build();
try {
PutItemResponse response = ddb.putItem(request);
System.out.println(tableName +" was successfully updated. The request id is "+response.responseMetadata().requestId());
} catch (ResourceNotFoundException e) {
System.err.format("Error: The Amazon DynamoDB table \"%s\" can't be found.\n", tableName);
System.err.println("Be sure that it exists and that you've typed its name correctly!");
System.exit(1);
} catch (DynamoDbException e) {
System.err.println(e.getMessage());
System.exit(1);
}
}
读取项目
public static void getDynamoDBItem(DynamoDbClient ddb,String tableName,String key,String keyVal ) {
HashMap<String,AttributeValue> keyToGet = new HashMap<>();
keyToGet.put(key, AttributeValue.builder()
.s(keyVal)
.build());
GetItemRequest request = GetItemRequest.builder()
.key(keyToGet)
.tableName(tableName)
.build();
try {
// If there is no matching item, GetItem does not return any data.
Map<String,AttributeValue> returnedItem = ddb.getItem(request).item();
if (returnedItem.isEmpty())
System.out.format("No item found with the key %s!\n", key);
else {
Set<String> keys = returnedItem.keySet();
System.out.println("Amazon DynamoDB table attributes: \n");
for (String key1 : keys) {
System.out.format("%s: %s\n", key1, returnedItem.get(key1).toString());
}
}
} catch (DynamoDbException e) {
System.err.println(e.getMessage());
System.exit(1);
}
}
更新项目
public static void updateTableItem(DynamoDbClient ddb,
String tableName,
String key,
String keyVal,
String name,
String updateVal){
HashMap<String,AttributeValue> itemKey = new HashMap<>();
itemKey.put(key, AttributeValue.builder()
.s(keyVal)
.build());
HashMap<String,AttributeValueUpdate> updatedValues = new HashMap<>();
updatedValues.put(name, AttributeValueUpdate.builder()
.value(AttributeValue.builder().s(updateVal).build())
.action(AttributeAction.PUT)
.build());
UpdateItemRequest request = UpdateItemRequest.builder()
.tableName(tableName)
.key(itemKey)
.attributeUpdates(updatedValues)
.build();
try {
ddb.updateItem(request);
} catch (ResourceNotFoundException e) {
System.err.println(e.getMessage());
System.exit(1);
} catch (DynamoDbException e) {
System.err.println(e.getMessage());
System.exit(1);
}
System.out.println("The Amazon DynamoDB table was updated!");
}
删除项目
public static void deleteDynamoDBItem(DynamoDbClient ddb, String tableName, String key, String keyVal) {
HashMap<String,AttributeValue> keyToGet = new HashMap<>();
keyToGet.put(key, AttributeValue.builder()
.s(keyVal)
.build());
DeleteItemRequest deleteReq = DeleteItemRequest.builder()
.tableName(tableName)
.key(keyToGet)
.build();
try {
ddb.deleteItem(deleteReq);
} catch (DynamoDbException e) {
System.err.println(e.getMessage());
System.exit(1);
}
}
查询表
public static int queryTable(DynamoDbClient ddb, String tableName, String partitionKeyName, String partitionKeyVal, String partitionAlias) {
// Set up an alias for the partition key name in case it's a reserved word.
HashMap<String,String> attrNameAlias = new HashMap<String,String>();
attrNameAlias.put(partitionAlias, partitionKeyName);
// Set up mapping of the partition name with the value.
HashMap<String, AttributeValue> attrValues = new HashMap<>();
attrValues.put(":"+partitionKeyName, AttributeValue.builder()
.s(partitionKeyVal)
.build());
QueryRequest queryReq = QueryRequest.builder()
.tableName(tableName)
.keyConditionExpression(partitionAlias + " = :" + partitionKeyName)
.expressionAttributeNames(attrNameAlias)
.expressionAttributeValues(attrValues)
.build();
try {
QueryResponse response = ddb.query(queryReq);
return response.count();
} catch (DynamoDbException e) {
System.err.println(e.getMessage());
System.exit(1);
}
return -1;
}
扫描表
public static void scanItems( DynamoDbClient ddb,String tableName ) {
try {
ScanRequest scanRequest = ScanRequest.builder()
.tableName(tableName)
.build();
ScanResponse response = ddb.scan(scanRequest);
for (Map<String, AttributeValue> item : response.items()) {
Set<String> keys = item.keySet();
for (String key : keys) {
System.out.println ("The key name is "+key +"\n" );
System.out.println("The value is "+item.get(key).s());
}
}
} catch (DynamoDbException e) {
e.printStackTrace();
System.exit(1);
}
}