AWS SDK for JavaScript v3 发布后,Node.js 开发者访问 DynamoDB 的方式发生了明显变化。v2 中习惯的 new AWS.DynamoDB.DocumentClient() 整体实例化模式被废弃,取而代之的是按服务拆分的独立 npm 包和命令式调用。虽然迁移有一定成本,但模块化带来的冷启动优化和体积下降是实打实的。本文从零开始,用可运行的代码演示如何在 Node.js 项目中使用 AWS SDK v3 完成 DynamoDB 的核心读写操作,并重点分析条件表达式、分页处理和错误重试这三个容易出错的环节。

一、安装与初始化:客户端和文档客户端的区别
v3 将 DynamoDB 服务封装在 @aws-sdk/client-dynamodb 中,同时提供了 @aws-sdk/lib-dynamodb 作为高层封装,负责自动处理 DynamoDB 的 AttributeValue 类型映射。如果只使用低层客户端,所有值都必须写成类似 { S: "hello" } 这样的对象,非常繁琐;而文档客户端可以直接写 JavaScript 原生类型,比如字符串、数字、布尔值和对象。因此大多数业务代码都会优先使用 DynamoDBDocumentClient。
安装依赖很简单,两个包都需要:
npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
初始化时,先创建低层 DynamoDBClient 并指定区域和凭证,然后通过 DynamoDBDocumentClient.from 包装得到文档客户端。凭证通常从环境变量 AWS_ACCESS_KEY_ID 和 AWS_SECRET_ACCESS_KEY 自动读取,本地开发也可以使用 ~/.aws/credentials 配置文件。下面的代码展示了 CommonJS 写法:
const { DynamoDBClient } = require("@aws-sdk/client-dynamodb");
const { DynamoDBDocumentClient } = require("@aws-sdk/lib-dynamodb");
const client = new DynamoDBClient({
region: "us-east-1",
// 可选超时设置,单位毫秒
requestTimeout: 3000,
maxAttempts: 3
});
const docClient = DynamoDBDocumentClient.from(client, {
marshallOptions: {
// 是否自动去除空值
removeUndefinedValues: true
}
});
module.exports = { docClient };
上面的 marshallOptions.removeUndefinedValues 可以在写入时自动忽略 undefined 字段,避免类型映射报错。此外如果使用 ESM,将 require 换成 import 即可,包名保持不变。需要注意的是,DynamoDBDocumentClient.from 返回的实例仍然使用 send 方法,但命令类需要从 @aws-sdk/lib-dynamodb 导入,而不是从低层客户端导入。
二、核心写入与条件更新:避免覆盖和精度丢失
DynamoDB 最常见的操作是 PutItem 和 GetItem。文档客户端提供了 PutCommand 和 GetCommand,参数结构与 v2 类似但更明确。定义一个用户表,主键为 userId,写入一条记录可以这样做:
const { PutCommand, GetCommand } = require("@aws-sdk/lib-dynamodb");
async function saveUser(docClient) {
const command = new PutCommand({
TableName: "Users",
Item: {
userId: "u-1001",
name: "Alice",
age: 28,
tags: ["vip", "active"],
profile: {
city: "Shanghai"
}
},
// 仅当该主键不存在时写入,防止覆盖已有数据
ConditionExpression: "attribute_not_exists(userId)"
});
try {
await docClient.send(command);
console.log("写入成功");
} catch (err) {
if (err.name === "ConditionalCheckFailedException") {
console.log("用户已存在,放弃覆盖");
} else {
throw err;
}
}
}
这段代码展示了两个关键点:一是复杂数据类型(数组和嵌套对象)可以直接写入,文档客户端会自动转换为 DynamoDB 的 List 和 Map 类型;二是通过 ConditionExpression 实现幂等写入,防止高并发下误覆盖数据。条件表达式中不能直接写属性名以外的保留字,如果属性名和 DynamoDB 保留字冲突,需要使用 ExpressionAttributeNames 做别名映射。
读取数据同样简单,但要注意 DynamoDB 的最终一致性模型。默认 GetCommand 是强一致读,如果需要降低延迟或节省吞吐量成本,可以显式指定 ConsistentRead: false。读取操作示例:
const { GetCommand } = require("@aws-sdk/lib-dynamodb");
async function getUser(docClient, userId) {
const command = new GetCommand({
TableName: "Users",
Key: { userId },
ConsistentRead: true
});
const response = await docClient.send(command);
return response.Item; // 不存在时返回 undefined
}
对于更新操作,推荐使用 UpdateCommand 而不是先读后写,这样可以减少一次网络往返并避免并发覆盖。比如给用户年龄加一并限制最小年龄条件:
const { UpdateCommand } = require("@aws-sdk/lib-dynamodb");
async function incrementAge(docClient, userId, inc) {
const command = new UpdateCommand({
TableName: "Users",
Key: { userId },
UpdateExpression: "set age = age + :inc",
ConditionExpression: "age >= :minAge",
ExpressionAttributeValues: {
":inc": inc,
":minAge": 18
},
ReturnValues: "ALL_NEW"
});
const response = await docClient.send(command);
return response.Attributes;
}
注意代码中的 ConditionExpression 使用了大于等于运算符,在 HTML 源码里必须转义为 >= 才能正常显示,但实际发送给 AWS 的是原始字符串 age >= :minAge。ReturnValues 设为 ALL_NEW 可以在更新后返回最新属性,省去一次读取。如果需要乐观锁控制版本,可以增加一个 version 字段,并在条件表达式中校验版本号,更新成功后递增版本,这样可以避免 ABA 问题。
三、查询与分页:Query、Scan 和 ExclusiveStartKey 的正确姿势
DynamoDB 的 Query 和 Scan 是两种不同的数据检索方式。Query 必须指定分区键,可以高效查询同一分区下的多个项目,支持对排序键做范围过滤;Scan 则全表扫描,成本高且消耗读吞吐,通常只用于后台任务或小表。业务代码应优先使用 Query,并配合二级索引满足不同查询模式。
以用户表为例,如果分区键是 userId,无法直接按城市查询用户,需要创建一个以 city 为分区键、age 为排序键的全局二级索引(GSI)。查询某个城市中年龄大于 25 的用户可以这样写:
const { QueryCommand } = require("@aws-sdk/lib-dynamodb");
async function queryByCity(docClient, city) {
const command = new QueryCommand({
TableName: "Users",
IndexName: "city-age-index",
KeyConditionExpression: "city = :city and age > :minAge",
ExpressionAttributeValues: {
":city": city,
":minAge": 25
},
ScanIndexForward: false // 按年龄降序
});
const response = await docClient.send(command);
return response.Items;
}
上面的 KeyConditionExpression 中同样需要转义大于号。当查询结果超过 1MB 或命中限制时,DynamoDB 会返回 LastEvaluatedKey,需要把它作为下一次请求的 ExclusiveStartKey 继续查询,直到该字段为空。完整的分页循环可封装如下:
const { QueryCommand } = require("@aws-sdk/lib-dynamodb");
async function queryAllByCity(docClient, city) {
let items = [];
let lastKey = undefined;
do {
const command = new QueryCommand({
TableName: "Users",
IndexName: "city-age-index",
KeyConditionExpression: "city = :city",
ExpressionAttributeValues: { ":city": city },
ExclusiveStartKey: lastKey,
Limit: 100 // 每次最多100条
});
const response = await docClient.send(command);
items = items.concat(response.Items || []);
lastKey = response.LastEvaluatedKey;
} while (lastKey);
return items;
}
这个循环使用 do...while 确保至少执行一次,每次携带上一页的 LastEvaluatedKey。如果数据量很大,建议加上循环上限或异步分批处理,防止内存占用过高。对于 Scan 操作,分页方式完全相同,只是没有分区键限制,但要注意 Scan 会消耗全部读吞吐,线上环境尽量避免实时全表扫描。
四、错误处理与性能调优:重试、超时和批量操作
DynamoDB 的常见异常包括 ConditionalCheckFailedException(条件检查失败)、ProvisionedThroughputExceededException(吞吐超限)和 ResourceNotFoundException(表不存在)。SDK 默认会按照指数退避自动重试可重试的错误,但默认重试次数对于某些场景可能不够。在创建 DynamoDBClient 时可以显式设置 maxAttempts 和 requestTimeout,例如将最大尝试次数设为 5 次,超时设为 5 秒,以平衡可用性和响应时间。
如果使用 DynamoDB 的按需容量模式,一般不会遇到吞吐超限;但如果使用预置容量模式,突发流量下仍可能触发限流。此时可以捕获该异常并实现应用层退避重试,或者改用批量接口提高吞吐。批量写入 BatchWriteCommand 一次可以操作最多 25 个 Put 或 Delete 请求,批量读取 BatchGetCommand 一次最多 100 条记录。批量接口可以显著减少网络往返次数,但要注意单个批次的总大小不能超过 16MB,未处理完的项目会返回在 UnprocessedKeys 中,需要继续重试。
一个批量写入示例:
const { BatchWriteCommand } = require("@aws-sdk/lib-dynamodb");
async function batchInsert(docClient, users) {
const putRequests = users.map(user => ({
PutRequest: { Item: user }
}));
const command = new BatchWriteCommand({
RequestItems: {
Users: putRequests
}
});
const response = await docClient.send(command);
if (response.UnprocessedItems && Object.keys(response.UnprocessedItems).length > 0) {
// 未处理完的项目需要手动重试
console.log("存在未处理项目,稍后重试");
}
return response;
}
这段代码中的箭头函数和大于号都需要转义。实际生产中建议封装一个通用的批量重试函数,将 UnprocessedItems 重新组装为新的 BatchWriteCommand 并递归调用,直到全部成功或达到重试上限。除了批量操作,还可以通过开启 SDK 的延迟监控和日志来定位性能瓶颈,但要注意日志可能包含敏感数据,上线前做好脱敏。
综上所述,AWS SDK v3 的模块化设计让 DynamoDB 访问更加轻量,但也要求开发者适应命令式 API 和更细粒度的参数控制。掌握条件表达式、分页令牌和错误重试这三板斧,足以应对大多数业务场景,并写出既安全又高效的 Node.js DynamoDB 访问代码。
DynamoDBAWS SDK v3Node.js修改时间:2026-09-27 17:48:46