はじめに
ここでは、NoSQLドキュメント指向データベースCouchbase Serverの機能について解説しています
Couchbase Serverでは、ドキュメントの同時変更に対するアプローチにおいて複数の手法が提供されています。
Couchbase Serverの存在意義、機能詳細、利用方法等については、拙著NoSQLドキュメント指向データベースCouchbase Serverファーストステップガイド(インプレスR&D刊)や、NoSQL/JSONデータベースCouchbase Server理解・活用へのロードマップにまとめてある記事をご参考ください。
ドキュメント同時変更
Couchbase Serverでは、一つのドキュメントに対して同時に変更が行われた場合の処理方法を制御できます。これは、一部の変更が他のクライアントによって行われた変更によって誤って失われたり上書きされたりする可能性のある潜在的な競合状態を回避するのに役立ちます。
CAS
CASは、アイテムの現在の状態を表す値です。CASはCompareAnd Swapの頭字語であり、楽観的ロックの形式として知られています。アイテムが変更されるたびに、そのアイテムのCAS値が変更されます。
CAS値は、ドキュメントにアクセスするたびにドキュメントのメタデータの一部として返されます。
このCAS値を、そのドキュメントの変更および削除操作時のパラメーターとして用いることができます。
このように、アプリケーションが操作リクエストと共にCAS値を提供する場合、サーバーはアプリケーションのCAS値を、サーバー上のドキュメントのCAS値と照合します。
- 2つのCAS値が一致する場合、ミューテーション操作は成功します。
- 2つのCAS値が異なる場合、ミューテーション操作は失敗します。
これがCAS値の単純な概念です。これをどのように用いるかは、この後で説明します。
ユースケース
CASを使用する典型的なユースケースは、既存のドキュメントに新しいフィールドを追加する場合です。あるいは、ユーザーに対してドキュメントのデータの一部のみが示される場合も、同様に考えることができるかもしれません。
つまり、ドキュメントの値が全てユーザーに対して示されている場合、例えそのユーザーは一部の値のみを変更したとしても、そのデータ全体を現在の適正なデータであることを意図していると言えます。この場合、ドキュメント操作の原子性の原則が適用され、変更の権限を持っているユーザーによる最新の変更が、その時点での適正な値であることになります。
(CAS値は、あくまで単一のドキュメントの変更に係る概念であり、複数ドキュメントに跨がるトランザクションとは異なることにご留意ください)
CASを用いず単純に同じドキュメントを同時に更新すると、意図せずに一部の更新が失われる可能性があることを示しました。これは、Couchbase Server自体が更新を失ったためではなく、アプリケーションがドキュメントに加えられた新しい変更を認識せずに、それらを上書きすることから生じます。
CASエラーの処理
現在のクライアントによって実行された最後の操作以降にアイテムのCASが変更された場合(つまり、ドキュメントが別のクライアントによって変更された場合)、アプリケーションによって使用されたCASは古くなっていると見なされます。古いCASがサーバーに送信された場合、サーバーはエラーで応答し、Couchbase SDKはこのエラーをアプリケーションに返します(言語によってエラーの返し方は異なり、リターンコードあるいは例外のいずれかになります)。
このエラーの処理方法は、アプリケーションロジックによって異なりますが、先ほどの例の、アプリケーションがドキュメントに新しいプロパティを挿入したい場合、ないしドキュメント内の他のプロパティに依存せずに特定の値のみを変更したい場合、典型的な方法は、エラー発生時に改めてアイテムを取得し直し(したがって、新しいCASを取得して)、この新しいドキュメントに対して、追加ないし変更するプロパティの値を反映の上、更新を再試行することです。この処理は、再度CASエラーに遭遇する可能性があるため、一点回数再試行サイクルを繰り返すことが考えられます。
この方法の例は、次のようになります。
ここでは、若干複雑な例として、特定のプロパティの値を増分するというユースケースを用いています。
int maxRetries = 10;
for (int i = 0; i < maxRetries; i++) {
// 現在のドキュメントの内容を取得
GetResult getResult = collection.get("document_10");
// 特定の値を増分
JsonObject content = getResult.contentAsObject();
content.put("id", content.getLong("count") + 1);
try {
// CASとともに、ドキュメントの置換を試行
collection.replace("airline_10", content, replaceOptions().cas(getResult.cas()));
break;
} catch (CasMismatchException ex) {
// CASミスマッチによって処理中断せず再試行実施
}
}
たとえば、プロパティが別のプロパティと相互に排他的である場合など(例えばどちらか一方のプロパティのみの存在が許されているが、両方同時の存在は許されない等)、更新を実行するときに、より多くのロジックが必要になることがあります。
パフォーマンス影響
CAS値は、利用しない場合であっても操作ごとに常にサーバーから返されるため、CAS操作には追加のオーバーヘッドは発生しません。
CAS値の形式
CAS値は、サーバー側での比較のために変更処理時のパラメーターとして使うためだけに用いられるものであり、アプリケーションがその値について何らかの仮定の元、比較などに用いることはできません(たとえば、単純なカウンター値であると仮定するのは誤りです)。
悲観的なロック
CASは同時実行制御を実行するための楽観的ロックの手法ですが、Couchbase Serverは明示的な悲観的ロックも提供します。ドキュメントがロックされている場合、正しいCASを指定せずにドキュメントを変更しようとすると失敗します。
ドキュメントは、「get-and-lock」操作を使用してロックし、ロック解除操作を使用して明示的にロック解除するか、有効なCASでドキュメントを変更することによって暗黙的にロック解除し変更を実施することができます。ドキュメントがロックされている間は、ドキュメントを取得することはできますが、正しいCAS値を使用せずに変更することはできません。ロックされたドキュメントが取得されると、サーバーは無効なCAS値を返し、そのドキュメントの変更を防ぎます。不適切なCASによるドキュメント変更は、CasMismatchエラーを引き起こします。
また、ロックの解除(「unlock」操作)の際も、適切なCASが必要になります。不適切なCASによる「unlock」操作は、Lockedエラーを引き起こします。
ロック期間
ドキュメントのロックは、一定期間維持されます。その期間が過ぎると、サーバーはドキュメントのロックを解除します。これは、不正なアプリケーションが誤ってドキュメントへのアクセスをブロックするのを防ぐためです。
デフォルト値は、15秒です。このロックが保持される時間は変更することができますが、30秒を超えることはできません。30秒を超えるロックを設定すると、Couchbase Serverはロック期間をサーバーの デフォルト値である15秒に設定します。
ドキュメントをロックするときは、 CASの扱いを考慮する必要があります。これは、ドキュメントを変更するときに必要になります。
GetResult getAndLockResult = collection.getAndLock("airline_1191", Duration.ofSeconds(2));
long lockedCas = getAndLockResult.cas();
collection.replace("airline_1191", "new value", replaceOptions().cas(lockedCas));
変更せずにアンロックする場合は、以下のようになります。この場合も、CASを用いる必要があります。
collection.unlock("key",lockedCas);
Couchbase Serverは、明示的なロック解除操作(unlock)、または正しいCASによる変更によって暗黙的に、ドキュメントのロックを解除します。
ロックしようとした際に、アイテムがすでにロックされている場合、サーバーはCasMismatch
エラーで応答します。CasMismatch
エラーは操作を一時的に実行できなかったことを意味しますが、再試行により成功する可能性があります。
関連情報