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 提供两类:
- 全局二级索引(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 是普通写入的两倍,且不支持在同一事务中操作同一项目两次。
在 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) 防止覆盖。
dynamodb:PutItem、GetItem、UpdateItem、DeleteItem、Query 等权限。生产环境应遵循最小权限原则,按表与索引粒度授权。建模检查清单
- 是否已列出全部访问模式,并据此确定主键?
- 分区键是否高基数、访问均匀,避免热分区?
- 需要按非主键查询的属性是否已建 GSI,并确认投影与一致性要求?
- 容量模式是否匹配流量特征,是否配置了自动扩缩或按需?
- 是否需要 Streams 触发下游处理,Lambda 是否配置了批处理与失败重试?
- 临时数据是否设置 TTL,读取时是否校验过期?
- 跨项目原子操作是否使用事务,并评估了双倍 WCU 成本?
掌握以上要点后,即可在 Lambda 中稳定地完成 DynamoDB 的建模与 CRUD 操作,并为后续的事件驱动架构打下基础。