第8章 ロックおよび同時実行
Data Grid は、マルチバージョン同時実行制御 (MVCC) を利用します (MVCC)。これは、リレーショナルデータベースや他のデータストアでよく使用される同時実行スキームです。MVCC には、粗粒度の Java 同期や、共有データにアクセスするための JDK ロックに比べて、次のような多くの利点があります。
- 同時リーダーとライターの許可
- リーダーとライターが互いにブロックしない
- 書き込みスキューを検出して処理できる
- 内部ロックのストライピングが可能
8.1. 実装の詳細のロック
Data Grid の MVCC 実装では、ロックと同期が最小限に抑えられており、可能な限り compare-and-swap などのロックフリー技術やロックフリーのデータ構造などに重点を置いています。これにより、マルチ CPU 環境とマルチコア環境の最適化に役立ちます。
特に、Data Grid の MVCC 実装はリーダーに対して高度に最適化されています。リーダースレッドは、エントリーの明示的なロックを取得せず、代わりに問題のエントリーを直接読み込みます。
一方、ライターは、書き込みロックを取得する必要があります。これにより、エントリーごとに 1 つの同時書き込みのみが保証されるため、同時ライターはキューイングしてエントリーを変更することになります。
同時読み取りを可能にするため、ライターはエントリーを MVCCEntry
でラップして、変更する予定のエントリーのコピーを作成します。このコピーは、同時リーダーが部分的に変更された状態を認識できないようにします。書き込みが完了したら、MVCCEntry.commit()
はデータコンテナーへの変更をフラッシュし、後続のリーダーに変更内容が反映されます。
8.1.1. クラスター化されたキャッシュでの仕組み
クラスター化されたキャッシュでは、各キーにキーをロックするノードがあります。このノードはプライマリー所有者と呼ばれます。
8.1.1.1. 非トランザクションキャッシュ
- 書き込み操作はキーのプライマリー所有者に送信されます。
プライマリー所有者はキーをロックしようとします。
- 成功すると、操作が他の所有者に転送されます。
- そうでない場合は、例外が発生します。
操作が条件付きで、プライマリーオーナーで失敗した場合、他のオーナーには転送されません。
操作がプライマリー所有者でローカルに実行される場合、最初のステップはスキップされます。
8.1.2. トランザクションキャッシュ
トランザクションキャッシュは、楽観的および悲観的ロックモードをサポートします。詳細は、トランザクションのロックを参照してください。
8.1.3. 分離レベル
分離レベルは、他のトランザクションと同時に実行されているときに読み取ることができるトランザクションに影響します。詳細は、分離レベルを参照してください。
8.1.4. LockManager
LockManager
は、書き込み用にエントリーをロックするコンポーネントです。LockManager
は、LockContainer
を使用して、ロックを検索、保持、作成します。LockContainers
には、ロックストライピングをサポートするものと、エントリーごとに 1 つのロックをサポートするものの 2 つの大まかな特徴があります。
8.1.5. ロックストライピング
ロックストライピングでは、固定サイズの共有ロックコレクションをキャッシュ全体に使用する必要があり、ロックはエントリーのキーのハッシュコードに基づいてエントリーに割り当てられます。JDK の ConcurrentHashMap
がロックを割り当てる方法と同様に、これにより、関連性のない可能性のあるエントリーが同じロックによってブロックされる代わりに、拡張性の高い固定オーバーヘッドのロックメカニズムが可能になります。
別の方法は、ロックストライピングを無効にすることです。これは、エントリーごとに 新しい ロックが作成されることを意味します。このアプローチでは、スループットが高くなる 可能性 がありますが、追加のメモリー使用量やガベージコレクションのチャーンなどのコストがかかります。
異なるキーのロックが同じロックストライプになってしまうとデッドロックが発生する可能性があるため、ロックストライピングはデフォルトで無効になっています。
ロックストライピングで使用される共有ロックコレクションのサイズは、<locking />
設定要素の concurrencyLevel
属性を使用して調整できます。
設定例:
<locking striping="false|true"/>
または、以下を実行します。
new ConfigurationBuilder().locking().useLockStriping(false|true);
8.1.6. 同時実行レベル
この同時実行レベルは、ストライプロックコンテナーのサイズを決定する他に、DataContainer
の内部など、関連する JDK ConcurrentHashMap
ベースのコレクションを調整するためにも使用されます。このパラメーターは、Data Grid でもまったく同じ方法で使用されているため、同時実行レベルの詳細については、JDK ConcurrentHashMap
Javadocs を参照してください。
設定例:
<locking concurrency-level="32"/>
または、以下を実行します。
new ConfigurationBuilder().locking().concurrencyLevel(32);
8.1.7. ロックタイムアウト
ロックタイムアウトは、競合するロックを待つ時間 (ミリ秒単位) を指定します。
設定例:
<locking acquire-timeout="10000"/>
または、以下を実行します。
new ConfigurationBuilder().locking().lockAcquisitionTimeout(10000); //alternatively new ConfigurationBuilder().locking().lockAcquisitionTimeout(10, TimeUnit.SECONDS);
8.1.8. 一貫性
(すべての所有者がロックされているのとは対照的に) 単一の所有者がロックされるという事実により、次の一貫性の保証が失われることはありません。キー K
がノード {A, B}
に対してハッシュ化され、トランザクション TX1
が、たとえば、A
上の K
のロックを取得したとします。別のトランザクション TX2
が B
(またはその他のノード) 上で開始され、TX2
が K
のロックを試みる場合、ロックがすでに TX1
によって保持されているため、タイムアウトでロックに失敗します。理由は、キー K
のロックがトランザクションの発生場所に関係なく、常に、確定的に、クラスターの同じノードで取得されるからです。