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;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()帶著兩個不同的 token 出去。所以那個 token 保護的是一次進行中的呼叫,而不是你自己的重試迴圈:捕捉、重送,你就有了新的 token 而沒有冪等性。如果重試可能跨越行程邊界,請自己提供一個。改了任何參數還沿用同一個,你會得到IdempotentParameterMismatch,而不是一次無聲的重複套用。 - 只有另外一個代碼需要專屬的程式碼路徑。
TransactionConflict代表有另一筆並行交易握住了你的某個項目,所以帶退避的重試才是對的回應,而ConditionalCheckFailed則永遠不是。其餘的都在 TransactionCanceledException 頁面上解碼。 - 成本 — 交易中的每個項目在底下都會被寫兩次(準備,然後提交),所以請以一般寫入約 2 倍的寫入容量來編列預算。單一項目的條件寫入只花一半,就能給你一個項目上的原子性。
你會先撞上哪一個上限
100 個動作的上限與 4 MB 的上限是各自獨立的,而讓人意外的是位元組那一個:一百次計數器遞增不算什麼,而十來個肥大的項目光憑自己就能耗光總量。在決定要把多少動作打成一批之前,先用 DynamoDB 項目大小計算機量一個具代表性的項目。想在還在寫條件的時候就先讀出某個動作會碰到的項目,就下載 DynoTable。
相關範例
- Python 中的 DynamoDB TransactWriteItems — 以 boto3 做同一筆交易。
- 以 AWS CLI 執行 DynamoDB TransactWriteItems — 從 shell 做同一筆交易。
- 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 文件。