トピックXに興味のあるユーザは誰か?といった逆引きを行うには、シンプルなリストを格納するいうのが一般的な解決策です。例えば、ドキュメント"topic-X"には次のようなリストを保存します:
user-1234,user-222,user-987,
このようなリストはCouchbaseのドキュメントの先頭および末尾に対し、"user-XXXX"のようなデータを追記することよって簡単に実現できます。
この例ではカンマ区切りのリストとなっていますが、区切り文字は任意に選択可能です。
ユーザーのトピックに対する興味を登録する際には、上記の方法は動作しますが、ユーザが関心を解除したいとき(例えば、退会またはフォローをやめる)にはどのように扱えばいいでしょうか?
一つのアプローチは、アトミックな更新を行うCAS識別子を使用することです。クライアントアプリケーションはまず、トピックに対応するリストに対して、CAS付きGETを実行します(asciiプロトコルの"gets"リクエスト)。そしてクライアントは指定のユーザをリストから削除し、最終的に先ほどの"gets"で返却されたCAS識別子を利用して、CAS識別子付きSETを実行します(asciiプロトコルの"cas"リクエスト)。
CAS識別子付きSETが成功した場合、クライアントは新しい、関連要素を削除した後のリストで、正常にアイテムを書き換えたことになります。
最初のクライアントが削除を試みている間に、他のクライアントがリストを更新していた場合、CAS識別子付きSET操作は失敗します。この場合、最初のクライアントは、リスト項目の削除操作のリトライを試みることができます。
しかし、競合性の高い、高速なリスト更新の状況下(ユーザが人気のユーザやトピックをフォローしようとしている)では、削除するクライアントが処理を完了するのは困難になるでしょう。このような状況に対処するアプローチを次に説明します。
CAS識別子付きSETを利用したアイテムの削除を実行する代わりに、削除済みアイテムを明示的に記録する方法があります。これは削除を示す"墓標"となる文字を利用し、アイテムの追加を末尾にし、アイテムの削除をリストの先頭に対して追記することで実現できます。例えば、"|"の前に現れたアイテムは削除済みとすることができます:
user-222,|user-1234,user-222,user-987,
クライアントライブラリがこのようなリストを取得し、後処理を行えば、実際に有効なユーザはuser-1234とuser-987となります。
user-222を再度リストに追加した場合(そして何らかの理由でユーザのクリックが重複クリックを防ぐためのアプリケーションロジックより高速な場合)に、正しくカウントするには注意が必要です。
user-222,|user-1234,user-222,user-987,user-222
シンプルなエンコーディング方法は"+"と"-"記号を区切り文字として使うことです。この場合、クライアントはリストへの要素の追加には"+ID"、要素の削除には"-ID"をそれぞれ追記します。この場合もクライアントアプリケーションは正しい要素のカウントを行うために、後処理を行う必要があります。いずれの方法でも、デリミタとして利用する文字をデータに含めないように注意しなければなりません:
+1234+222+987-222
削除済みアイテムを別のリストに保存する方法もあります。すなわち、1つのトピックに対し、"follow-X"と"unfollow-X"の2つのリストを保持するということです。
最終的に、アプリケーションは不要な要素を除去したり、リストを圧縮する必要があります。これを実行するために、クライアントアプリケーションは他のリクエストに便乗する形でリストを取得できます。
しかしまた、競合し頻繁に更新されるリストでは、圧縮を試みてもCAS識別子付きSETと同様に失敗するでしょう。解決策としては多くのソフトウェアエンジニアリングと同様に、間接参照のレベルを導入します。例えば、各トピックで2つのリストを保持し、マーカーアイテムを使ってどちらのリストがアクティブかをクライアントに伝えることができます。
topic-X.a => +1234+222+987-222 topic-X.b => (empty) topic-X.active => topic-X.a
クライアントはtopic-X.aとtopic-X.bに対してmilti-GETを実行し、結果を結合して完全なリストとします。リストを変更する際は、クライアントはtopic-X.activeの"ポインタ"を参照し、topic-X.aに追記すればよいと分かります。
リストのサイズが大きくなった場合、クライアントがランダムにこれを自己解決する方法として、圧縮版のtopic-X.aをtopic-X.bに書き込み、topic-X.activeアイテムのポインタを"b"に切り替えることで、アクティブリストのガベージコレクションをすることができます(この操作をXXXとします)。新しいクライアントはtopic-X.bに書込みを開始します。依然として、並列実行環境における他のクライアントは以前アクティブだったtopic-X.aのアイテムに追記する可能性があります。その際、クライアントは、次の切り替えに備えて、topic-X.aを空にするために、topic-X.aからtopic-X.bへの圧縮を継続することを選択することもできます。
"topic-X.active"のポインタアイテムを切り離す方法以外に、墓標マーカを非アクティブなリストアイテムの先頭に挿入する方法があります。例えば、"^"が墓標の文字だった場合、同時接続注の全てのクライアントはそのリストに追記するべきではないと判断できます:
topic-X.a => +1234+222+987-222 topic-X.b => ^+1234
この"アクティブリストの切り替え"手法には同時並列実行時の不具合があります。前述の"XXX"のステップが失敗したクライアントプロセスが存在する場合、ある期間で重複や再出現するリストアイテムが発生することがあります。
一般的に、これらの自己解決する独立したクライアントは結果的に安定した状態となるように処理するアイデアです。一時的に不整合が発生する可能性があることを考慮してください。
リストが巨大になった場合(例えば、あるユーザのフォロワーが20万人)、すぐにCouchbaseのデフォルトのアイテムサイズ上限である1メガバイトに達してしまうでしょう。ここでも、リストのリストを持つ間接参照のレベルは有効です:
topic-X => +0+1 topic-X.0 => ... many actual items ... topic-X.1 => ... more actual items ...
"topic-X"アイテムは実際のリストを持つアイテムへのポインタ一覧だけを持ちます。
このアプローチではクライアント自身が新規のトピックサブリスト(topic-X.N)の作成を決定し、更新した情報を"インデックス"アイテム(topic-X)に追記します。
加えて、古くなったサブリストを圧縮することもできるでしょう。