[和訳] 2相コミットを行う (MongoDB 2.6 CRUD Tutorials)


MongoDB 2.6マニュアルの[MongoDB CRUD Tutorials](http://docs.mongodb.org/manual/applications/crud/)から[Perform Two Phase Commits](http://docs.mongodb.org/manual/tutorial/perform-two-phase-commits/)を和訳してみました。

----

# 2相コミットを行う

## 概要

本文書は複数ドキュメント[^1]更新や"複数ドキュメントトランザクション"のための、2相コミット方法を用いるパターンを提供する。さらにあなたはこの方法をロールバック風の機能をもたらすためにこの処理を拡張することもできる。

[^1]: 本文書ではmongodbのdocumentをドキュメントと表記する。

## 背景

MongoDBデータベースでは単一ドキュメントへの操作は常に原子的である。しかしながら複数ドキュメントへの操作、しばしば"複数ドキュメントトランザクション"と言われる操作は原子的でない。ドキュメントはかなり複雑で複数の"入れ子になった"ドキュメントを含むことができるので、単一ドキュメントの原子性は多くの実務的なユースケースに必要なサポートを提供している。

とはいえ単一ドキュメントへの原子的操作の威力にも関わらず、複数ドキュメントトランザクションを要する場合が存在する。逐次的な操作からなるトランザクションを実行するとき、次のような課題がある:

* 原子性: ある操作が失敗したら、トランザクション内のそれまでの操作も元の状態に「ロールバック」しなければならない。
* 一貫性: 重要な失敗（すなわちネットワークやハードウェア）がトランザクションを中断した場合、データベースが一貫性を保った状態で復元できなければならない。

複数ドキュメントトランザクションを要する状況のために、あなたはアプリケーションで2相コミットを実装することができる。2相コミットを用いることでデータの一貫性と、エラーが生じた場合はトランザクションの前の状態に復帰させることを保証する。この手続きの間、ドキュメントは未決定の状態として表現することができる。


> MongoDBでは単一ドキュメント操作は原子的であるため、2相コミットはトランザクション風の意味論を可能にするだけである。アプリケーションは2相コミットやロールバックの間、中間点の状態に戻ることが可能である。

## パターン

### 概観

口座Aから口座Bに送金するシナリオを考える。リレーショナルデータベースであれば、Aから預金を減らし、Bに預金を加えることを単一の複数状態トランザクションで行うことができる。MongoDBでは同様の結果を得るような2相コミットをエミュレートできる。

このチュートリアルの例では以下の２つのコレクションを用いる:

1. 口座情報を保持する __accounts__ コレクション
2. 送金トランザクションを保持する __transactions__ コレクション

### 送金元口座と送金先口座を初期化する

__accounts__ コレクションに口座Aと口座Bのドキュメントを挿入する。


```javascript
db.accounts.insert(
	[
		{ _id: "A", balance: 1000, pendingTransactions: [] },
		{ _id: "B", balance: 1000, pendingTransactions: [] }
	]
)
```

この操作は操作の状態を表す[BulkWriteResult()](http://docs.mongodb.org/manual/reference/method/BulkWriteResult/#BulkWriteResult)オブジェクトを返す。挿入に成功すると[BulkWriteResult()](http://docs.mongodb.org/manual/reference/method/BulkWriteResult/#BulkWriteResult)の[nInserted](http://docs.mongodb.org/manual/reference/method/BulkWriteResult/#BulkWriteResult.nInserted)が2になる。

### 送金記録を初期化する

送金を実施するごとに、 __transactions__ コレクションに送金情報を挿入する。このドキュメントには次のフィールドが含まれる:

* __source__ と __destination__ フィールドには送金元と送金先の口座への参照として __accounts__ コレクションの口座情報の **_id** フィールドが入る。
* __value__ フィールドには送金額が入る。
* __state__ フィールドには送金の現在の状態が入る。取りうる値は __initial__, __pending__, __applied__, __done__, __canceling__ と __canceled__ である。
* __lastModified__ フィールドには最終変更日時が入る。


```javascript
db.transactions.insert(
	{ _id: 1, source: "A", destination: "B", value: 100, state: "initial", lastModified: new Date() }
)
```

この操作は操作の状態を表す[WriteResult()](http://docs.mongodb.org/manual/reference/method/WriteResult/#WriteResult)を返す。挿入に成功すると[WriteResult()](http://docs.mongodb.org/manual/reference/method/WriteResult/#WriteResult)の[nInserted](http://docs.mongodb.org/manual/reference/method/WriteResult/#WriteResult.nInserted)が1になる。

### 口座間の送金を2相コミットを用いて行う

#### 1. 開始するトランザクションを取り出す

__transactions__ コレクションから __initial__ 状態のトランザクションを見つける。現在 __transactions__ コレクションには[送金記録を初期化する]()で追加したドキュメントがひとつあるだけである。もしそのコレクションに複数のドキュメントが追加されていたら、さらに条件を追加しない限り __initial__ 状態の他のトランザクションも返る。


```javascript
var t = db.transactions.findOne( { state: "initial" } )
```

mongoシェルで __t__ をタイプしてその変数内のコンテンツを表示してみよう。これにより次のようにドキュメントが表示されるが __lastModified__ フィールドはあなたが挿入した時刻が反映される。

```javascript
{ "_id" : 1, "source" : "A", "destination" : "B", "value" : 100, "state" : "initial", "lastModified" : ISODate("2014-07-11T20:39:26.345Z") }
```

#### 2. トランザクション状態をPendingにする

トランザクションの __state__ を __initial__ から __pending__ にセットし、[$currentDate](http://docs.mongodb.org/manual/reference/operator/update/currentDate/#up._S_currentDate)オペレーターを用いて __lastModified__ フィールドに現在時刻をセットする。

```javascript
db.transactions.update(
    { _id: t._id, state: "initial" },
    {
      $set: { state: "pending" },
      $currentDate: { lastModified: true }
    }
)
```

この操作は操作の状態を表す[WriteResult()](http://docs.mongodb.org/manual/reference/method/WriteResult/#WriteResult)を返す。挿入に成功すると[nMatched](http://docs.mongodb.org/manual/reference/method/WriteResult/#WriteResult.nMatched)と[nModified](http://docs.mongodb.org/manual/reference/method/WriteResult/#WriteResult.nModified)が1になる。

update文の __state: "initial"__ 条件は他のプロセスがこの記録を更新しないことを確実にする。もし[nMatched](http://docs.mongodb.org/manual/reference/method/WriteResult/#WriteResult.nMatched)と[nModified](http://docs.mongodb.org/manual/reference/method/WriteResult/#WriteResult.nModified)が0なら、最初のステップに戻り別のトランザクションを取り出し再度ここまでの処理を実行する。


#### 3. トランザクションを両口座に適用する

トランザクション __t__ を両口座に[update()](http://docs.mongodb.org/manual/reference/method/db.collection.update/#db.collection.update)メソッドを用いて適用する。本ステップを複数回実行するような場合にトランザクションが再度適用されないよう更新条件に __pendingTransactions : { $ne : t._id }__ を入れる。

トランザクションを口座に適用することで、口座の __balance__ と __pendingTransactions__ フィールドが更新される。

送金元口座を更新する。 __balance__ からトランザクションの __value__ を減らし __pendingTransactions__ 配列にトランザクションの **_id** を追加する。


```javascript
db.accounts.update(
   { _id: t.source, pendingTransactions: { $ne: t._id } },
   { $inc: { balance: -t.value }, $push: { pendingTransactions: t._id } }
)
```

更新が成功すると、このメソッドは[WriteResult()](http://docs.mongodb.org/manual/reference/method/WriteResult/#WriteResult)を返し[nMatched](http://docs.mongodb.org/manual/reference/method/WriteResult/#WriteResult.nMatched)と[nModified](http://docs.mongodb.org/manual/reference/method/WriteResult/#WriteResult.nModified)が1になる。

送金先口座を更新する。 __balance__ にトランザクションの __value__ を加え __pendingTransactions__ 配列にトランザクションの **_id** を追加する。

```javascript
db.accounts.update(
   { _id: t.destination, pendingTransactions: { $ne: t._id } },
   { $inc: { balance: t.value }, $push: { pendingTransactions: t._id } }
)
```

更新が成功すると、このメソッドは[WriteResult()](http://docs.mongodb.org/manual/reference/method/WriteResult/#WriteResult)を返し[nMatched](http://docs.mongodb.org/manual/reference/method/WriteResult/#WriteResult.nMatched)と[nModified](http://docs.mongodb.org/manual/reference/method/WriteResult/#WriteResult.nModified)が1になる。

#### 4. トランザクション状態をAppliedにする

以下の[update()](http://docs.mongodb.org/manual/reference/method/db.collection.update/#db.collection.update)操作でトランザクションの __state__ を __applied__ にセットし、 __lastModified__ フィールドを更新する。

```javascript
db.transactions.update(
   { _id: t._id, state: "pending" },
   {
     $set: { state: "applied" },
     $currentDate: { lastModified: true }
   }
)
```

更新が成功すると、このメソッドは[WriteResult()](http://docs.mongodb.org/manual/reference/method/WriteResult/#WriteResult)を返し[nMatched](http://docs.mongodb.org/manual/reference/method/WriteResult/#WriteResult.nMatched)と[nModified](http://docs.mongodb.org/manual/reference/method/WriteResult/#WriteResult.nModified)が1になる。

#### 5. 両口座のPendingトランザクションリストを更新する

適用されたトランザクションの **_id** を両口座の __pendingTransactions__ から削除する。

送金元口座を更新する。

```javascript
db.accounts.update(
   { _id: t.source, pendingTransactions: t._id },
   { $pull: { pendingTransactions: t._id } }
)
```

更新が成功すると、このメソッドは[WriteResult()](http://docs.mongodb.org/manual/reference/method/WriteResult/#WriteResult)を返し[nMatched](http://docs.mongodb.org/manual/reference/method/WriteResult/#WriteResult.nMatched)と[nModified](http://docs.mongodb.org/manual/reference/method/WriteResult/#WriteResult.nModified)が1になる。

送金先口座を更新する。

```javascript
db.accounts.update(
   { _id: t.destination, pendingTransactions: t._id },
   { $pull: { pendingTransactions: t._id } }
)
```

更新が成功すると、このメソッドは[WriteResult()](http://docs.mongodb.org/manual/reference/method/WriteResult/#WriteResult)を返し[nMatched](http://docs.mongodb.org/manual/reference/method/WriteResult/#WriteResult.nMatched)と[nModified](http://docs.mongodb.org/manual/reference/method/WriteResult/#WriteResult.nModified)が1になる。

#### 6. トランザクション状態をDoneにする

トランザクションを完了する。 __state__ を __done__ にし、 __lastModified__ を更新する:

```javascript
db.transactions.update(
   { _id: t._id, state: "applied" },
   {
     $set: { state: "done" },
     $currentDate: { lastModified: true }
   }
)
```

更新が成功すると、このメソッドは[WriteResult()](http://docs.mongodb.org/manual/reference/method/WriteResult/#WriteResult)を返し[nMatched](http://docs.mongodb.org/manual/reference/method/WriteResult/#WriteResult.nMatched)と[nModified](http://docs.mongodb.org/manual/reference/method/WriteResult/#WriteResult.nModified)が1になる。


## 失敗シナリオからの回復

トランザクション手続きの最も重要なパートは上述の例ではなく、むしろトランザクションが成功裏に完了しなかった時の様々な失敗シナリオからの回復の可能性にある。本節ではありうる失敗の概観と回復の手順を示す。

### 回復操作

2相コミットはトランザクションを再開し一貫性のある状態に到達することを可能にする。未完了のトランザクションをとらえるために、回復操作をアプリケーション開始時や一定間隔で実行する。

一貫性のある状態に到達するために要する時間はアプリケーションが各トランザクションの回復に要するのにどの程度かかるかに依存する。

以下の回復手続きでは __lastModified__ の日時をトランザクションの回復が必要かどうかを示すインジケーターとして用いている; 特にpendingやapplied状態のトランザクションが30分更新されていなかった場合、回復が必要と判断する。あなたは異なる条件でこの判断を行うこともできる。


#### Pending状態のトランザクション

"2. トランザクション状態をPendingにする"ステップのあと、"4. トランザクション状態をAppliedにする"ステップの前までで生じた失敗から回復するためには、 __transactions__ コレクションからpending状態のトランザクションを取得する。

```javascript
var dateThreshold = new Date();
dateThreshold.setMinutes(dateThreshold.getMinutes() - 30);

var t = db.transactions.findOne( { state: "pending", lastModified: { $lt: dateThreshold } } );
```

そして"3. トランザクションを両口座に適用する"ステップから再開する。

#### Applied状態のトランザクション

"4. トランザクション状態をAppliedにする"ステップのあと、"6. トランザクション状態をDoneにする"ステップの前までで生じた失敗から回復するためには、 __transactions__ コレクションからapplied状態のトランザクションを取得する。

```javascript
var dateThreshold = new Date();
dateThreshold.setMinutes(dateThreshold.getMinutes() - 30);

var t = db.transactions.findOne( { state: "applied", lastModified: { $lt: dateThreshold } } );
```

そして"5. 両口座のPendingトランザクションリストを更新する"ステップから再開する。

### ロールバック操作

いくつかの場合では、あなたは"ロールバック"またはトランザクションを取り消す必要がある; アプリケーションに"キャンセル"機能が必要である、あるいはトランザクションの口座のなかに存在しないまたは停止しているものがある場合である。

#### Applied状態のトランザクション

"4. トランザクション状態をAppliedにする"ステップの後では、あなたはトランザクションをロールバックすべきで __ない__ 。その代わりにトランザクションを完遂させ、新しく逆のトランザクションを送金元と送金先を逆にすることで作成する。

#### Pending状態のトランザクション

"2. トランザクション状態をPendingにする"ステップの後から"4. トランザクション状態をAppliedにする"ステップの前までは、あなたはトランザクションを次の手続きでロールバックできる:

#### 1. トランザクション状態をcancelingにする

トランザクションの __state__ を __pending__ から __canceling__ に更新する。

```javascript
db.transactions.update(
   { _id: t._id, state: "pending" },
   {
     $set: { state: "canceling" },
     $currentDate: { lastModified: true }
   }
)
```

更新が成功すると、このメソッドは[WriteResult()](http://docs.mongodb.org/manual/reference/method/WriteResult/#WriteResult)を返し[nMatched](http://docs.mongodb.org/manual/reference/method/WriteResult/#WriteResult.nMatched)と[nModified](http://docs.mongodb.org/manual/reference/method/WriteResult/#WriteResult.nModified)が1になる。

#### 2. 両口座でトランザクションを取り消す

両口座のトランザクションを取り消すには、既に適用されたトランザクション __t__ を逆適用する。既にトランザクションが適用された口座だけを更新するよう、更新条件に __pendingTransactions : t._id__ を入れる。

送金先口座を更新する。送金先口座の __balance__ からトランザクションの __value__ を減らし、トランザクションの **_id** を口座の __pendingTransactions__ 配列から取り除く。

```javascript
db.accounts.update(
   { _id: t.destination, pendingTransactions: t._id },
   {
     $inc: { balance: -t.value },
     $pull: { pendingTransactions: t._id }
   }
)
```

更新が成功すると、このメソッドは[WriteResult()](http://docs.mongodb.org/manual/reference/method/WriteResult/#WriteResult)を返し[nMatched](http://docs.mongodb.org/manual/reference/method/WriteResult/#WriteResult.nMatched)と[nModified](http://docs.mongodb.org/manual/reference/method/WriteResult/#WriteResult.nModified)が1になる。もし過去にこの口座にトランザクションが適用されていなければ、この更新条件にマッチするドキュメントはなく、[nMatched](http://docs.mongodb.org/manual/reference/method/WriteResult/#WriteResult.nMatched)と[nModified](http://docs.mongodb.org/manual/reference/method/WriteResult/#WriteResult.nModified)は0になる。

送金元口座を更新する。送金元口座の __balance__ にトランザクションの __value__ を加え、トランザクションの **_id** を口座の __pendingTransactions__ 配列から取り除く。

```javascript
db.accounts.update(
   { _id: t.source, pendingTransactions: t._id },
   {
     $inc: { balance: t.value},
     $pull: { pendingTransactions: t._id }
   }
)
```

更新が成功すると、このメソッドは[WriteResult()](http://docs.mongodb.org/manual/reference/method/WriteResult/#WriteResult)を返し[nMatched](http://docs.mongodb.org/manual/reference/method/WriteResult/#WriteResult.nMatched)と[nModified](http://docs.mongodb.org/manual/reference/method/WriteResult/#WriteResult.nModified)が1になる。もし過去にこの口座にトランザクションが適用されていなければ、この更新条件にマッチするドキュメントはなく、[nMatched](http://docs.mongodb.org/manual/reference/method/WriteResult/#WriteResult.nMatched)と[nModified](http://docs.mongodb.org/manual/reference/method/WriteResult/#WriteResult.nModified)は0になる。

#### 3. トランザクション状態をcanceledにする

ロールバックを完了させるために、トランザクションの __state__ を __canceling__ から __canceled__ にする。

```javascript
db.transactions.update(
   { _id: t._id, state: "canceling" },
   {
     $set: { state: "cancelled" },
     $currentDate: { lastModified: true }
   }
)
```

更新が成功すると、このメソッドは[WriteResult()](http://docs.mongodb.org/manual/reference/method/WriteResult/#WriteResult)を返し[nMatched](http://docs.mongodb.org/manual/reference/method/WriteResult/#WriteResult.nMatched)と[nModified](http://docs.mongodb.org/manual/reference/method/WriteResult/#WriteResult.nModified)が1になる。

## 複数のアプリケーション

複数のアプリケーションが作ることが出来て、データの一貫性を壊したり衝突を起こすことなく各々平行に実行できるようなトランザクションというものも時に存在する。我々の手続きでは、トランザクションドキュメントを更新または検索するにあたり、複数アプリケーションからのトランザクションの再適用を防ぐために更新条件に __state__ フィールドを含める。

例えば、アプリケーション __App1__ と __App2__ の両方が同じ __initial__ 状態のトランザクションをつかんでいるとする。 __App1__ は __App2__ が始める前にトランザクションを適用する。 __App2__ が"2. トランザクション状態をPendingにする"ステップを実行しようとするとき、更新条件に __state : "initial"__ を含めることで何のドキュメントも得られず[nMatched](http://docs.mongodb.org/manual/reference/method/WriteResult/#WriteResult.nMatched)と[nModified](http://docs.mongodb.org/manual/reference/method/WriteResult/#WriteResult.nModified)が0になる。このことは __App2__ に異なるトランザクションで最初のステップからやり直すべきだと知らせている。

複数のアプリケーションが実行している場合、常に単一のアプリケーションだけがトランザクションを操作できることが重要である。更新条件で予期される状態に加えてどのアプリケーションがトランザクションを操作したかを示すマーカーを残すことができる。[FindAndModify()](http://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/#db.collection.findAndModify)メソッドを使い、トランザクションの取得と更新を１ステップで行う:

```javascript
t = db.transactions.findAndModify(
       {
         query: { state: "initial", application: { $exists: false } },
         update:
           {
             $set: { state: "pending", application: "App1" },
             $currentDate: { lastModified: true }
           },
         new: true
       }
    )
```

トランザクションの __application__ フィールドでマッチしたアプリケーションだけがトランザクションを適用することを保証するよう修正する。

もし __App1__ がトランザクション実行を失敗したときあなたは回復操作を行うことができるが、その際アプリケーションを適用したアプリケーションだけが行えることを保証する。例えばpending状態のトランザクションを見つけて再実行する際のクエリーは以下のようにする:

```javascript
var dateThreshold = new Date();
dateThreshold.setMinutes(dateThreshold.getMinutes() - 30);

db.transactions.find(
   {
     application: "App1",
     state: "pending",
     lastModified: { $lt: dateThreshold }
   }
)
```

## 2相コミットを実製品で用いる

本例のトランザクションはあえて単純にしている。例えば口座のロールバックは常に可能で、口座の残高が負の値を取りうることを仮定している。

実製品の実装はより複雑になるだろう。典型的には口座は現在の残高や引き落とし予定のクレジットや振込み金額の情報を必要とするだろう。

あなたのアプリケーションの要求に合わせて適切な[write Concern](http://docs.mongodb.org/manual/core/write-concern/)のレベルを使うようにしよう。


Copyright © 2011-2015 [MongoDB, Inc](http://www.mongodb.com/?_ga=1.121667851.1049854520.1421886942). Licensed under [Creative Commons](http://creativecommons.org/licenses/by-nc-sa/3.0/). MongoDB, Mongo, and the leaf logo are registered trademarks of MongoDB, Inc.