MongoDBのcursor

f:id:y_d:20180605222656p:plain

下記の感じでなんとなくでMongoDBのcursorを使っているけど、今までちゃんと公式ドキュメントを読んだ事がなかったので改めてドキュメントを読んでみた。

var cursor = db.users.find();
while (myCursor.hasNext()) {
   printjson(cursor.next());
}

現行バージョン:
https://docs.mongodb.com/manual/tutorial/iterate-a-cursor/

過去バージョン:
https://docs.mongodb.com/v3.0/core/cursors/

仕様概要

軽く読んだ感じ次の仕様になっているらしい。

  1. cursor.next() を使ってループする実装にしていても、実は毎回MongoDBサーバーからデータを取得している訳ではない(ネットワーク負荷がボトルネックになってパフォーマンスが出ないので)。

  2. MongoDBドライバは実は裏でMongoDBサーバーから小分けにデータを受け取っていて、 cursor.next() はその小分けのデータから1ドキュメントずつ返しているだけ。小分けで受け取っていたデータがなくなったらMongoDBサーバーに問い合わせて小分けのデータを補充する。(裏でMongoDBドライバがMongoDB Wire Protocolを使ってデータ取得やらなんやらしているが、それをMongoDBドライバが隠匿している)

  3. MongoDBドライバがどれくらい小分けにして受け取るかというと、一番最初は101個のドキュメントをまとめて取得し、それ以降は16MBずつ取得する。この挙動は batchSize()limit() を使って制御する事が可能。
    ※3.4より前のバージョンだとちょっと挙動が違う

  4. MongoDBドライバは、MongoDBサーバーから小分けにデータを貰ってくるが、MongoDBサーバーはその瞬間の最新データを返す。したがってデータの更新が重なると1回のカーソルのループで、同じレコードが複数回返ってくる事がある。
    ※insert,updateでnatural orderが変わった時に複数回返ってくると思われる
    ※ただし前述の通り、結果が101ドキュメント以下なら複数回返ってくるのは起こり得ないはず

  5. 上記の、同じレコードが複数回返ってくる問題は cursor.snapshot() を使って回避する事ができる。

という事で、cursorを使っていても最新のデータが返ってくるケースがあるっぽい(もちろん古いデータが返ってくることもある)。

個人的に注意が必要だと思ったのは下記の点。

  • cursorから返ってくるドキュメントは、 find() した瞬間のドキュメントが返ってくることもあれば、そうでないこともある
  • cursorをループしていると、同じドキュメントが複数回返ってくることがある

概念図

文章だけだとわかりづらいので図にしてみる。

+---------+   +---------+   +--------+
| MongoDB |   | MongoDB |   | Client |
| Server  |   | Driver  |   |        |
+-+-------+   +----+----+   +------+-+
  |                |               |
  |                |     find()    |
  |    OP_QUERY    <---------------+
  <----------------+               |
  | 101 documents  |               |
  +---------------->     cursor    |
  |                +--------------->
  |                |               |
  |                | cursor.next() |
  |                <---------------+
  |                |   document    |
  |                +--------------->
  |                |               |
  |                | cursor.next() |
  |                <---------------+
  |                |   document    |
  |                +--------------->
  :                :               :
        101個documentを取得する
  :                :               :
  |                | cursor.next() |
  |   OP_GET_MORE  <---------------+
  <----------------+               |
  | some documents |               |
  +---------------->   document    |
  |                +--------------->
  |                |               |
  |                | cursor.next() |
  |                <---------------+
  |                |   document    |
  |                +--------------->
  |                |               |

補足

  • MongoDBサーバーとMongoDB Driverは MongoDB Wire Protocol を使ってやり取りを行う。
  • (言語は何でもいいけど)例えばPHPからMongoDBクラスを使ってMongoDBサーバーと通信を行うとき、裏では MongoDB Wire Protocol を使っている。
  • PHPからすると、MongoDBからcursorを取得して、cursorから 1 documentずつドキュメントを受け取るように実装したとしても、実はある程度の塊でMongoDBサーバーからMongoDB Driverが複数のドキュメントを受け取っていて、MongoDB Driverがそれを 1 documentずつ返している。

参考資料