NoSQLでも、複数のドキュメントや項目をまとめて「全部成功か全部失敗か」で書き込むトランザクションは使えます。ただし、範囲と上限は製品ごとにまったく違います。MongoDBはレプリカセットが前提で既定の実行時間は1分未満、DynamoDBは1回100操作・合計4MBまで、Cosmos DBとCassandraは同じパーティションの中だけです。この記事では、MongoDBのwith_transactionとDynamoDBのTransactWriteItemsを動くコードで示し、再送の冪等性、失敗理由の読み方、料金の増え方、そしてトランザクションを使わずに済ませる設計の判断までを実装者の目線で整理します。
まとめ:NoSQLのトランザクションは1件更新で済まない箇所だけに絞る判断
NoSQLのトランザクションは、データを1件にまとめられない更新だけに使う保険です。まず埋め込みと条件付き更新で1件の原子的な書き込みに寄せ、それでも2件以上を同時に変えなければ整合が崩れる箇所に限ってトランザクションを書きます。
実装では3点を外さないでください。MongoDBはコールバックが複数回実行される前提で書くこと。DynamoDBは再送に同じClientRequestTokenを使い、失敗時はCancellationReasonsで原因の項目を特定すること。どちらも書き込み単価が増えることを見積もりに入れること。サービスやリージョンをまたぐ更新は、どの製品でもトランザクションの範囲外です。そこはSagaとoutboxで設計します。
NoSQL製品ごとのトランザクション範囲と上限値を1つの表で比べる基礎
「NoSQLはトランザクションが使えない」という理解は古く、主要製品はいずれも何らかの形で複数件の原子的な書き込みを持っています。違うのは、どこまでをひとまとめにできるかです。
MongoDB・DynamoDB・Cosmos DB・Cassandraの範囲・上限比較
4製品の公式ドキュメントから、範囲と上限値を抜き出しました(2026年11月時点)。
| 製品 | まとめられる範囲 | 主な上限 | 前提条件 |
|---|---|---|---|
| MongoDB | 複数コレクション・複数シャード | 既定で1分未満 | レプリカセットかシャード構成 |
| DynamoDB | 同じアカウント・リージョンの複数テーブル | 100操作・合計4MB | 同じ項目を2回指定しない |
| Cosmos DB | 同じ論理パーティションキー | 100操作・2MB・5秒 | NoSQL APIのSDKで実行 |
| Cassandra | LWTは1行、バッチの分離は1パーティション | Paxosで遅くなる | 条件付きのINSERTかUPDATE |
表の右に行くほど、設計で守る制約が増えます。MongoDBは範囲が広い代わりに単体起動では動かず、Cosmos DBとCassandraはパーティションキーの決め方がそのままトランザクションの範囲を決める基準です。各製品の基本構造はNoSQLとは:4つの型をDockerで動かして分かるRDBとの違いで整理しています。
BASEと結果整合性の製品でもACIDを使える範囲が決まっている理由
NoSQLは、多少古い値を許す代わりに台数を増やして書き込みをさばく「BASE」の考え方で語られることが多い分野です。それでもACIDを提供できるのは、範囲を小さく区切っているからです。
Cosmos DBはスナップショット分離のACIDを、同じ論理パーティションキーの操作に限って保証するとTransactional batchの公式解説に明記しています。DynamoDBもACIDの保証は書き込みを受けたリージョンの中だけで、グローバルテーブルの別リージョンでは途中まで反映された状態が見えることがあります。範囲の外は結果整合性。範囲の内側だけがACIDです。RDBの分離レベルやcommit・rollbackの基本はトランザクションとは?分離レベルの違い・ACID・commit/rollbackを実装目線で解説を参照してください。
MongoDBのマルチドキュメントトランザクションをPythonで動かす手順
MongoDBで1件のドキュメントへの操作は、もともと原子的に実行されます。ここでは、在庫の引当と注文の作成のように、別コレクションの2件を同時に書く場合を扱います。
Dockerで1ノードのレプリカセットを立ててトランザクションを使える状態にする
MongoDBの本番運用の注意点によると、スタンドアロン構成はトランザクションに対応していません。手元で試すときも、1ノードのレプリカセットとして起動します。
# mongo:9.0 をレプリカセット名 rs0 で起動する
docker run -d --name mongo-rs -p 27017:27017 mongo:9.0 --replSet rs0
# メンバーを localhost:27017 として初期化する(ホストのPythonから接続できるようにするため)
docker exec mongo-rs mongosh --quiet --eval 'rs.initiate({_id: "rs0", members: [{_id: 0, host: "localhost:27017"}]})'
# PRIMARY と表示されれば準備完了
docker exec mongo-rs mongosh --quiet --eval 'rs.status().members[0].stateStr'
メンバー名をコンテナのホスト名のまま初期化すると、ホスト側のドライバがその名前を引けずに接続できません。localhostを明示するのはそのためです。本番の要件は、レプリカセットならfeatureCompatibilityVersion 4.0以上、シャード構成なら4.2以上と公式に記載されています。マネージドで使う場合はMongoDB Atlasとは?クラスタ階層・料金・セキュリティ設定と採用判断も参考になります。
with_transactionで在庫引当と注文作成を1つにまとめるPythonコード
PyMongo 4.20.0のwith_transactionは、開始・コールバック実行・コミットを1回の呼び出しで行います。在庫が足りないときは例外を投げ、トランザクションごと取り消します。
from pymongo import MongoClient
from pymongo.read_concern import ReadConcern
from pymongo.write_concern import WriteConcern
client = MongoClient("mongodb://localhost:27017/?replicaSet=rs0")
db = client.shop
db.stock.insert_one({"sku": "A-1", "qty": 10}) # 検証用の初期データ
def reserve(session, sku, qty, order_id):
# 在庫が足りるときだけ減らす。足りなければ0件更新になる
r = db.stock.update_one(
{"sku": sku, "qty": {"$gte": qty}},
{"$inc": {"qty": -qty}},
session=session,
)
if r.modified_count == 0:
raise ValueError("在庫不足") # ここで中止され、在庫も注文も書かれない
# 注文IDを_idにして、同じ注文の二重登録をキー重複で止める
db.orders.insert_one({"_id": order_id, "sku": sku, "qty": qty}, session=session)
with client.start_session() as s:
s.with_transaction(
lambda s: reserve(s, "A-1", 2, "ORD-1001"),
read_concern=ReadConcern("snapshot"),
write_concern=WriteConcern("majority"),
)
write concernはトランザクションの単位で渡し、中の個々の書き込みには付けません。MongoDBのTransactionsには、個別の書き込みに指定するとエラーになると書かれています。読み取りの整合性として選べるのはlocal・majority・snapshotの3つです。
120秒の再試行と1分の実行上限から決めるコールバックの書き方
with_transactionの便利さは、再試行を自動で行う点にあります。Drivers APIの解説によると、TransientTransactionErrorでは全体をやり直し、UnknownTransactionCommitResultではコミットだけをやり直します。
注意すべきは、コールバックが2回以上実行されうることです。PyMongoのclient_sessionは、サーバー上のコミットは最大1回と保証する一方、再試行は120秒で打ち切り、この時間は変更できないと明記しています。コールバックの中でメール送信や外部APIの呼び出しをすると、DBは1回でも外部には2回届きます。外部への通知はコミットの後に出してください。
実行時間にも上限があります。既定では1分未満で終わらせる必要があり、超えたトランザクションは定期処理で中止されます。ロックの取得待ちは既定で5ミリ秒です。数千件を1つのトランザクションで更新するバッチ処理は、この2つの上限に当たりやすいので、件数で区切って小さなトランザクションに分けます。
DynamoDBのTransactWriteItemsで条件付き書き込みを束ねる実装手順
DynamoDBのトランザクションは、セッションを開いて順に書くMongoDBとは形が違います。書き込みの一覧を1回のAPI呼び出しで送り、全件が条件を満たしたときだけ反映されます。
ConditionCheckとUpdateによる残高確認・引き落としのboto3コード
TransactWriteItemsのAPIリファレンスによると、1回で指定できるのはPut・Update・Delete・ConditionCheckの組み合わせで最大100件です。次の例は、会員が有効であることを確認し、残高を減らし、注文を登録する3件を1つにまとめています。
import uuid
import boto3
from botocore.exceptions import ClientError
ddb = boto3.client("dynamodb", region_name="ap-northeast-1")
token = str(uuid.uuid4()) # 36文字。再送するときも同じ値を使う
items = [
{"ConditionCheck": { # 書き換えない項目は条件だけを確認する
"TableName": "Customers",
"Key": {"PK": {"S": "CUSTOMER#1001"}},
"ConditionExpression": "#s = :active",
"ExpressionAttributeNames": {"#s": "status"},
"ExpressionAttributeValues": {":active": {"S": "ACTIVE"}},
}},
{"Update": { # 残高が足りるときだけ減らす
"TableName": "Wallets",
"Key": {"PK": {"S": "WALLET#1001"}},
"UpdateExpression": "SET balance = balance - :amt",
"ConditionExpression": "balance >= :amt",
"ExpressionAttributeValues": {":amt": {"N": "3000"}},
}},
{"Put": { # 同じ注文IDが無いときだけ登録する
"TableName": "Orders",
"Item": {"PK": {"S": "ORDER#1001-0001"}, "amount": {"N": "3000"}},
"ConditionExpression": "attribute_not_exists(PK)",
}},
]
try:
ddb.transact_write_items(TransactItems=items, ClientRequestToken=token)
except ClientError as e:
if e.response["Error"]["Code"] == "TransactionCanceledException":
codes = [r["Code"] for r in e.response.get("CancellationReasons", [])]
print(codes) # 例: ['None', 'ConditionalCheckFailed', 'None']
raise
同じ項目に2つの操作を指定することはできません。会員の確認と更新を同じ項目に対して行いたいなら、Updateの条件式に確認を含めます。インデックスに対するトランザクションも使えません。
ClientRequestTokenの10分窓で二重実行を防ぐ再送設計
通信のタイムアウトで応答が返らなかったとき、成功したかどうかは呼び出し側から分かりません。ClientRequestTokenを付けておけば、同じトークンでの再送は副作用なしで成功として返ります。
有効期間は最初のリクエストの完了から10分間です。10分を過ぎると新しいリクエストとして扱われ、二重に書き込まれます。10分以内でも、トークンが同じで中身が違えばIdempotentParameterMismatchで拒否されます。キューから再実行するジョブでは、トークンを処理のたびに作り直すのではなく、注文IDなどから決まる値としてメッセージに持たせてください。長さは1〜36文字なので、UUIDの文字列がそのまま収まります。
CancellationReasonsの順序から失敗した項目を特定する例外処理
トランザクションが中止されると、TransactionCanceledExceptionが返ります。CancellationReasonsはTransactItemsと同じ順序で並び、問題のない項目に入るのは文字列の”None”です。上のコード例で2番目がConditionalCheckFailedなら、残高不足だと判断できます。
原因のコードごとに扱いを分けます。
- ConditionalCheckFailed:業務上の失敗。再試行せず、残高不足や登録済みとして利用者に返す
- TransactionConflict:同じ項目を別のトランザクションが更新中。間隔を空けて再試行する
- ThrottlingErrorとProvisionedThroughputExceeded:容量不足。指数的に間隔を延ばして再試行する
- ValidationError:項目が400KBを超えるなどの入力誤り。再試行しても通らないので修正する
DynamoDBのトランザクション解説には、この例外はAWS SDKが再試行しないと書かれています。TransactionConflictの再試行は自分のコードで書く必要があります。
1項目あたり2倍の書き込み単位を見込む必要容量と料金の見積もり
トランザクションの機能自体は無料です。ただし、DynamoDBは各項目について準備とコミットの2回の書き込みを行うため、消費する書き込み単位は通常の2倍になります。公式の例では、500バイトの項目3件を書くトランザクションを毎秒1回実行すると、6WCUが必要です。
見落としやすいのは、条件を満たさずに中止されたトランザクションも同じだけ容量を消費する点です。在庫切れの商品に注文が集中すると、書き込まれないのに課金だけが増えます。そのような箇所は、トランザクションの前に普通の読み取りで在庫を確認し、明らかに失敗する呼び出しを減らす方針です。料金体系の全体はAmazon DynamoDBとは:RDSとの違い・料金の仕組み・使い方と設計の要点で解説しています。
Cosmos DBとCassandraで単一パーティションに閉じるトランザクションの書き方
Cosmos DBとCassandraでは、トランザクションの範囲がパーティションの単位で決まります。同時に更新したいデータを同じパーティションに置けるかが、設計の分かれ目です。
Cosmos DBのtransactional batch:同一パーティションキーの操作
Cosmos DBのtransactional batchは、同じ論理パーティションキーを持つ最大100件の操作をまとめて実行します。ペイロードは2MBまで、実行時間は5秒までです。Python SDKでは、操作を(種類, 引数, オプション)の組で並べます。
# テナントIDをパーティションキーにしたコンテナで、注文作成と在庫の減算を同時に行う
# 在庫の項目 STOCK-A-1 も tenantId = "tenant-01" で保存しておく(別パーティションならbatchに入れられない)
order = {"id": "ORD-1001", "tenantId": "tenant-01", "sku": "A-1", "qty": 2}
ops = [
("create", (order,), {}),
("patch", ("STOCK-A-1", [{"op": "incr", "path": "/qty", "value": -2}]),
{"filter_predicate": "FROM c WHERE c.qty >= 2"}),
]
container.execute_item_batch(batch_operations=ops, partition_key="tenant-01")
どれか1件が失敗すると全体が取り消されます。失敗した操作には本来のステータスコードが入り、残りの操作はすべて424(失敗した依存関係)になります。作成しようとした項目が既にあれば409です。424が並んだ中から424以外を探せば、原因の操作が分かります。パーティションキーの選び方と料金はAzure Cosmos DBとは?料金の目安・RU課金・API選定と設計の注意点を参照してください。
CassandraのLWTとLOGGEDバッチで原子性と分離性が分かれる境界
Cassandraには、複数行を任意にまとめるトランザクションがありません。代わりに使うのが、条件付きのINSERTやUPDATEを指すLWT(軽量トランザクション)です。
-- ログインIDが未登録のときだけ登録する(二重登録の防止)
INSERT INTO shop.users (login_id, user_id) VALUES ('taro', 1001) IF NOT EXISTS;
-- 在庫が10のときだけ8に書き換える(楽観ロック)
UPDATE shop.stock SET qty = 8 WHERE sku = 'A-1' IF qty = 10;
CassandraのCQL公式ドキュメントは、LWTがPaxosを使うため性能のコストが無視できず、控えめに使うべきだとしています。BATCHについては、LOGGEDバッチなら全操作が最終的にすべて反映されるかどれも反映されないかが保証されますが、分離されるのは1つのパーティションの中だけです。UNLOGGEDバッチは失敗すると一部だけ反映されることがあります。「全部書かれる」と「途中の状態が他から見えない」は別の保証です。Cassandraのデータモデル全体はCassandraとは?分散NoSQLの仕組み・データモデル・採用判断で扱っています。
NoSQLでトランザクションを使うべき処理と設計で避けるべき処理の判断基準
使える機能でも、使う箇所は絞ります。判断の順番は、1件の書き込みに寄せられるか、同じDBとリージョンの中に収まるか、の2段階です。
埋め込みと条件付き更新で済むならトランザクションを使わない判断
MongoDB自身が、分散トランザクションは単一ドキュメントの書き込みより性能コストが大きく、スキーマ設計の代わりにはならないと注記しています。注文と明細のように常に一緒に読み書きするデータは、1つのドキュメントに埋め込めば原子的に更新できます。
DynamoDBも同じです。公式のベストプラクティスは、複数項目にまたがって一緒に更新される属性を1つの項目にまとめることを勧めています。大量データの取り込みではトランザクションの使用を避け、使うAPIはBatchWriteItemです。BatchWriteItemは一部だけ成功しうるので、取り込みは再実行しても結果が変わらない作りにします。トランザクションを書く前に「この2件は本当に別の項目でなければならないか」を確かめる。これが最初の判断です。
複数サービスやリージョンをまたぐ更新はSagaとoutboxへ切り替える条件
次の2つに当てはまる更新は、どのNoSQL製品のトランザクションでも守れません。
- 決済サービスと在庫サービスのように、別々のデータベースや外部APIにまたがる更新
- DynamoDBのグローバルテーブルのように、複数リージョンで同時に整合を取りたい更新
この場合は、各サービスのローカルな書き込みを順に実行し、失敗したら取り消しの処理を流すSagaパターンに切り替えます。実装方式の選び方はSagaパターンとは?補償トランザクション設計と実装方式の選び方を参照してください。DBへの書き込みと次のサービスへの通知を確実に対にしたいときは、通知内容を同じトランザクションでoutbox用のコレクションや項目に書き、別のプロセスが送信します。どこまでを1つのトランザクションに入れ、どこからSagaにするかは、データの持ち方と一体で決める設計です。既存システムのDB分割やNoSQLへの移行でこの線引きに迷う場合は、データベース設計・移行支援で要件の整理から対応しています。
ストリームや二次インデックスに原子性が届かない前提での読み取り設計
トランザクションが成功しても、その結果がすべての読み取り経路に同時に届くわけではありません。DynamoDBの公式解説には、トランザクションの完了後、変更はGSI・ストリーム・バックアップへ順に伝わり、同じトランザクションのストリームレコードが別々の時刻に現れることもあると書かれています。
ストリームを受けて集計する処理は、1つのトランザクションの変更が揃って届くと仮定してはいけません。トランザクション直後に最新値が必要な画面では、結果整合性の読み取りではなくConsistentReadを指定するか、TransactGetItemsでまとめて読みます。BatchGetItem・Query・Scanとトランザクションの間の分離はread committedにとどまります。
よくある質問
NoSQLのトランザクションを実装するときによく出る質問に答えます。
NoSQLはトランザクションに対応していますか?
主要な製品はトランザクションに対応しています。MongoDBは4.0以降のレプリカセットで複数ドキュメントのトランザクションを使え、DynamoDBが提供するのはTransactWriteItemsとTransactGetItemsです。Cosmos DBは同じパーティションキーの範囲でtransactional batchを、Cassandraは条件付き書き込みのLWTを持っています。ただし、まとめられる範囲と上限は製品ごとに異なる仕様です。RDBと同じ感覚で任意のデータをまとめられるのはMongoDBに近く、他の製品はパーティションや件数の制約を設計の段階で守る必要があります。
MongoDBのトランザクションは単体起動のサーバーで使えますか?
使えません。スタンドアロン構成はトランザクションに対応しておらず、レプリカセットかシャード構成が必要です。開発環境では、Dockerで起動したmongodに–replSetを付け、rs.initiateで1ノードのレプリカセットとして初期化すれば試せます。テストのCI環境も同じ構成にしておかないと、本番だけでトランザクションのエラーが出る原因になります。MongoDB Atlasのクラスタは最初からレプリカセットなので、追加の設定は不要です。
DynamoDBのBatchWriteItemとTransactWriteItemsの違いは何ですか?
全件の成否が揃うかどうかが違います。TransactWriteItemsは全件成功か全件失敗のどちらかで、条件式の指定も可能です。BatchWriteItemは一部の書き込みだけが成功することがあり、応答に返される未処理分は再送が必要です。料金も異なり、トランザクションは1項目につき2回分の書き込み単位を消費します。複数の項目の整合が必要なときはTransactWriteItems、件数の多い取り込みにはBatchWriteItemを使い分けてください。
DynamoDBのトランザクションは料金が高くなりますか?
同じ項目数の通常の書き込みと比べて、消費する容量はおよそ2倍です。DynamoDBは各項目を準備とコミットの2回に分けて書くためです。さらに、条件を満たさずに中止されたトランザクションも、試みた項目の分だけ容量を消費します。オンデマンドモードでもプロビジョンドモードでも、見積もりに使う考え方は同じです。中止が多発する箇所は、事前の読み取りで失敗が明らかな呼び出しを減らすと費用を抑えられます。
NoSQLではACIDとBASEのどちらを選ぶべきですか?
データ全体でどちらかを選ぶのではなく、更新の種類ごとに決めます。残高や在庫の引当のように、途中の状態が見えると損害が出る更新はACIDの範囲に収めます。閲覧数やログ、検索用の集計のように、数秒遅れても困らないデータは結果整合性で十分です。1つのシステムの中に両方が混在するのが普通です。ACIDが必要な更新が多く、しかもパーティションをまたぐなら、その部分はRDBに置くことも検討してください。
関連記事
- NoSQLとは:4つの型をDockerで動かして分かるRDBとの違いと選び方:NoSQLの4つの型と整合性レベルの全体像
- トランザクションとは?分離レベルの違い・ACID・commit/rollbackを実装目線で解説:RDBを含むトランザクションの基本
- MongoDBとは?ドキュメント指向データベースの仕組み・利点・使い方入門:埋め込みで1件にまとめる設計の前提
- Amazon DynamoDBとは:RDSとの違い・料金の仕組み・使い方と設計の要点:キー設計と容量モードの選び方
- Sagaパターンとは?補償トランザクション設計と実装方式の選び方:サービスをまたぐ更新の整合の取り方