Amazon DynamoDB用法心得
Amazon DynamoDB 是一款无服务器的 NoSQL 数据库,能够为各种规模的应用提供高性能和高扩展性。本文将详细介绍 DynamoDB 的核心概念,包括主键设计、数据类型、表格定义、增删改查操作、事务处理、全局二级索引 (GSI) 以及表格设计的最佳实践。
1. 主键
主键结构
在 DynamoDB 中,每个表必须定义一个主键(Primary Key)。主键有两种形式:
- 分区键(Partition Key, PK): 一个简单的主键,由单一属性构成,用于确定数据存储在哪个分区。
- 分区键+排序键(Sort Key, SK): 复合主键,由两个属性组成,分区键决定分区,排序键决定分区内的排序。
无论使用哪个形式的主键,必须保证主键的唯一性。
示例:
{
"PK": "USER#12345",
"SK": "USER_INFO"
}
主键设计建议
推荐使用 分区键 + 排序键 的方式设计主键。例如,要存储用户数据,可以将 PK 设为 USER#<user_id>,SK 设为 USER_INFO。
-
PK 的第一部分为常量(如
USER),用于标识数据类别,第二部分为用户 ID。 -
SK 通常用常量或动态值表示数据类型或数据内容。
这样设计既能保证唯一性,又能实现高效查询。
2. 数据类型
DynamoDB 支持以下数据类型:
- 标量类型: String(S)、Number(N)、Binary(B)、Boolean(BOOL)
- 文档类型: Map(M)、List(L)
- 集合类型: String Set(SS)、Number Set(NS)、Binary Set(BS)
括号里的是定义DynamoDB Table的时候实际使用的类型写法。
使用时应注意:
- 灵活性: 文档类型(如 Map 和 List)非常适合存储嵌套数据。
- 索引支持: 只有标量类型可作为分区键或排序键。
示例:
{
"PK": "USER#12345",
"SK": "USER_INFO",
"Name": "Alice",
"Age": 30,
"Preferences": {
"Language": "English",
"TimeZone": "UTC+9"
},
"Tags": ["Developer", "Writer"]
}
3. 表格定义
DynamoDB 表格可以通过CloudFormation,或者是CDK来定义。
CloudFormation定义方式:
Resources:
<asset_name>:
Type: AWS::DynamoDB:Table
Properties:
TableName: <table_name>
AttributeDefinitions:
- AttributeName: <pk_name>
AttributeType: <pk_type>
- AttributeName: <sk_name>
AttributeType: <sk_type>
KeySchema:
- AttributeName: <pk_name>
AttributeType: HASH
- AttributeName: <sk_name>
AttributeType: RANGE
BillingMode: PAY_PER_REQUEST
CDK的定义方式:
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
const table = new dynamodb.Table(this, '<asset_name>', {
tableName: '<table_name>',
partitionKey: { name: '<pk_name>', type: dynamodb.AttributeType.<pk_type> },
sortKey: { name: '<sk_name>', type: dynamodb.AttributeType.STRING.<sk_type> },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
});
参数说明:
<asset_name>: CloudFormation/CDK的资源名称<table_name>: DynamoDB的表名<pk_name>: 分区键的名称<pk_type>: 分区键的类型<sk_name>: 排序键的名称<sk_type>: 排序键的类型
4. 数据库操作
初始化数据库连接API
安装依赖
npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
初始化 DynamoDB 客户端
// dynamodb.util.ts
import { DynamoDB } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
const dbClient = new DynamoDB({});
const marshallOptions = {
// 空白的字符串,二进制数据,集合是否自动转换成null
convertEmptyValues: false, // 默认值:false
// 插入数据时是否删除undefined的值
removeUndefinedValues: false, // 默认值:false
// object类型的数据是否自动转换为map
convertClassInstanceToMap: false, // 默认值:false
};
const unmarshallOptions = {
// DynamoDB检索结果里的Number数据是否转成字符串类型
wrapNumbers: false, // 默认值:false
};
const translateConfig = { marshallOptions, unmarshallOptions };
/**
* 操作数据库时用的API Client
*/
const docClient = DynamoDBDocumentClient.from(dbClient, translateConfig);
export { docClient };
常用操作示例
新增数据
在user_table里新增一个用户。
import { docClient } from './dynamodb.util';
import { PutCommand } from '@aws-sdk/lib-dynamodb';
const command = new PutCommand({
TableName: 'user_table',
Item: {
pk: 'USER#12345',
sk: 'USER_INFO',
name: 'Alice',
age: 30,
},
});
const result = await docClient.send(command);
删除数据
从user_table里删除一个id为 12345 的用户。
import { docClient } from './dynamodb.util';
import { DeleteCommand } from '@aws-sdk/lib-dynamodb';
const command = new DeleteCommand({
TableName: 'user_table',
Key: {
pk: 'USER#12345',
sk: 'USER_INFO',
},
});
const result = await docClient.send(command);
修改数据
把user_table里id为 12345 的用户的name属性修改为 Bob。
import { docClient } from './dynamodb.util';
import { UpdateCommand } from '@aws-sdk/lib-dynamodb';
const command = new UpdateCommand({
TableName: 'user_table',
Key: {
pk: 'USER#12345',
sk: 'USER_INFO',
},
UpdateExpression: 'set #name = :name',
ExpressionAttributeNames: {
'#name': 'name',
},
ExpressionAttributeValues: {
':name': 'Bob',
},
});
const result = await docClient.send(command);
获取单个用户
从user_table里获取id为 12345 的用户。
import { docClient } from './dynamodb.util';
import { GetCommand } from '@aws-sdk/lib-dynamodb';
const command = new GetCommand({
TableName: 'user_table',
Key: {
pk: 'USER#12345',
sk: 'USER_INFO',
},
});
const result = await docClient.send(command);
获取多个用户
从user_table里获取age大于20并小于30的用户。
import { docClient } from './dynamodb.util';
import { ScanCommand } from '@aws-sdk/lib-dynamodb';
const command = new ScanCommand({
TableName: 'user_table',
FilterExpression: '#sk = :sk and #age between :start and :end',
ExpressionAttributeNames: {
'#sk': 'sk',
'#age': 'age',
},
ExpressionAttributeValues: {
':sk': 'USER',
':start': 20,
':end': 30,
},
});
const result = await docClient.send(command);
ScanCommand命令在执行的时候会进行全表扫描,查询效率较慢。更好的查询方式,我会在后面进行讲解。
使用事务同时修改多个用户
DynamoDB 支持 ACID 事务,确保多项操作要么全部成功,要么全部回滚。
常用事务操作
- TransactWriteCommand 同时写入(增删改)多条数据。
- TransactGetCommandInput 同时读取多条数据。
import { docClient } from './dynamodb.util';
import { TransactWriteCommand } from '@aws-sdk/lib-dynamodb';
const command = new TransactWriteCommand({
TransactItems: [
{
Update: {
TableName: 'user_table',
Key: {
pk: 'USER#12345',
sk: 'USER_INFO',
},
UpdateExpression: 'set #name = :name',
ExpressionAttributeNames: {
'#name': 'name',
},
ExpressionAttributeValues: {
':name': 'Bob',
},
},
},
{
Update: {
TableName: 'user_table',
Key: {
pk: 'USER#56789',
sk: 'USER_INFO',
},
UpdateExpression: 'set #name = :name',
ExpressionAttributeNames: {
'#name': 'name',
},
ExpressionAttributeValues: {
':name': 'Lisa',
},
},
},
],
});
const result = await docClient.send(command);
5. 全局二级索引(GSI)
全局二级索引(GSI) 是 DynamoDB 提供的强大工具,我们可以为 GSI 定义与主表不同的分区键和排序键,用于扩展查询能力。
比如,从user_table里获取age大于20并小于30的用户的时候,我们使用了ScanCommand命令来扫描的整张表。这里我们可以用GSI来做优化。做法如下:
首先,我们要添加一个GSI。
Resources:
WorkTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: user_table
AttributeDefinitions:
- AttributeName: pk
AttributeType: S
- AttributeName: sk
AttributeType: S
- AttributeName: age # 新增 age 的属性定义
AttributeType: N # 类型为Number
KeySchema:
- AttributeName: pk
KeyType: HASH
- AttributeName: sk
KeyType: RANGE
BillingMode: PAY_PER_REQUEST
GlobalSecondaryIndexes: # 新增全局二级索引
- IndexName: UserAgeIndex # 索引名称
KeySchema:
- AttributeName: sk # 二级索引的分区键
KeyType: HASH
- AttributeName: age # 二级索引的排序键
KeyType: RANGE
Projection:
ProjectionType: ALL
然后我们就可以这样来进行查询:
import { docClient } from './dynamodb.util';
import { QueryCommand } from '@aws-sdk/lib-dynamodb';
const command = new QueryCommand({
TableName: 'user_table',
IndexName: 'UserAgeIndex',
KeyConditionExpression: '#sk = :sk and #age between :start and :end',
ExpressionAttributeNames: {
'#sk': 'sk',
'#age': 'age',
},
ExpressionAttributeValues: {
':sk': 'USER',
':start': 20,
':end': 30,
},
ScanIndexForward: false, // 根据二级索引的排序键 age 排序: true为升序、false为降序(默认值为true)
});
const result = await docClient.send(command);
6. 表格设计最佳实践
DynamoDB 的设计理念不同于关系型数据库。需要根据访问模式优化表结构。以下是一个示例:
数据结构:
- 部门:部门ID,部门名
- 员工:员工ID,员工名,邮箱
- 出勤记录:日期,开始时间,结束时间,休息时间
示例需求:
- 获取部门列表
- 根据员工ID获取员工信息
- 根据部门ID获取,其所属的员工的员工信息
- 根据员工ID及年月,获取该员工当月的出勤记录
如果用关系型数据库的理念来设计的话,数据库结构大概是这个样子的:
而如果用DynamoDB来设计的话,则应该是这样的:
获取部门列表
import { docClient } from './dynamodb.util';
import { QueryCommand } from '@aws-sdk/lib-dynamodb';
const command = new QueryCommand({
TableName: 'EmployeeTable',
KeyConditionExpression: 'pk = :pk',
ExpressionAttributeValues: {
':pk': 'DEPARTMENT',
},
});
const result = await docClient.send(command);
根据员工ID获取员工信息
import { docClient } from './dynamodb.util';
import { GetCommand } from '@aws-sdk/lib-dynamodb';
const command = new GetCommand({
TableName: 'EmployeeTable',
Key: {
pk: 'Employee#<employee_id>',
sk: 'INFO',
},
});
const result = await docClient.send(command);
根据部门ID获取,其所属的员工的员工信息
import { docClient } from './dynamodb.util';
import { QueryCommandInput } from '@aws-sdk/lib-dynamodb';
const command = new QueryCommandInput({
TableName: 'EmployeeTable',
IndexName: 'DepartmentIndex',
KeyConditionExpression: 'departmentId = :departmentId',
ExpressionAttributeValues: {
':departmentId': '<department_id>',
},
});
const result = await docClient.send(command);
根据员工ID及年月,获取该员工当月的出勤记录
import { docClient } from './dynamodb.util';
import { QueryCommandInput } from '@aws-sdk/lib-dynamodb';
const command = new QueryCommandInput({
TableName: 'EmployeeTable',
KeyConditionExpression: 'pk = :pk and begins_with(sk, :sk)',
ExpressionAttributeValues: {
':pk': 'Employee#<employee_id>',
':sk': 'WORK#202501',
},
});
const result = await docClient.send(command);
最佳实践总结
-
明确查询模式 DynamoDB 是根据如何查询数据而设计的,而不是根据要存储的数据结构。
-
单表设计 将同类数据存储在单个表中,并利用分区键、排序键和 GSI 实现高效查询,而不是存储到多个表中。
-
利用索引 适当使用 GSI 和 LSI(本地二级索引)以确保查询灵活性。
-
最小化扫描 全表扫描性能较差,因此尽可能使用 GSI 或使用特定键的查询。
-
使用事务 利用 DynamoDB 事务确保数据完整性。
通过本文的介绍,希望您能更好地理解和使用 DynamoDB,从而构建出高效、可扩展的应用。