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