MongoDB: トランザクション処理:ACIDとマルチドキュメント整合性
最終更新:2026-08-26
トランザクションはマルチドキュメント操作の原子性を保証します。この概念を習得することで、信頼性の高い金融・注文システムを開発できます。
1. 学習内容
- MongoDB 4.0+ マルチドキュメントトランザクション
- session.startTransaction / commitTransaction
- ACID特性
- readConcern / writeConcern / readPreference
- 因果整合性(Causal Consistency)
- Mongooseトランザクションラッパー
sequenceDiagram
participant App as アプリケーション
participant DB as MongoDB<br/>レプリカセット
participant Log as ログ
App->>DB: startTransaction()
activate DB
DB-->>App: session
App->>DB: 100ドル引き落とし
DB-->>App: OK
App->>DB: 100ドル入金
DB-->>App: OK
App->>DB: 取引ログ作成
DB-->>App: OK
alt 全て成功
App->>DB: commitTransaction()
DB-->>App: ✅ コミット完了
DB->>Log: 永続化
else いずれか失敗
App->>DB: abortTransaction()
DB-->>App: ❌ ロールバック
Note over DB: 全ての変更が取り消される<br/>データはトランザクション前の状態に復元
end
deactivate DB
2. なぜトランザクションが必要なのか
概念説明: トランザクションは、データベース操作の論理的な単位であり、その中の全ての操作が全体として成功するか、全体としてロールバックされることを保証します。複数のドキュメントやコレクションへの書き込み(資金移動や注文確定など)において、トランザクションなしではデータ整合性を保証できません。一部成功・一部失敗という状況はデータ汚染を引き起こします。
動作原理: MongoDB 4.0以降はマルチドキュメントACIDトランザクションをサポートしており、WiredTigerエンジンベースのスナップショット分離で実装されています。トランザクション開始時にスナップショットが作成され、全ての読み書き操作はそのスナップショットに対して実行されます。コミット時に変更が原子的にデータファイルに適用され、ロールバック時は全変更が破棄されます。内部的には、レプリカセットのoplogによって永続性とレプリケーションが保証されます。
ACID原則の詳細解説:
| 特性 | 意味 | MongoDBでの実装 | 原理 |
|---|---|---|---|
| 原子性(Atomicity) | トランザクションは全て成功か全て失敗 | commit / abort | WiredTigerはロールバックログで保証:コミット時は原子的書き込み、ロールバック時はログで復元 |
| 整合性(Consistency) | データ整合性制約が維持される | スキーマ検証 + トランザクション制約 | トランザクションコミット前に全制約をチェック、違反があれば拒否 |
| 分離性(Isolation) | 同時実行トランザクションが互いに干渉しない | スナップショット分離 | トランザクション開始時にデータスナップショット取得、同一トランザクション内では一貫した状態を維持 |
| 永続性(Durability) | コミット後は永続保存 | Journal + レプリカセットoplog | 書き込みはまずjournal(WAL)に記録され、その後レプリカセットの過半数に複製 |
MVCCメカニズム: MongoDBはMulti-Version Concurrency Control(MVCC)でスナップショット分離を実装しています。各ドキュメントは複数の歴史バージョンを保持し、トランザクションは開始タイムスタンプに対応するバージョンからデータを読み取り、書き込み操作は古いバージョンを上書きせず新しいバージョンを作成します。コミット時にのみ新しいバージョンが他のトランザクションから可視になります。
graph TB
subgraph "MVCCマルチバージョン"
D1["ドキュメント v1<br/>balance: 1000"]
D2["ドキュメント v2<br/>balance: 900<br/>(トランザクションA編集)"]
D3["ドキュメント v3<br/>balance: 1100<br/>(トランザクションB編集)"]
end
subgraph "トランザクションスナップショット読み取り"
T1["トランザクションA (t1)<br/>v1を読む"] --> R1["balance: 1000"]
T2["トランザクションB (t2)<br/>v1を読む"] --> R2["balance: 1000"]
end
D1 --> D2
D1 --> D3
style D2 fill:#cce5ff
style D3 fill:#d4edda
使用シーン:
- 金融取引(引き落としと入金は原子性が必須)
- EC注文確定(注文 + 在庫減少 + 残高引き落とし + ログ)
- マルチテーブル結合更新(ユーザーとロールテーブルの同期)
- 不向き: 単一ドキュメント操作(MongoDB単一ドキュメントはデフォルトで原子性あり)、高頻度の短いトランザクション(トランザクションはオーバーヘッドが大きい)
// ❌ 反例:トランザクションなしの送金
async function transfer(fromUserId, toUserId, amount) {
await User.updateOne({ _id: fromUserId }, { $inc: { balance: -amount } });
// システムクラッシュ!
await User.updateOne({ _id: toUserId }, { $inc: { balance: amount } });
// 送金元の残高は減ったが、送金先に入金されていない
}
3. トランザクションの基本使用
概念説明: MongoDBトランザクションはSessionオブジェクトで管理します。startSession()でセッションを作成、startTransaction()でトランザクション開始、commitTransaction()でコミット、abortTransaction()でロールバックします。トランザクション内の全操作に{ session }パラメータを渡す必要があります。
動作原理: トランザクションの完全なライフサイクルは:セッション開始 → トランザクション開始 → 操作実行(sessionパラメータ付き) → コミット/ロールバック → セッション終了。コミット時、WiredTigerは全変更をjournalに原子的に書き込み、ロールバック時はロールバックログで全変更を復元します。デフォルトのトランザクションタイムアウトは60秒で、タイムアウトすると自動的にロールバックされます。
トランザクションライフサイクル:
stateDiagram-v2
[*] --> StartSession: startSession()
StartSession --> Active: startTransaction()
Active --> Active: 操作を実行(session付き)
Active --> Committed: commitTransaction()
Active --> Aborted: abortTransaction()
Committed --> [*]: endSession()
Aborted --> [*]: endSession()
note right of Active: デフォルト60秒で<br/>タイムアウト自動ロールバック
note right of Committed: 変更をjournalに永続化
構文ルール:
| ステップ | メソッド | 説明 |
|---|---|---|
| 1 | startSession() |
セッション作成 |
| 2 | startTransaction() |
トランザクション開始 |
| 3 | 操作 + {session} |
全読み書き操作にsessionを渡す |
| 4a | commitTransaction() |
全成功 → コミット |
| 4b | abortTransaction() |
いずれか失敗 → ロールバック |
| 5 | endSession() |
セッションリソース解放 |
// === MongoDB 4.0+ マルチドキュメントトランザクション ===
const session = db.getMongo().startSession();
session.startTransaction();
try {
// 1. 引き落とし
db.users.updateOne(
{ _id: fromUserId },
{ $inc: { balance: -amount } },
{ session }
);
// 2. 入金
db.users.updateOne(
{ _id: toUserId },
{ $inc: { balance: amount } },
{ session }
);
// 3. トランザクションコミット
await session.commitTransaction();
} catch (err) {
// 4. ロールバック
await session.abortTransaction();
throw err;
} finally {
session.endSession();
}
ポイント解説:
{ session }を渡し忘れた操作はトランザクションに含まれず、保護されません。commitTransactionとabortTransactionは冪等操作で、繰り返し呼び出してもエラーになりません。- トランザクションはタイムアウトで自動ロールバックされるため、アプリケーション層で適切なタイムアウトとリトライロジックを実装すべきです。
4. ACID特性
概念概要: ACIDはデータベーストランザクションの4つのコア保証を指します—原子性、整合性、分離性、永続性。MongoDBでACIDがどう実装されているかを理解することは、信頼性の高いトランザクションシステム設計の基礎です。
分離レベルの詳細: MongoDBは3つの読み取り分離レベルをサポートし、readConcernで制御します:
| 分離レベル | readConcern | 動作 | 使用シーン |
|---|---|---|---|
| 未コミット読み取り | local |
最新のローカルデータを読む(ロールバックの可能性) | デフォルト、パフォーマンス優先 |
| コミット済み読み取り | majority |
過半数で確認されたデータを読む | 強整合性が必要な場合 |
| スナップショット分離 | snapshot |
トランザクション内で一貫したスナップショット | トランザクション内デフォルト |
sequenceDiagram
participant T1 as トランザクション1
participant T2 as トランザクション2
participant DB as MongoDB
Note over DB: 初期残高=1000
T1->>DB: startTransaction(readConcern: snapshot)
T1->>DB: 残高を読む → 1000
T2->>DB: startTransaction()
T2->>DB: 残高 -100 → 900を書き込み
T2->>DB: commitTransaction()
T1->>DB: 残高を読む → 1000(スナップショット分離、T2の変更は見えない)
Note over T1: スナップショットがトランザクション内整合性を保証
T1->>DB: commitTransaction()
Note over DB: 競合検出 → T1も残高を変更している場合、エラー発生
| 特性 | 意味 | MongoDB実装 |
|---|---|---|
| 原子性 | トランザクションは全成功か全失敗 | commit / abort |
| 整合性 | データ整合性制約 | スキーマ検証 + トランザクション |
| 分離性 | 同時実行トランザクションが干渉しない | スナップショット分離 |
| 永続性 | コミット後は永続保存 | Journal + レプリカセット |
5. readConcern / writeConcern / readPreference
概念説明: この3つの設定はMongoDBトランザクション整合性制御の「三位一体」であり、それぞれ読み取り整合性、書き込み永続性、読み取りルーティングポリシーを制御します。これらがトランザクションの整合性とパフォーマンスのトレードオフを決定します。
動作原理:
- writeConcern: 書き込み操作が成功と見なされるために確認が必要なノード数。
w: majorityは書き込みが消失しないことを保証 - readConcern: 読み取り操作が見るデータのバージョン。
majorityはコミット済みデータの読み取り、snapshotはトランザクション内整合性を保証 - readPreference: 読み取り操作がどのノードにルーティングされるか。
primaryは最強の整合性、secondaryはプライマリノードの負荷軽減
(1) 書き込み確認(Write Concern)
原理: 書き込み操作がPrimaryに書き込まれた後、Secondaryから指定された数の確認を受けてから成功を返します。
| パラメータ | 値 | 動作 | 整合性 | パフォーマンス |
|---|---|---|---|---|
w |
1 | Primary確認のみ | 低い | 最速 |
w |
majority | 過半数ノード確認 | 高い | 遅い |
j |
true | ディスクjournalに書き込み | 最も安全 | 最も遅い |
wtimeout |
ms | 待機タイムアウト | — | タイムアウトエラー |
session.startTransaction({
writeConcern: {
w: 'majority', // 過半数ノードで確認
j: true, // ディスクjournalに書き込み
wtimeout: 5000 // 5秒タイムアウト
}
});
(2) 読み取り確認(Read Concern)
原理: 読み取り操作がアクセスできるデータバージョンを制御—最新のローカルバージョン(コミットされていない可能性)か、過半数確認済みバージョン(コミット済み)か。
| レベル | 説明 | 適用シーン |
|---|---|---|
local |
最新のローカルデータを読む(デフォルト) | パフォーマンス優先、未コミットデータの読み取りを許容 |
majority |
過半数確認済みデータを読む | 強整合性が必要 |
snapshot |
スナップショット分離(トランザクション内のみ) | トランザクションデフォルト、ファジーリード防止 |
session.startTransaction({
readConcern: {
level: 'majority' // コミット済みデータを読む
},
writeConcern: { w: 'majority' }
});
(3) 読み取り設定(Read Preference)
原理: 読み取り操作をPrimaryかSecondaryノードにルーティングするかを制御し、読み書き分離を実現します。
| モード | 動作 | 適用シーン |
|---|---|---|
primary |
プライマリノードのみ読み取り | 強整合性トランザクション |
primaryPreferred |
プライマリ優先、不可ならセカンダリ | 一般的なシーン |
secondary |
セカンダリノードのみ読み取り | レポート/分析、プライマリ負荷軽減 |
secondaryPreferred |
セカンダリ優先、不可ならプライマリ | 読み取り多・書き込み少 |
nearest |
ネットワーク遅延最小 | 地理分散クラスター |
session.startTransaction({
readPreference: 'primary' // プライマリノードのみ読み取り
});
session.startTransaction({
readPreference: 'secondary' // セカンダリから読み取り
});
session.startTransaction({
readPreference: 'secondaryPreferred' // セカンダリ優先
});
推奨3点セット:
| シーン | writeConcern | readConcern | readPreference |
|---|---|---|---|
| 金融取引 | majority + j:true | snapshot | primary |
| 一般トランザクション | majority | majority | primary |
| レポート分析 | — | local | secondary |
| 開発・テスト | w:1 | local | primaryPreferred |
6. Mongooseトランザクションカプセル化
概念説明: MongooseはよりエレガントなトランザクションAPIを提供—startSession()とsessionパラメータ。ただし、手動でトランザクションを管理するtry-catch-commit-abortの定型コードは冗長です。withTransactionユーティリティ関数にカプセル化することで、ビジネスロジックコードを大幅に簡素化できます。
動作原理: MongooseのwithTransactionラッパーは、セッションライフサイクル管理(開始 → コミット/ロールバック → 終了)を自動化し、ビジネス関数はコアロジックに集中できます。MongooseはModel操作(findById、create、updateOne)に{ session }パラメータを渡すことをサポートし、CRUD操作をトランザクション内でシームレスに統合できます。
トランザクションカプセル化パターンの比較:
| パターン | コード量 | エラー処理 | リトライ対応 | 使用シーン |
|---|---|---|---|---|
| 手動try-catch | 多い | 手動 | なし | 単純なシーン |
| withTransactionカプセル化 | 少ない | 自動 | 追加可能 | 本番環境推奨 |
| mongoose.connection.transaction | 最小 | 自動 | 組み込み | Mongoose 6+ |
// === Mongooseトランザクションカプセル化 ===
async function withTransaction(callback) {
const session = await mongoose.startSession();
session.startTransaction();
try {
const result = await callback(session);
await session.commitTransaction();
return result;
} catch (err) {
await session.abortTransaction();
throw err;
} finally {
session.endSession();
}
}
// === 使用例:送金 ===
async function transfer(fromUserId, toUserId, amount) {
return withTransaction(async (session) => {
const fromUser = await User.findById(fromUserId).session(session);
if (fromUser.balance < amount) {
throw new Error('残高不足');
}
await User.updateOne(
{ _id: fromUserId },
{ $inc: { balance: -amount } },
{ session }
);
await User.updateOne(
{ _id: toUserId },
{ $inc: { balance: amount } },
{ session }
);
await TransactionLog.create([{
fromUserId,
toUserId,
amount,
createdAt: new Date()
}], { session });
return { success: true };
});
}
▶ サンプル 2:Mongooseトランザクションリトライカプセル化
// AliceのShopHub金融システム:トランザクションでWriteConflictに遭遇したら自動リトライ
async function withRetryTransaction(callback, maxRetries = 3) {
let lastError;
for (let i = 0; i < maxRetries; i++) {
const session = await mongoose.startSession();
session.startTransaction({
readConcern: { level: 'snapshot' },
writeConcern: { w: 'majority' }
});
try {
const result = await callback(session);
await session.commitTransaction();
return result;
} catch (err) {
await session.abortTransaction();
lastError = err;
if (err.errorLabels && err.errorLabels.includes('TransientTransactionError')) {
console.log(`WriteConflictによりリトライ ${i + 1}/${maxRetries}`);
continue;
}
throw err;
} finally {
session.endSession();
}
}
throw lastError;
}
// 使用例
await withRetryTransaction(async (session) => {
await User.updateOne({ _id: fromId }, { $inc: { balance: -100 } }, { session });
await User.updateOne({ _id: toId }, { $inc: { balance: 100 } }, { session });
});
7. トランザクションの制限
概念説明: MongoDBトランザクションには明確な使用境界があります—レプリカセット内で実行する必要があり、サイズ制限があり、一部の操作はサポートしていません。これらの制限を理解することが本番環境でのトラブル回避の鍵です。
制限の詳細:
| 制約 | 説明 | 理由 | 軽減策 |
|---|---|---|---|
| レプリカセット必須 | スタンドアロンMongoDBはトランザクション未対応 | トランザクションはoplogに依存 | 開発環境では単一ノードレプリカセットを許可 |
| 16MBドキュメント | トランザクション内全操作の合計 | WiredTiger単一ドキュメント制限 | 大きなトランザクションを分割 |
| デフォルト60秒タイムアウト | maxTransactionLockRequestTimeoutMillis | 長時間トランザクションのロック保持防止 | タイムアウトパラメータ調整 |
| Cappedコレクション操作不可 | 一部制限 | Cappedコレクションはロールバック未対応 | トランザクション内でCappedコレクションを避ける |
| トランザクション内コレクション作成不可 | 一部制限(4.4+で緩和) | DDLとトランザクションの競合 | トランザクション前にコレクション作成 |
| 書き込み競合 | 同一ドキュメントへの同時変更 | 楽観ロック機構 | TransientTransactionErrorを自動リトライ |
| ロック待機 | 長時間トランザクションが他操作をブロック | 意図書き込みロック | トランザクションを短くし、時間のかかる操作を避ける |
トランザクションパフォーマンスへの影響:
| 操作 | 非トランザクション | トランザクション内 | オーバーヘッド理由 |
|---|---|---|---|
| 単一ドキュメント書き込み | ベースライン | +30–50% | スナップショット維持 + ロック管理 |
| マルチドキュメント書き込み | N回の独立I/O | 1回のコミット | トランザクションを1回のI/Oにマージ、実際には高速な場合も |
| 読み取り | ベースライン | +10–20% | スナップショット読み取りの追加オーバーヘッド |
| コミット | — | 5–50 ms | journal fsync + oplog書き込み |
トランザクションのベストプラクティス:
| プラクティス | 説明 |
|---|---|
| トランザクションは可能な限り短く | ロックを保持する長時間トランザクションを避ける、100ms以内に |
| トランザクション内での計算を避ける | 複雑な計算はトランザクション外で実行、トランザクションは読み書きのみに |
| WriteConflictをリトライ | MongoDB 4.0+はerrorLabelsでリトライ可能エラーを識別 |
| 単一ドキュメント原子操作を優先 | updateOne + $incはそれ自体で原子性あり、トランザクション不要 |
8. 因果整合性(Causal Consistency)
概念説明: 因果整合性は強整合性より軽量な整合性モデルです。全ての操作がグローバルに順序付けられることは保証しませんが、因果関係のある操作が正しい順序で実行されることは保証します。例えば、「残高を読んでから引き落とす」場合、引き落とし操作は直前に読んだ残高に基づく必要があります。これが因果依存関係です。
動作原理: MongoDBはoperationTimeとclusterTimeで因果整合性を実現します。セッション内の各操作は前の操作のlogicalTimeを持ち、サーバーは後続の操作が先行操作の結果を見ることを保証します。因果整合性を有効にするにはreadConcern: majority + writeConcern: majorityが必要です。
因果整合性と他の整合性モデルの比較:
| モデル | 保証 | パフォーマンス | 適用性 |
|---|---|---|---|
| 強整合性(線形化可能性) | グローバル順序 | 最も遅い | 金融コア |
| 因果整合性 | 因果順序 | 比較的速い | マルチステップ操作 |
| 結果整合性 | 順序なし | 最も速い | ログ、通知 |
| 自分の書き込みが見える | 自分の書き込みは可視 | 速い | ユーザー体験 |
sequenceDiagram
participant A as Alice
participant P as Primary
participant S as Secondary
A->>P: 残高を読む(readConcern: majority)
P-->>A: balance=1000, clusterTime=t1
A->>P: 100ドル引き落とし(writeConcern: majority)
Note over A,P: afterClusterTime=t1を付与
P->>S: oplog複製
S-->>P: 確認
P-->>A: OK, clusterTime=t2
A->>P: 取引履歴を見る(readConcern: majority)
Note over A,P: afterClusterTime=t2を付与
P-->>A: 直前の引き落とし記録を含む ✅
Note over A,P: 因果整合性:読んだら、自分の書き込みは必ず認識できる
// === 因果整合性:操作の正しい順序を保証 ===
const session = db.getMongo().startSession();
session.startTransaction({
readConcern: { level: 'majority' },
writeConcern: { w: 'majority' }
});
// 操作1:現在の残高を読む
const account = db.accounts.findOne({ userId: 'user_001' }, { session });
// 操作2:読み取り結果に基づいて書き込み
db.accounts.updateOne(
{ userId: 'user_001' },
{ $set: { balance: account.balance - 100 } },
{ session }
);
// 保証:操作2は操作1の後の状態を見る
ポイント解説:
- 因果整合性を保証するには、セッションを使用し、
readConcernとwriteConcernを両方majorityに設定する必要があります。 - ノードをまたいで読み取る(readPreference: secondary)場合、因果整合性はローカルノードが行った書き込みを反映した読み取りを保証します。
- 因果整合性は、MongoDBのマルチドキュメントトランザクションとChange Streamsの基盤メカニズムです。
▶ サンプル:EC注文トランザクションの完全なハンズオンガイド
// シーン:注文確定プロセス(注文 + 在庫減少 + ウォレット引き落とし + ログ記録)、完全に原子的
// 前提:レプリカセットが必要。トランザクションは実行中であること
// データ初期化
db.products.insertOne({ sku: 'PHONE-001', stock: 10, price: 599 });
db.users.insertOne({ _id: 'user_001', balance: 1000 });
db.transaction_logs.createIndex({ userId: 1, createdAt: -1 });
// 完全なトランザクション関数
async function placeOrder(userId, items) {
const session = db.getMongo().startSession();
session.startTransaction({
readConcern: { level: 'snapshot' },
writeConcern: { w: 'majority' }
});
try {
// 1. 合計金額を計算 + 在庫チェック(原子的読み取り)
let total = 0;
for (const item of items) {
const product = db.products.findOne(
{ sku: item.sku, stock: { $gte: item.qty } },
{ session }
);
if (!product) {
throw new Error(`在庫切れ: ${item.sku}`);
}
total += product.price * item.qty;
}
// 2. ユーザー残高チェック
const user = db.users.findOne({ _id: userId }, { session });
if (user.balance < total) {
throw new Error('残高不足');
}
// 3. 在庫減少(条件付き、オーバーセール防止)
for (const item of items) {
const result = db.products.updateOne(
{ sku: item.sku, stock: { $gte: item.qty } },
{ $inc: { stock: -item.qty } },
{ session }
);
if (result.modifiedCount === 0) {
throw new Error(`在庫減少失敗: ${item.sku}`);
}
}
// 4. ユーザー残高から引き落とし
db.users.updateOne(
{ _id: userId, balance: { $gte: total } },
{ $inc: { balance: -total } },
{ session }
);
// 5. 注文作成
const orderResult = db.orders.insertOne({
userId,
items,
total,
status: 'paid',
createdAt: new Date()
}, { session });
// 6. 取引ログを記録
db.transaction_logs.insertOne({
userId,
orderId: orderResult.insertedId,
amount: total,
type: 'purchase',
createdAt: new Date()
}, { session });
// 7. トランザクションコミット
session.commitTransaction();
return { success: true, orderId: orderResult.insertedId };
} catch (err) {
// いずれか失敗 → 全てロールバック
session.abortTransaction();
return { success: false, error: err.message };
} finally {
session.endSession();
}
}
// 実行:注文確定
placeOrder('user_001', [
{ sku: 'PHONE-001', qty: 1 }
]);
// ロールバックシナリオのテスト:意図的にエラーを作成
placeOrder('user_001', [
{ sku: 'NONEXIST', qty: 1 } // 商品が存在しない
]);
// 例外発生 → トランザクションロールバック → 在庫、残高、注文、ログ全て変更なし
// 原子性の検証:
// db.products.findOne({ sku: 'PHONE-001' }) → stock: 10(減少していない)
// db.users.findOne({ _id: 'user_001' }) → balance: 1000(引き落としなし)
出力:
TEXT 📖 参照専用トランザクション成功時は全変更がまとめてコミット、失敗時は全変更がロールバックされ、データ整合性が保証される。
▶ サンプル 3:決済と在庫減少を伴うEC注文(難易度 ⭐⭐)
// シーン:ShopHubチェックアウト - 在庫予約、注文作成、決済引き落としを原子的に
const mongoose = require('mongoose');
const session = await mongoose.startSession();
session.startTransaction();
try {
const Product = mongoose.model('Product');
const Order = mongoose.model('Order');
const User = mongoose.model('User');
const userId = 'user_001';
const items = [
{ productId: 'prod_001', sku: 'PHONE-001', qty: 2, price: 599 },
{ productId: 'prod_002', sku: 'BOOK-001', qty: 1, price: 29 }
];
const totalAmount = 1227;
// ステップ1:在庫チェックと予約(悲観ロック付き)
for (const item of items) {
const product = await Product.findOneAndUpdate(
{ _id: item.productId, stock: { $gte: item.qty } },
{ $inc: { stock: -item.qty } },
{ session, new: true }
);
if (!product) {
throw new Error(`${item.sku}の在庫不足`);
}
}
// ステップ2:ユーザー残高から引き落とし
const user = await User.findOneAndUpdate(
{ _id: userId, balance: { $gte: totalAmount } },
{ $inc: { balance: -totalAmount } },
{ session, new: true }
);
if (!user) {
throw new Error('残高不足');
}
// ステップ3:注文作成
const order = await Order.create([{
userId,
items,
total: totalAmount,
status: 'paid',
paidAt: new Date()
}], { session });
await session.commitTransaction();
console.log('注文作成完了:', order[0]._id);
} catch (err) {
await session.abortTransaction();
console.error('トランザクション失敗:', err.message);
} finally {
session.endSession();
}
出力:
TEXT 📖 参照専用注文作成完了: 67890abcdef12345 // または失敗時: トランザクション失敗: PHONE-001の在庫不足
❓ よくある質問
📖 まとめ
- MongoDB 4.0+ マルチドキュメントトランザクションはレプリカセットが必要
- session.startTransaction / commit / abort
- ACID特性:原子性 / 整合性 / 分離性 / 永続性
- 三位一体:readConcern / writeConcern / readPreference
- 因果整合性(Causal Consistency)
- Mongooseトランザクションラッパー
📝 練習問題
- 基礎問題(⭐): mongoshを使用して送金トランザクションを実装(try-catchロールバック付き)。
- 基礎問題(⭐): Mongooseで
withTransactionユーティリティ関数をカプセル化。 - 応用問題(⭐⭐): 注文トランザクションを実装(注文確定 + 在庫減少 + 注文作成 + カートクリア—全て原子的)。
- 応用問題(⭐⭐): 失敗時のトランザクションロールバックをテスト(意図的にエラーを投げて原子性を検証)。
- チャレンジ(⭐⭐⭐): 分散ロールバック対応の完全なECトランザクションシステム(注文 + 在庫 + ウォレット + ログ)を構築。