Node.js 中的 DynamoDB TransactWriteItems(AWS SDK v3)
一次成功的 TransactWriteItemsCommand 几乎什么都不告诉你:没有项目、没有属性、一个空响应。你需要的一切都在异常上,所以在 SDK v3 里,下面那个 catch 块才是真正的 API 表面,而且值得确切地知道会有什么落进去。(至于事务到底什么时候才是对的选择,参见 DynamoDB 事务。)
代码
import {DynamoDBClient, TransactWriteItemsCommand} from '@aws-sdk/client-dynamodb';
const client = new DynamoDBClient({region: 'us-east-1'});
// Move one award between two songs — atomically. If the first song has no
// award to give, NEITHER update happens.
const command = new TransactWriteItemsCommand({
TransactItems: [
{
Update: {
TableName: 'Music',
Key: {Artist: {S: 'Arturo Sandoval'}, SongTitle: {S: 'Cubano Chant'}},
UpdateExpression: 'SET #upd0 = #upd0 - :one',
ConditionExpression: '#upd0 >= :one',
ExpressionAttributeNames: {'#upd0': 'Awards'},
ExpressionAttributeValues: {':one': {N: '1'}}
}
},
{
Update: {
TableName: 'Music',
Key: {Artist: {S: 'Arturo Sandoval'}, SongTitle: {S: 'A Mis Abuelos'}},
UpdateExpression: 'SET #upd0 = if_not_exists(#upd0, :zero) + :one',
ExpressionAttributeNames: {'#upd0': 'Awards'},
ExpressionAttributeValues: {':one': {N: '1'}, ':zero': {N: '0'}}
}
}
]
});
try {
await client.send(command);
console.log('Transaction committed');
} catch (err) {
if (err.name === 'TransactionCanceledException') {
// One reason per action, in TransactItems order. 'None' means that action
// was fine — some OTHER action sank the transaction.
const codes = (err.CancellationReasons ?? []).map((r) => r.Code);
console.log('Transaction canceled:', codes); // e.g. ['ConditionalCheckFailed', 'None']
} else {
throw err;
}
}说明
TransactItems——一个由Put、Update、Delete和ConditionCheck动作组成的有序数组。这个顺序不是执行顺序(事务是原子的),但它_确实_是失败原因回来时的顺序,而这也是你唯一需要在意它的理由。上限在下文讲。- v3 实际抛出的是什么。捕获到的对象自身的属性是
$fault、$retryable、$metadata、name、CancellationReasons、message和__type。没有err.code;要 switch 的字符串是err.name,而err.$metadata带着httpStatusCode: 400外加attempts: 1——你正是靠它才能判断 SDK 没有悄悄替你重试这次取消。 CancellationReasons是按位置的,而且是稀疏的。对上面这个事务,它回来时是[{"Code":"ConditionalCheckFailed","Message":"The conditional request failed"},{"Code":"None"}]。那个None条目根本没有Message属性,所以err.CancellationReasons.map((r) => r.Message.trim())会在你的错误处理器内部、恰恰在那些成功了的动作上抛错。ReturnValuesOnConditionCheckFailure: 'ALL_OLD'会给那个动作的原因加上一个Item,排在Code和Message前面,格式是原始的 DynamoDB JSON。落败项目的属性白送给你;不然的话,你就得在已经输掉这场竞争之后再补一次GetItem。err.name这个判断有个缺口,而且值得知道是哪一个。把两个动作指向同一个项目,DynamoDB 会回答ValidationException,消息是Transaction request cannot include multiple operations on one item,并且完全没有CancellationReasons,因为什么都还没尝试。上面那个else { throw err }分支会把它重新抛出去。这是正确行为,不是 bug,但它意味着结构性的错误永远到不了你的取消日志里。- v3 本来就会发送
ClientRequestToken,哪怕你没写它。抓取序列化后的请求体能看到线上有一个全新的 UUID,而对同一个命令对象调用两次send()发出去的是两个不同的令牌。所以这个令牌保护的是一次在途调用,而不是你自己的重试循环:catch 之后重发,你拿到的是一个新令牌和零幂等性。如果一次重试可能跨越进程边界,就自己提供一个。改了任何参数还复用它,你得到的是IdempotentParameterMismatch,而不是悄无声息地重复生效。 - 只有另外一个码需要单独的代码路径。
TransactionConflict表示有个并发事务占住了你的某个项目,所以带退避的重试是对的应对,而对ConditionalCheckFailed则永远不是。其余的在 TransactionCanceledException 页面上有逐一解读。 - 成本——事务里的每个项目在底层都会被写两次(准备,然后提交),所以按普通写入约 2 倍的写入容量来做预算。单项目的条件写入以一半的代价给你单个项目上的原子性。
你会先撞上哪个上限
100 个动作的上限和 4 MB 的上限是彼此独立的,而让人意外的是字节那个:一百次计数器自增算不了什么,而十来个大项目光靠自己就能把总量吃光。在你决定一次批多少个动作之前,先用 DynamoDB 项目大小计算器量一个有代表性的项目。想在还在写条件的时候就读一读某个动作将要碰到的项目,就下载 DynoTable。
相关示例
- Python 中的 DynamoDB TransactWriteItems——用 boto3 做同样的事务。
- 用 AWS CLI 执行 DynamoDB TransactWriteItems——从命令行做同样的事务。
- Node.js 中的 DynamoDB 条件写入——不花那 2 倍代价的单项目原子性。
- DynamoDB 事务——隔离性、幂等性,以及事务什么时候值得用。
- DynamoDB TransactionCanceledException——每一个取消原因码的解读。
- "Too many actions in a TransactWriteItems call"——事务的 100 动作与 4 MB 上限。
- "Transaction request cannot include multiple operations on one item"——每个事务里,每个项目只能有一个动作。
参考资料
- TransactWriteItems — Amazon DynamoDB API Reference
- Amazon DynamoDB transactions: how it works — Amazon DynamoDB Developer Guide
- DynamoDB read and write operations (capacity unit consumption) — Amazon DynamoDB Developer Guide
最后核实于 2026-07-28,依据上方链接的 AWS 官方文档。