DynamoDBトランザクション
DynamoDBトランザクションは複数の書き込みを単一のall-or-nothing操作にまとめます。すべてのアクションがコミットされるか、1つもコミットされないかのどちらかです。書き込みが半分失敗したときに、関連する2つのアイテムが食い違わないようにする方法です。
監査ログのシナリオでは、追加するすべてのイベントは、テナントごとのeventCountも増やすべきです。イベントが届いてもカウンターが届かなければ — あるいはその逆でも — ログとカウントは永遠に食い違います。トランザクションはそれを不可能にします。
DynamoDBはトランザクションをサポートしていますか?
はい。DynamoDBはTransactWriteItemsとTransactGetItemsを通じてACIDトランザクションをサポートしており、1つ以上のテーブルにまたがって最大100のアクションを1つのall-or-nothing操作にまとめます。すべての書き込みがコミットされるか、1つもコミットされないかのどちらかなので、関連するアイテムが食い違うことはありません。トランザクション書き込みは通常の書き込みの2倍のキャパシティを消費し、条件の失敗や競合が発生するとリクエスト全体がキャンセルされます。
TransactWriteItemsは最大100の書き込みアクションをまとめます。 1つ以上のテーブルにまたがり、all-or-nothingです。アイテムの合計サイズは4 MBを超えられません。- アクションは
Put、Update、Delete、ConditionCheckです。ConditionCheckは、書き込まないアイテムについて何かをアサートします。 - コストは2倍です。 トランザクション書き込みは通常の書き込みの2倍のキャパシティを消費します — DynamoDBは準備してからコミットします。
- 競合や失敗した条件は全体をキャンセルし、
TransactionCanceledExceptionを返します。部分的なものは何も残りません。
問題:一致しなければならない2つの書き込み
新しい監査イベントごとに、テナントの累積カウントも増やしたいとします。2つの別々の呼び出しとして行うと、その間のどんな失敗もデータを破損させます:
- 新しい
EVENT#…アイテムをPutItem— 成功する。 ADD eventCount 1へUpdateItem— タイムアウトする。
これでログはカウンターが主張するより1行多くなります。盲目的にステップ2をリトライすると二重カウントのリスクがあり、リトライしないと不整合のままになります。2つの書き込みは決して結び付けられていなかったので、安全なリカバリはありません。
SQLから来た人なら、両方をBEGIN … COMMITで包むでしょう。DynamoDBの答えは、両方の書き込みを一緒に運ぶ単一のトランザクションリクエストです。
TransactWriteItemsの仕組み
AWS開発者ガイドによれば、TransactWriteItemsは最大100の異なるアイテムを対象として「最大100の書き込みアクションを単一のall-or-nothing操作にまとめ」、「トランザクション内のアイテムの合計サイズは4 MBを超えられません」。アクションはアトミックに完了します — すべて成功するか、1つも成功しないかです。
1つのトランザクションで4種類のアクションを混在させられます:
Put— アイテムを作成または置換する。Update— 属性を編集する(このカウンターのためのADDを含む)。Delete— キーでアイテムを削除する。ConditionCheck— それ以外では書き込まないアイテムについて条件をアサートする(例:「このテナントはまだアクティブである」)。
実際にはさらに2つのルールが効いてきます。第一に、トランザクションは等価な非トランザクション書き込みの2倍のキャパシティを消費します — DynamoDBは準備フェーズとコミットフェーズを行います。第二に、1つのトランザクションで同じアイテムを2回対象にすることはできず、トランザクションはインデックスに対して実行できません。
実例:追加とカウントをアトミックに
監査ログに戻ります。テナントacmeのイベントを追加してそのカウンターを増やすのは、2つのアクションを持つ1つのトランザクションです:
| action | item | effect | |
|---|---|---|---|
| Put | TENANT#acme | EVENT#2026-06-24T09:14Z#a1 | write the new audit row |
| Update | TENANT#acme | COUNTER | ADD eventCount 1 |
いずれかのアクションの条件が失敗した場合 — 例えばテナントが停止されていないことのConditionCheck — リクエスト全体がTransactionCanceledExceptionでキャンセルされ、どちらの書き込みも起きません。ログとカウンターが食い違うことは決してありません。
各アクションのConditionExpressionがてこになります。イベント行がまだ存在しないこと(リトライが重複させられないように)とテナントがアクティブであることをアサートするには、Putにattribute_not_exists(SK)を、ConditionCheckとしてstatus = :activeを組み合わせます。
ExpressionAttributeNamesと:valプレースホルダーを手で組み立てる代わりに、それらの型付き条件式をDynamoDB Expression Builderで構築してコピーしてください — 条件付き書き込みモードがTransactWriteItemsの求める形そのものを出力します。
不安定な接続での安全なリトライには、クライアントトークンを付与します。同じトークンを持つTransactWriteItemsを10分以内に繰り返すと、書き込みを再適用することなく成功を返します(冪等性)。
DynoTableでの実践
DynoTableは自身の書き込みに内部でトランザクションを使います。複数のアイテム編集をステージしてコミットすると、楽観ロックの条件式付きでTransactWriteItemsとして送信するので、編集のバッチはall-or-nothingになります — 複数アイテムの変更を中途半端に適用することは決してありません。
つまり、イベント行とカウンターを同じステージしたバッチで編集し、差分をレビューし、SDKコードを一切書かずに両方をアトミックにコミットできます。

落とし穴と次のステップ
- 2倍のキャパシティを見込んでください。 トランザクション書き込みはプレーンな書き込みの2倍のWCUを課金します — たまの整合性が重要なペアには問題ありませんが、すべての書き込みをトランザクションで包むと高くつきます。アトミック性が実際に重要なところで使ってください。
TransactionCanceledExceptionを明示的に処理してください。 これは失敗した条件または同じアイテムに対する別の進行中トランザクションとの競合に対して返されます。キャンセル理由がどのアクションが失敗したかを教えてくれます — それを調べ、盲目的にリトライしないでください。- ストリームレコードはトランザクションを認識しません。 1つのトランザクションからの変更はStreamsへ段階的に伝播し、他と入り混じることがあります。コンシューマーはアトミック性や順序を仮定できません — DynamoDB Streamsを参照してください。
- 高スループットのカウンターには不向きです。 重い並行トランザクション負荷の下にある単一のホットカウンターはスロットリングします。その場合はアトミックカウンターやカウンターのシャーディングを検討してください。
トランザクションは「これらの書き込みは一致しなければならない」ためのツールです。イベントが整合性をもって届くようになったら、次の懸念はそれらに反応することです — それがDynamoDB Streamsです。
自分のテーブルに対して複数アイテムの編集をステージし、それらを単一のトランザクションとしてコミットするには、DynoTableをダウンロードしてください。


