1

各ユーザー デバイスのアプリケーション関連データを格納するためのドキュメント構造があります。同社には、利用可能なアプリケーションがいくつかありますが、それらは限られており、頻繁に変更されることはありません。そこで、埋め込みサブドキュメント配列アプローチを使用してドキュメントを設計し、シークを減らして集約パイプラインに適したものにしました。

1 つのドキュメントを定義します。

{
    _id: "device_unique_identification_code",
    device_model: "iPhone5,2",
    applications: [
        {
            app_id: "a_game",
            push_token: "edbca9078d6c0c3a9f17166bbcf5be440c8ed2c6",
            last_user_id: 132522
        },
        {
            app_id: "an_app",
            push_token: "fed949982ceac84279f22a29bdd66cc13b7750e1",
            last_user_id: 132522
        },
        {
            app_id: "yet_another_game",
            push_token: "5cbf5a2bf0db7d6d55bd454222844d37d4f351b6",
            last_user_id: 842452
        },
        {
            app_id: "yet_another_app",
            push_token: "d1b60db7d54246d55bd37f4f35d45c2284b5a2bf",
            last_user_id: 842452
        }
    ]
}

このコレクションはデバイスアプリ固有のデータのみを保存し、セッション/ユーザー関連のデータはすべて別のコレクションに保持されます。

これらのアプリケーションは非常にビジーであるため、競合状態のリスクを軽減するために、atomic コマンドを使用して何かを行う必要があります。

これが質問です。

デバイス "a" とアプリケーション "b" を指定して、1 つのアトミック コマンドでデバイス アプリの値を格納します (たとえば、push_token を保存します)。

ここにテストケースがあります。

  • デバイス「a」のドキュメントがない場合は、デバイスアプリ データで作成します。
  • デバイス「a」のドキュメントは既に存在するが、アプリ「b」は存在しない場合。新しいデバイス アプリをプッシュします。
  • デバイス「a」のドキュメントがすでに存在し、アプリ「b」がすでに存在する場合。既存のものを更新します。

upsert/addToSet/setOnInsert/etc でさまざまなクエリを試して、数日を無駄にしました。しかし、まだ手がかりはありません。

PS。私が考えた2つのオプションがあります。

  • 分離されたコレクションを使用します。これは機能しますが、シーク パフォーマンスと引き換えに、一種の RDBMS のように感じます。
  • 配列の代わりにサブドキュメントへのマップ キーとして app_id を使用します。これも機能しますが、集計パイプラインの機能が失われ、(遅い) マップ削減にフォールバックする可能性があります。

回答の説明

解決策は、バージョン フィールドで楽観的ロックを使用することです。

previousVersion = 0

while (true) {
    // find target document with current version number,
    // also create a new document with version 1 initially
    // also find whether there is an existing app_id
    // so we don't have to loop through the array
    doc = db.devices.findAndModify({
        query:{_id:"given_device_id"},
        update:{$setOnInsert:{version:1}},
        fields:{version:1,applications:{$elemMatch:{app_id:"given_app_id"}}},
        upsert:true,
        new:true})

    // prevent unexpected infinite loop
    if (previousVersion == doc['version']) {
        throw new InfiniteLoopExpectedException()
    }

    previousVersion = doc['version']

    if (doc contains applications) {
        // if document contains the target application
        // update it using $ positioning because I am too lazy to find the index
        result = db.devices.update(
            {
                _id:"given_device_id",
                version:doc['version'],
                "applications.app_id":"given_app_id"
            }, 
            {
                $inc:{version:1},
                $set:{"applications.$.push_token":"given_value"}
            })
    } else {
        // no app_id found ? simply push
        result = db.devices.update(
            {_id:"given_device_id",version:doc['version']},
            {
                $inc:{version:1},
                $push:{applications:{app_id:"given_app_id",push_token:"given_value"}}
            })
    }

    // if the update command failed, retry the process again
    if (result['nModified'] == 1) {
        break
    }
}
4

1 に答える 1