log
码中赤兔

Amazon DynamoDB用法心得

发布于 2025年1月17日
更新于 2025年1月30日
34 分钟阅读
AWS

Amazon DynamoDB 是一款无服务器的 NoSQL 数据库,能够为各种规模的应用提供高性能和高扩展性。本文将详细介绍 DynamoDB 的核心概念,包括主键设计、数据类型、表格定义、增删改查操作、事务处理、全局二级索引 (GSI) 以及表格设计的最佳实践。


1. 主键

主键结构

在 DynamoDB 中,每个表必须定义一个主键(Primary Key)。主键有两种形式:

  1. 分区键(Partition Key, PK): 一个简单的主键,由单一属性构成,用于确定数据存储在哪个分区。
  2. 分区键+排序键(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里删除一个id12345 的用户。

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_tableid12345 的用户的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里获取id12345 的用户。

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及年月,获取该员工当月的出勤记录

如果用关系型数据库的理念来设计的话,数据库结构大概是这个样子的:

rds

而如果用DynamoDB来设计的话,则应该是这样的:

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);

最佳实践总结

  1. 明确查询模式 DynamoDB 是根据如何查询数据而设计的,而不是根据要存储的数据结构。

  2. 单表设计 将同类数据存储在单个表中,并利用分区键、排序键和 GSI 实现高效查询,而不是存储到多个表中。

  3. 利用索引 适当使用 GSI 和 LSI(本地二级索引)以确保查询灵活性。

  4. 最小化扫描 全表扫描性能较差,因此尽可能使用 GSI 或使用特定键的查询。

  5. 使用事务 利用 DynamoDB 事务确保数据完整性。


通过本文的介绍,希望您能更好地理解和使用 DynamoDB,从而构建出高效、可扩展的应用。

关于

分享技术见解、经验和思考的个人博客

联系方式

  • Email: hushukang_blog@proton.me
  • GitHub

© 2025 码中赤兔. 版权所有