【DynamoDB】トランザクションをどう実現させる? (TransactWriteItems)
本ページでは、AWS DynamoDBにおいてトランザクションをどう実現させるべきかについて、調べたこと・考えたことをまとめています。
備え付けのトランザクション機能は「使い勝手の良い万能機能」というわけではありませんので、トランザクション機能が使えない時はどうすればよいかも考えてみました。

基礎知識のおさらい
DynamoDBとは

AWS DynamoDBは、AWSのデータベースサービスです。
より具体的には、処理が高速で、拡張性・柔軟性にも優れた、Key-Value型のNoSQLデータベースサービスとなっています。詳細はこちらでも紹介しています。

トランザクションとは
トランザクションは、複数の処理をひとまとめにして、必ず「全部成功するか、全部失敗するか」のどちらかになることを保証する仕組みです。
「全部成功」であれば何も問題ありませんが、「一部の処理だけ失敗」が問題になる場合に使用されます。例として銀行の送金システムを考え、Aさん→Bさんに1万円送金するとします。
- Aさんの口座から1万円減らす
- Bさんの口座で1万円増やす
といった順に処理が行われますが、❷だけ失敗したらまずいです。トランザクションでは、こういった場合に❶の処理を無かったことにします(=Aさんの口座で1万円増やして、元通りにする)。
より細かい話をすると、トランザクションの話でよく出てくるのが「ACID特性」ですね。トランザクションが備えるべき4つの特性を合わせてACID特性と呼びます。

ACID特性を完璧に備えていないとトランザクションと呼べないかというと、そういう訳ではありません。現実世界で「完璧」は難しいので、最終的には程度問題になります。
基本的な傾向として、完璧にやろうとすると処理速度が落ちたりします。「丁寧にやるか、スピード感を持ってやるか」みたいな話は、人間の仕事でも一緒ですよね。
一般的なトランザクションの実装方法
一般的な検討フローは下記の通りです。

基本的には、DBMSのトランザクション機能が使えるのであれば、使った方が良いです。上記のACID特性うんぬんの話をDBMS側が全てやってくれますので、楽ですし安心です。
しかし、場合によっては制約があって使いにくい/使えないといった場合も発生します。その場合は、トランザクション(風)機能を自分で実装(DMBSに頼らず、アプリで制御)することになります。「風」と書いたのは、ACID特性を高いレベルで備えた信頼性の高い処理を、自分で書くのが難しいためです。
DynamoDBのトランザクション機能
Amazon DynamoDB Transactions の概要
DynamoDBにはトランザクション機能(Amazon DynamoDB Transactions)が備わっており、下記の2つのAPIが使えます。
- TransactWriteItems API:書き込みのトランザクション
- TransactGetItems API:読み込みのトランザクション
よく使われるのはTransactWriteItemsの方かなと思います。これら2つに共通する特徴、制約は以下の通りです。
- 「全部成功 or 全部失敗」を保証(原子性:Atomicity)
- 同じAWSアカウントかつリージョン内であれば、複数テーブルにまたがって処理可能
- 同時に処理できるのは最大100項目
- トランザクション内のアイテムは合計4MB以下
さらに、TransactWriteItemsはクライアントトークンを使うことで、べき等性(同じ処理を何回やっても、1回やった場合と同じ結果になる性質)も持たせることができます。

べき等性も持たせることで、よりセキュアな処理を実現させることができます。ちょっとしたエラーが許されないような厳しいシステムでも使いやすいですね。

トランザクション機能、いつどう使う?
同じテーブル内でのトランザクションはもちろん、複数テーブルにまたがって使えるのが非常に強力かつ便利です。
DynamoDBのデザインパターンとして「シングルテーブル設計」というものがありますが、あくまでマイクロサービスごとくらいの粒度でテーブルを一つにしましょうという考えであり、ある程度の規模のシステムであれば複数テーブル使用するのが一般的かと思います。トランザクション機能を使うことで、複数テーブル間での整合性も簡単に保てます。
シングルテーブル設計については、下記もご覧ください。

一方、最大100項目、合計4MB以下というのは少しネックです。ここから察しがつく通り、大量データの読み書きには不向きです。さらにAWS公式では、「できるだけトランザクションを小分けにすることで、パフォーマンス向上が期待できる」とされています。

処理を小分けにしても上限以内に収まらない場合は?
トランザクション機能を諦めるよりも先に、スキーマ設計を一度見直してみましょう。そんなに大量のデータ間で整合性を保たないといけないというのは、どこかの設計に問題があるかもしれません。
自前の実装で、トランザクションを実現するには?
もしDynamoDBのトランザクション機能を使えない/使わない場合、自前で実装する必要があります。どう頑張っても信頼性で劣ってしまいますが、各種機能・テクニックを組み合わせることで、トランザクション風に処理可能です。以下では代表例を示します。
条件付きの書き込み
DynamoDBには、「条件付きの書き込み」という機能があります。
好きな条件を設定し、「その条件を満たさなければ処理しない」という処理を実現できます。主に、同じ項目が複数のクライアントから更新される場合の競合を防ぐのに有効な機能です。
「ひとつ前の処理が成功していなければ、次の処理を実行しない」となるように条件付きの書き込みを実装することで、トランザクションに近い処理が実現可能です。

2段階コミット
こちらはDynamoDBの機能ではなく、自前(アプリ側)でその処理を実装するという話になります。
一例として、対象の項目にコミット管理用のステータス情報を付与し、下記のように処理します。status = "committed"の項目だけを正常なデータとして扱うことで、トランザクション風な機能を実現できます。
- 更新対象の各項目について、更新が終わったものから
status = "pending"にする - 全部成功したら、全て
status = "committed"に更新 - 一部失敗したら、全て
status = "cancelled"にする
ただし、実装が複雑になってしまうことに加えて、下記のようなエラーケースの対策は考える必要があるため注意が必要です。
- 途中で障害やネットワークエラー等が発生したら?
- 他処理と競合が発生したら?

あくまで、高い信頼性が求められないケース(ACID特性を完璧に備えていなくてOKなケース)での対応と思っておきましょう。
まとめ
ここまで、AWS DynamoDBでのトランザクションの実装についてまとめました。
基本的には、DynamoDB備え付けのトランザクション機能の利用をおすすめします。
ですが、どうしてもこれを利用できない/したくない、かつ、そこまで高い信頼性を求められない場合においては、自前でトランザクション風に実装も良いと思います。

なお、勉強目的という観点では、自前の実装は結構良いかもしれませんね。データベースに関する理解がぐっと深まります。
以上、DynamoDBにおけるトランザクションについて、少しでも参考になれば幸いです。
