4. DynamoDB数据建模与操作

DynamoDB 是 AWS 提供的全托管 NoSQL 键值与文档数据库,它的数据模型与关系型数据库有本质差异。理解表、项目、属性、主键、二级索引、容量模式、Streams、TTL 与事务,是在 Lambda 中正确执行 CRUD 的前提。本节围绕这些核心概念展开,并给出可落地的建模与操作示例。

表、项目与属性

DynamoDB 中,表(Table)是数据的容器,项目(Item)是表中的一行记录,属性(Attribute)是项目中的字段。与关系型数据库不同,DynamoDB 的项目是半结构化的:同一张表中不同项目可以拥有不同的属性集合,除主键外没有强制 schema。属性支持标量(String、Number、Binary、Boolean、Null)、集合(String Set、Number Set、Binary Set)和文档类型(List、Map)。

例如一张订单表,订单项目可以包含 orderId、customerId、amount、status、items(List)等属性,而另一个订单项目可能额外带有 couponCode。这种灵活性让建模更贴近业务,但也要求开发者自行保证数据一致性。

主键设计

主键是 DynamoDB 中唯一必须预先定义的 schema,分为两种:

  • 分区键(Partition Key):单属性主键,DynamoDB 对其做哈希,决定项目落在哪个分区。
  • 分区键 + 排序键(Sort Key):复合主键,同一分区键下可有多条项目,按排序键有序存储,支持范围查询。

主键设计直接决定访问模式与性能。分区键应具备高基数且访问均匀,避免“热分区”。例如用 userId 作为分区键、orderId 作为排序键,可以高效查询“某用户的所有订单”以及“某用户的某笔订单”。若把 status 作为分区键,则大量订单集中在少数状态值上,容易造成热点。

建模口诀:先确定访问模式,再设计主键。DynamoDB 不支持随意新增索引来补救错误的主键设计,索引数量也有限。

二级索引

当业务需要按非主键属性查询时,使用二级索引。DynamoDB 提供两类:

  • 全局二级索引(GSI):拥有独立的分区键和排序键,可跨分区查询,容量独立配置,支持最终一致性读取(也可选强一致性)。
  • 本地二级索引(LSI):分区键与基表相同,排序键不同,必须在建表时创建,容量与基表共享。

例如订单表以 userId 为分区键、orderId 为排序键,若需要按“下单时间”查询,可建立 GSI:分区键 userId、排序键 createdAt。GSI 是异步更新的,写入基表后索引可能短暂滞后,读取时需接受最终一致性。

读写容量模式

DynamoDB 提供两种容量模式:

模式计费方式适用场景
按需(On-Demand)按实际读写请求计费流量不可预测、突发明显
预置(Provisioned)按预置 RCU/WCU 计费,可配自动扩缩流量稳定、成本敏感

1 个 RCU 表示每秒 1 次强一致性读取(4 KB 以内)或 2 次最终一致性读取;1 个 WCU 表示每秒 1 次写入(1 KB 以内)。例如每秒写入 100 条、每条 2 KB,则需要 200 WCU。容量不足会触发 ProvisionedThroughputExceededException,Lambda 中应配合重试与退避。

DynamoDB Streams 与 TTL

DynamoDB Streams 捕获表中项目的变更(插入、修改、删除),按时间顺序保存 24 小时,可触发 Lambda 做下游处理,如同步到 OpenSearch、发送通知、更新聚合表。流记录包含 eventName、Keys、NewImage、OldImage 等字段。

TTL(Time To Live)允许为项目设置过期时间戳属性(Unix 秒),到期后 DynamoDB 自动删除,不消耗 WCU。TTL 删除通常在到期后数分钟到 48 小时内完成,因此业务读取时仍应校验过期时间。典型用途是会话数据、临时缓存、验证码。

事务操作

DynamoDB 支持 TransactWriteItems 和 TransactGetItems,可在单次请求中对最多 100 个项目执行原子性读写,跨表也可。事务要么全部成功,要么全部失败,适合扣减库存、转账等场景。事务写入消耗的 WCU 是普通写入的两倍,且不支持在同一事务中操作同一项目两次。

DynamoDB 数据模型与访问路径 基表 Orders PK: userId (S) SK: orderId (S) amount: 199.00 status: "PAID" createdAt: 1717000000 ttl: 1719600000 GSI: UserCreatedIndex PK: userId (S) SK: createdAt (N) 投影: ALL 查询: 按时间范围 一致性: 最终一致 异步同步 DynamoDB Streams INSERT / MODIFY / REMOVE 保留 24 小时 变更捕获 Lambda 消费者 批量读取流记录 下游同步 / 通知 事件触发 TTL 自动过期删除(不消耗 WCU) 到期后数分钟至 48 小时内删除,读取时仍需校验过期时间

在 Lambda 中使用 AWS SDK 执行 CRUD

Lambda 中推荐使用 AWS SDK for JavaScript v3,按需引入 @aws-sdk/client-dynamodb 与 @aws-sdk/lib-dynamodb。以下示例演示写入、读取、更新与删除。

import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, PutCommand, GetCommand,
         UpdateCommand, DeleteCommand, QueryCommand } from "@aws-sdk/lib-dynamodb";

const client = new DynamoDBClient({});
const doc = DynamoDBDocumentClient.from(client);
const TABLE = "Orders";

export const handler = async (event) => {
  const { userId, orderId, amount } = JSON.parse(event.body);

  // 写入
  await doc.send(new PutCommand({
    TableName: TABLE,
    Item: { userId, orderId, amount, status: "CREATED",
            createdAt: Math.floor(Date.now()/1000) }
  }));

  // 读取
  const got = await doc.send(new GetCommand({
    TableName: TABLE, Key: { userId, orderId }
  }));

  // 更新
  await doc.send(new UpdateCommand({
    TableName: TABLE,
    Key: { userId, orderId },
    UpdateExpression: "SET #s = :s",
    ExpressionAttributeNames: { "#s": "status" },
    ExpressionAttributeValues: { ":s": "PAID" }
  }));

  // 查询某用户全部订单
  const list = await doc.send(new QueryCommand({
    TableName: TABLE,
    KeyConditionExpression: "userId = :u",
    ExpressionAttributeValues: { ":u": userId }
  }));

  // 删除
  await doc.send(new DeleteCommand({
    TableName: TABLE, Key: { userId, orderId }
  }));

  return { statusCode: 200, body: JSON.stringify(list.Items) };
};

注意:UpdateExpression 中若属性名是保留字(如 status),必须用 ExpressionAttributeNames 映射;条件写入可用 ConditionExpression 实现乐观锁,例如 attribute_not_exists(orderId) 防止覆盖。

Lambda 执行角色需具备 dynamodb:PutItem、GetItem、UpdateItem、DeleteItem、Query 等权限。生产环境应遵循最小权限原则,按表与索引粒度授权。

建模检查清单

  • 是否已列出全部访问模式,并据此确定主键?
  • 分区键是否高基数、访问均匀,避免热分区?
  • 需要按非主键查询的属性是否已建 GSI,并确认投影与一致性要求?
  • 容量模式是否匹配流量特征,是否配置了自动扩缩或按需?
  • 是否需要 Streams 触发下游处理,Lambda 是否配置了批处理与失败重试?
  • 临时数据是否设置 TTL,读取时是否校验过期?
  • 跨项目原子操作是否使用事务,并评估了双倍 WCU 成本?

掌握以上要点后,即可在 Lambda 中稳定地完成 DynamoDB 的建模与 CRUD 操作,并为后续的事件驱动架构打下基础。