アトミックな操作

based on v43 (2010-12-13更新) - オリジナル

MongoDBは、 一つのドキュメント でのアトミックな操作をサポートしていますが、伝統的なロックと複雑なトランザクションを以下のいくつかの理由からサポートしていません。

  • まず、分散された環境で、ロックの情報を分散させるのは、コストが高く、そして遅いです。MongoDBの目標は、軽く速いことです。
  • 私たちはデッドロックという考えかたが嫌いです。 そういうことがない、シンプルでわかりやすいシステムにしたいです。
  • 私たちは、MongoDBを、リアルタイムな問題に対して、よく動くようにしたいです。オペレーションが大量のデータをロックしてしまうと、長い時間、軽い小さなクエリーを止めてしまうことがあります。("リアルタイム"に関しては、MongoDBはまだ完璧だとは言えません。しかし、ロックがあると、それをさらに難しくします。)

MongoDBは、一つのドキュメントをアトミックに操作するための、下記に示すいくつかのメソッドをサポートしています。

modifierオペレーション

MongoDBのアップデートコマンドは、いくつかの modifier をサポートしています。この操作ではドキュメントのエレメントをアトミックに更新します。次のものがあります。

  • $set - 特定の値をセット
  • $unset - 特定の値をセット (バージョン1.3.0以上)
  • $inc - 値に対して指定した数でインクリメント
  • $push - 配列に値を追加
  • $pushAll - 配列に、複数の値を追加
  • $pull - 配列から値を削除
  • $pullAll - 配列から、複数の値を削除

これらのmodifierは、アトミックな実行をするのに使えます。

"値が変更されていなかったら更新"

もう一つの、atomicにアップデートをする方法として、 "値が変更されていなかったら更新" という方法があります。
これはOSの世界で、Compare and Swapと呼ばれているものです。

  1. オブジェクトを取得
  2. オブジェクトをローカルで修正
  3. "まだ変更前の値のままなら、新しい値でアップデートしろ" というリクエストを送る

このオペレーションが失敗した場合には、最初のステップからやりなおします。

例えば、一つのオブジェクトをinventoryから取得したいとします。もしそのオブジェクトが存在する場合、取得したinventoryから1引きます。次のコードは、mongoシェルを使いこれをするためのコードです。(どの言語でも同じようなファンクションがあるでしょう)

> t=db.inventory 
> s = t.findOne({sku:'abc'}) 
{"_id" : "49df4d3c9664d32c73ea865a" , "sku" : "abc" , "qty" : 30} 
> qty_old = s.qty; 
> --s.qty; 
> t.update({_id:s._id, qty:qty_old}, s); db.$cmd.findOne({getlasterror:1}); 
{"err" : , "updatedExisting" : true , "n" : 1 , "ok" : 1} // 成功 

上記の例では、skuから実際に1引けるかどうかというのを考えていません。そのため下のコードの方が優れていますが汎用性は低いです。 modifierオペレーション ($inc) を使います。一般的なアップデートに関して($incを使えないような)は、上記の"値が変更されていなかったら更新"の方法が推奨されます。

> t.update({sku:"abc",qty:{$gt:0}}, { $inc : { qty : -1 } } ) ; db.$cmd.findOne({getlasterror:1}) 
{"err" : , "updatedExisting" : true , "n" : 1 , "ok" : 1} // 成功 
> t.update({sku:"abcz",qty:{$gt:0}}, { $inc : { qty : -1 } } ) ; db.$cmd.findOne({getlasterror:1}) 
{"err" : , "updatedExisting" : false , "n" : 0 , "ok" : 1} // 失敗 

ABA問題

上記の最初の例で、"もし qty が変化していなかったらオブジェクトを更新" ということをしました。しかし、 sku が更新されてたらどうでしょう。 その変更を上書きし、消してしまいます。

この 問題 を防ぐいくつかの方法があります。基本的には、単純にそのことに気づくことです。

  1. アップデートのクエリ表現で、 _id と qty フィールドだけでなく、オブジェクト全体を使う。
  2. $set でそのフィールドをセットする。他の項目が変更されていても影響受けません。
  3. オブジェクトにバージョンを持たせ、各更新の際にインクリメントする。
  4. 可能ならば、 この処理に $ 操作を使う。

"存在していなかったらインサート"

もう一つの楽観的な並列処理として、存在しない場合だけインサートする、という方法があります。これは、条件に対してユニークインデックスを持っている場合にだけ可能です。次の例は、この方法を使って、_idを単純に増やしながらインサートする例です。

function insertObject(o) { 
    x = db.myCollection; 
    while( 1 ) { 
        // determine next _id value to try 
        var c = x.find({},{_id:1}).sort({_id:-1}).limit(1); 
        var i = c.hasNext() ? c.next()._id + 1 : 1; 
        o._id = i; 
        x.insert(o); 
        var err = db.getLastErrorObj(); 
        if( err && err.code ) { 
            if( err.code == 11000 /* dup key */ ) 
                continue; 
            else 
                print("unexpected error inserting data: " + tojson(err)); 
            } 
        break; 
    } 
} 

findしてmodify(またはremove)

findAndModifyコマンドドキュメント を参照してください。

複数のオブジェクトを同時に更新

すべての対象のドキュメントに対してmulti-updateを行なうことができます。デフォルトでは、multi-updateは他の操作が割り込むことを許します。そのためこれは真のアトミックな操作ではありません。完全にisolatedに実行するには、 $atomic をつけます。 (訳注: multi-updateとは、updateで複数のオブジェクトを更新するもの。4つ目の引数で指定する)

isolatedではない:

db.foo.update( { x : 1 } , { $inc : { y : 1 } } , false , true );

isolated:

db.foo.update( { x : 1 , $atomic : 1 } , { $inc : { y : 1 } } , false , true );
isolatedはアトミックではありません。 アトミックとは、更新時に、「すべて」か「なし」かになります。これは二つ以上のドキュメントではできません。isolatedとは、更新時に、一回の書き込みだけが発生するということを意味します。これはそれぞれの更新が、他に何も影響を受けずに完了することを意味します。

Enter labels to add to this page:
Please wait 
Looking for a label? Just start typing.

IF YOU HAVE A QUESTION, POST IT TO THE USER GROUP.

These pages are fine for comments, but for questions, your best bet will always be the MongoDB User Group.

blog comments powered by Disqus