進階閱讀時間 3 分鐘

DynamoDB 交易

DynamoDB 交易把數筆寫入組成單一「全有或全無」的操作:要嘛每個動作都提交,要嘛全都不提交。這就是當一次寫入半途失敗時,你讓兩個相關項目不至於彼此偏離的方法。

在稽核記錄的情境中,你附加的每個事件也應該讓某個依租戶的 eventCount 加一。如果事件落地但計數器沒有 — 或反過來 — 那麼記錄與計數就會永遠不一致。一筆交易讓這不可能發生。

DynamoDB 支援交易嗎?

是的。DynamoDB 透過 TransactWriteItemsTransactGetItems 支援 ACID 交易,可將最多 100 個動作組成橫跨一或多張表格的單一全有或全無操作。要嘛每筆寫入都提交,要嘛全都不提交,因此相關的項目不會彼此偏離。交易式寫入消耗的容量是一般寫入的兩倍,而條件失敗或衝突會取消整個請求。

  • TransactWriteItems 把最多 100 個寫入動作組起來,橫跨一或多張表格,全有或全無。彙總的項目大小不能超過 4 MB。
  • 動作有 PutUpdateDeleteConditionCheck ConditionCheck 對一個你並未寫入的項目做出斷言。
  • 它的成本是雙倍。 一次交易式寫入消耗的容量是一般寫入的兩倍 — DynamoDB 會先準備再提交。
  • 衝突與失敗的條件會取消整筆操作,並回傳 TransactionCanceledException;不會留下任何部分完成的東西。

問題:兩筆必須一致的寫入

你希望每個新的稽核事件也讓該租戶的累計計數加一。若做成兩次獨立的呼叫,兩者之間的任何失敗都會損毀你的資料:

  1. PutItem 寫入新的 EVENT#… 項目 — 成功。
  2. UpdateItem 執行 ADD eventCount 1 — 逾時。

現在記錄比計數器所宣稱的多了一列。盲目重試步驟 2 有重複計數的風險;不重試則讓它們不一致。沒有安全的復原方式,因為這兩筆寫入從未被連結在一起。

從 SQL 過來,你會把兩者包進 BEGIN … COMMIT。DynamoDB 的答案是一個單一交易式請求,把兩筆寫入一起攜帶。

TransactWriteItems 如何運作

根據 AWS 開發人員指南TransactWriteItems「把最多 100 個寫入動作組成單一全有或全無的操作」,目標最多 100 個不同的項目,而且「交易中各項目的彙總大小不能超過 4 MB」。這些動作會原子式地完成 — 全部成功或全部不做。

你可以在一筆交易中混用四種動作類型:

  • Put — 建立或取代一個項目。
  • Update — 編輯屬性(包括我們計數器用到的 ADD)。
  • Delete — 依鍵移除一個項目。
  • ConditionCheck — 對一個你並未另行寫入的項目斷言某個條件(例如「此租戶仍為使用中」)。

實務上還有兩條規則會咬人。第一,交易消耗的容量是等效非交易式寫入的兩倍 — DynamoDB 會做一個準備階段與一個提交階段。第二,你不能在一筆交易中對同一項目下手兩次,且交易不能對 index 執行。

"DynamoDB"App"DynamoDB"Appprepare both, checkconditions"TransactWriteItems [Put EVENT,Update counter]""both commit, orTransactionCanceledException"

一個實作範例:附加+計數,原子式地

回到稽核記錄。為租戶 acme 附加一個事件並讓其計數器加一,是一筆有兩個動作的交易:

actionitemeffect
PutTENANT#acmeEVENT#2026-06-24T09:14Z#a1write the new audit row
UpdateTENANT#acmeCOUNTERADD eventCount 1

若任一動作的條件失敗 — 比如說一個檢查該租戶未被停權的 ConditionCheck — 整筆請求就會以 TransactionCanceledException 取消,而且兩筆寫入都不會發生。記錄與計數器永遠不可能不一致。

每個動作上的 ConditionExpression 就是那根槓桿。若要斷言事件列尚不存在(讓重試無法產生重複)且租戶為使用中,你就在 Put 上組出 attribute_not_exists(SK)、並以 status = :active 作為一個 ConditionCheck 之類的條件。

DynamoDB 表達式建構器 中建構並複製這些帶類型的條件表達式,而不要手動拼湊 ExpressionAttributeNames:val 佔位符 — 條件式寫入模式所產生的,正是 TransactWriteItems 想要的結構。

若要在不穩定的連線上安全重試,請附上一個用戶端 token:在 10 分鐘內以相同 token 重複的 TransactWriteItems 會回傳成功,而不會重新套用那些寫入 (冪等性)。

在 DynoTable 中實作

DynoTable 在底層就用交易來進行它自己的寫入:當你暫存好數筆項目編輯並提交時,它會以帶有樂觀鎖定條件表達式的 TransactWriteItems 送出,所以你那批編輯是全有或全無的 — 你絕不會半套地套用一次多項目變更。

這表示你可以在同一個暫存批次中編輯事件列與計數器、檢視差異,並原子式地一起提交兩者,而不必寫任何 SDK 程式碼。

在 DynoTable 中暫存新的稽核事件與租戶計數器編輯,再把兩者作為一筆全有或全無的交易提交。
在 DynoTable 中暫存新的稽核事件與租戶計數器編輯,再把兩者作為一筆全有或全無的交易提交。

陷阱與後續步驟

  • 為雙倍容量編列預算。 一次交易式寫入計收的 WCU 是一般寫入的兩倍 — 對於偶爾、對一致性至關重要的成對寫入沒問題,但若把每一筆寫入都包進交易就所費不貲。請用在原子性確實重要之處。
  • 明確處理 TransactionCanceledException 它在條件失敗與同一批項目上另一筆進行中的交易發生衝突時回傳。取消原因會告訴你是哪個動作失敗了 — 請檢視它們,不要盲目重試。
  • stream 記錄不感知交易。 來自一筆交易的變更會逐步傳播到 Streams,並可能與其他變更交錯;消費端不能假設原子性或排序 — 請見 DynamoDB Streams
  • 不適合高輸送量的計數器。 單一熱計數器在大量並行交易負載下會被節流;若有此需要,請看原子計數器或將計數器分片。

交易是用於「這些寫入必須一致」的工具。一旦事件能一致地落地,下一項考量就是對它們做出反應 — 那就是 DynamoDB Streams

下載 DynoTable 來暫存多項目編輯,並把它們作為單一交易對你自己的表格提交。

已更新