このドキュメントでは、Coherence名前付きキャッシュのコレクションでその状態を管理する、オブジェクト・モデル管理のベスト・プラクティスについて説明します。一連のエンティティ・クラスとエンティティ・リレーションシップがあることを前提に、Coherence名前付きキャッシュのコレクション間でオブジェクト・モデルを表現および管理するための最適な方法について考えます。
クラスタ化されたキャッシング・ソリューションの価値は、その使用方法に依存します。それは、アプリケーション層のデータベースのデータをキャッシングして、瞬時にアクセスできるよう準備を整えておくことでしょうか。次の段階に進んでトランザクション制御をアプリケーション層に移行することでしょうか。または、さらに段階を踏んで積極的にトランザクション制御を最適化することでしょうか。
単純なデータ・キャッシング
特に並行処理制御が必要ない場合(コンテンツのキャッシングなど)やトランザクション制御がデータベースで管理されている場合(Hibernate製品およびJDO製品のプラグイン・キャッシュなど)は、単純なデータのキャッシュが一般的です。このアプローチはアプリケーション設計への影響が最低限に抑えられ、多くの場合、オブジェクト・リレーショナル・マッピング(ORM)層またはアプリケーション・サーバー(EJBコンテナ、Springなど)で透過的に実装されます。ただし、これではデータベース・サーバーのオーバーロードの問題が完全には解決されません。特に非トランザクション読取りがアプリケーション層で処理されている場合、すべてのトランザクション・データ管理にはデータベースとの相互作用が依然として必要です。
データ・アクセス要件が主キーによる単純なアクセス以上のことを必要とする場合、キャッシングは無関係な問題ではないということを認識することが重要です。つまり、キャッシングの恩恵を確実に受けるには、キャッシングを念頭に置いてアプリケーションを設計する必要があります。
トランザクション・キャッシング
データベースを単独で操作し、独自のスケーラビリティを必要とするアプリケーションは、データ管理のより大きな役割を担うことから始める必要があります。これには、Coherenceの読取りアクセス機能(リードスルー・キャッシング、キャッシュ問合せ、集計)、データベース・トランザクションの最小化機能(ライトビハインド)、および並行処理の管理機能(ロック、キャッシュ・トランザクション)を使用することがあります。
トランザクションの最適化
フォルト・トレランス、短い待機時間、および高いスケーラビリティを組み合せる必要があるアプリケーションでは、一般に、トランザクションをより最適化する必要があります。従来のトランザクション制御では、Orderオブジェクトを管理する際、アプリケーションでSERIALIZABLE分離を指定する必要があることがありました。分散環境では、これは非常にコストのかかる操作です。分散環境以外でも、ほとんどのデータベースおよびキャッシング製品では、多くの場合、これを実現するために表ロックを使用します。そのため、使用可能なハードウェア・リソースに関係なく、スケーラビリティに大きな制限がかかります。実際に、最新のハードウェアを使用してもトランザクション率が毎秒数百トランザクションに制限される場合もあります。ただし、慣例によるロックは、たとえば、すべてのアクセッサが親Orderオブジェクトのみをロックする必要がある場合などに役立ちます。これを実行すると、ロックの有効範囲が表レベルから命令レベルに小さくなり、より高いスケーラビリティを達成できます(もちろん、アプリケーションによっては、複数のJMSキュー間でイベント処理をパーティション化して明示的な並行処理制御の必要性を排除することで、すでに同様の結果を実現しているものもあります)。さらに最適化するためには、EntryProcessorを使用してクラスタ化の調整を不要にすることなどがあります。これにより、特定のキャッシュ・エントリに対するトランザクション率を飛躍的に増加させることができます。
リレーションシップという用語は、オブジェクト同士がどのように関連し合うかを意味します。たとえば、Orderオブジェクトには、一連のLineItemオブジェクト(のみ)が含まれます。Orderオブジェクトに関連付けられたCustomerオブジェクトを指す場合もあります。
データ・アクセス層は、一般にデータ・アクセス・オブジェクト(DAO)とデータ転送オブジェクト(DTO)の2つのキー・コンポーネント(つまり動作と状態)に分けられます。DAOはデータ・アクセスの動作を制御し、通常はデータベースまたはキャッシュを管理するロジックを含みます。DTOには、DAOで使用するデータ(Orderレコードなど)が含まれます。また、(一部のアプリケーションでは)単一のオブジェクトがDTOとDAOの両方として動作するものもあります。これらの用語は使用パターンを述べています。これらのパターンはアプリケーションにより異なりますが、コアとなる原理は応用できます。このドキュメントのサンプルは、わかりやすくするために、DAO/DTOの組合せアプローチ(動作の豊富なオブジェクト・モデル)に従っています。
エンティティ・リレーションシップの管理は、特にスケーラビリティおよびトランザクション性が求められる場合、困難な作業になります。この課題のコアは、理想的な解決策として、開発者の作業を極力抑えてこれらのエンティティ間リレーションシップの複雑性を管理可能にする必要があることにあります。概念的には、(XMLやJavaソースなどのいくつかの任意の形式で表示可能な)リレーションシップ・モデルを使用して、その記述を遵守する実行時動作を提供することにあります。
現在の課題は次のいくつかのグループに分類できます。
コード生成(.java
または.class
ファイル)
実行時バイトコードの使用(ClassLoader
インターセプション)
事前定義のDAOメソッド
コード生成
コード生成は、.javaまたは.classファイルの生成を含む一般的なオプションです。このアプローチは、通常、いくつかの管理および監視、AOPおよびORMツール(AspectJ、Hibernate)で使用されます。このアプローチの主な課題はアーチファクトの生成にあり、ソフトウェア構成管理(SCM)システムでの管理が必要になることがあります。
バイトコードの使用
このアプローチでは、ClassLoaderのインターセプションを使用して、JVMへのロード時にクラスを調整します。このアプローチは、一般にはAOPツール(AspectJ、JBossCacheAop、TerraCotta)および一部のORMツール(JDOの実装では一般的)で使用されます。実行時のコード変更(アプリケーション・サーバーでのホット・デプロイ・オプションのブレーク傾向を含む)に関する(認識された、または実際の)リスクがあるため、多くの組織ではこのオプションは実行されません。そのため、これは初期オプションではありません。
開発者実装クラス
最も柔軟なオプションは、実行時問合せエンジンを使用することです。ORM製品は、ほとんどの処理をデータベース・サーバーに委任しています。また、問合せエンジンをアプリケーション層内で提供する方法もありますが、これは完全なデータベース・サーバーの管理性および保守性を制限することと同じ複雑性をもたらします。
Coherenceにおける推奨案は、DAOメソッドの綿密な計画を明示的に立てることです。これにより、開発に多少の手間を掛けた、決定性のある(動的に評価される問合せを回避する)動作が提供されます。この取組みにおける努力は、リレーションシップ・モデルの複雑さに正比例します。小規模から中規模サイズのモデル(Coherenceで管理する最大50エンティティのタイプ)の場合は、非常にわずかな開発努力で済みます。大規模な(特に複雑なリレーションシップを持つ)モデルになると、相当な開発努力を必要とすることがあります。
ベスト・プラクティスとしては、すべての状態、リレーションシップおよびアトミック・トランザクションをオブジェクト・モデルで処理してください。より高度なトランザクション制御では、並行性を調整する追加のサービス層を用意する必要があります(これにより構成可能なトランザクションを使用できます)。
構成可能なトランザクション:
Coherenceのトランザクションの詳しい使用法は、『Oracle Coherence開発者ガイド』のトランザクションの実行に関する項を参照してください。
NamedCache
には(データベース表にエンティティの1つのタイプを含めるのと同様に)1つのエンティティ・タイプを含める必要があります。これに共通の唯一の例外は、多くの場合に任意の値が含まれる、ディレクトリタイプのキャッシュです。
それぞれの追加のNamedCache
では、参加クラスタ・メンバーごとに数十バイトしか消費しません。これは、バッキング・マップに応じて異なります。透過的なデータベース統合に使用する<read_write_backing_map_scheme
>で構成されたキャッシュは、ライトビハインド・キャッシュが有効な場合はさらにリソースを消費しますが、数百の名前付きキャッシュがなければこれは要素になりません。
可能であれば、ビジネス・トランザクションが単一のキャッシュ・エントリの更新にマップするように、キャッシュ・レイアウトを設計します。これによりトランザクションの制御が簡略化され、その結果、大きなスループットが得られます。
ほとんどのキャッシュでは意味のあるキーを使用する必要があります(それに対してリレーショナル・システムでは、一般には同一性を管理するための意味のないキーが使用されます)。このデメリットの1つは問合せのサポートが制限されることです(Coherenceの問合せでは現在、エントリ・キーでなく、エントリ値にのみ適用されます)。キー属性に対して問い合せるには、値を属性に複製する必要があります。
DAOオブジェクトは、NamedCache
アクセスに関してゲッター、セッター、問合せメソッドを実装する必要があります。NamedCache
APIにより、ほとんどの操作のタイプに対してこれは非常に単純になります。特に主キー検索や単純な検索問合せでは顕著です。
例13-1 NamedCacheアクセスのメソッドの実装
public class Order implements Serializable { // static"Finder" method public static Order getOrder(OrderId orderId) { return (Order)m_cacheOrders.get(orderId); } // ... // mutator/accessor methods for updating Order attributes // ... // lazy-load an attribute public Customer getCustomer() { return (Customer)m_cacheCustomers.get(m_customerId); } // lazy-load a collection of child attributes public Collection getLineItems() { // returns map(LineItemId -> LineItem); just return the values return ((Map)m_cacheLineItems.getAll(m_lineItemIds)).values(); } // fields containing order state private CustomerId m_customerId; private Collection m_lineItemIds; // handles to caches containing related objects private static final NamedCache m_cacheCustomers = CacheFactory.getCache("customers"); private static final NamedCache m_cacheOrders = CacheFactory.getCache("orders"); private static final NamedCache m_cacheLineItems = CacheFactory.getCache("orderlineitems"); }
コンポジット・トランザクションが必要なアプリケーションは、サービス層を使用する必要があります。これにより、2つのことを実現できます。1つは、ACIDの特質を損なうことなく、複数のエンティティの正しい構成を単一のトランザクションにすることです。もう1つは、並行処理制御を一元管理できることです。これにより、積極的に最適化されたトランザクションを管理できます。
基本的なトランザクション管理には、(分離レベルに基づく)正確な読取りおよび(並行性方針に基づく)一貫性のあるアトミック更新を保証することがあります。(J2CAアダプタまたはプログラムを介してアクセス可能な)Transaction Framework APIは、これらの問題を自動的に処理します。
『Oracle Coherence開発者ガイド』のトランザクションの実行に関する項を参照してください。
(トランザクション全体の分離レベルおよび並行性方針の組合せとして記述された)データベース・トランザクションに共通なトランザクションの特性では、残念ながら非常にきめの粗い制御しか提供できません。この制御はキャッシュに適さないことが多く、一般にはトランザクション率に大きく影響されます。トランザクションを手動で制御することにより、アプリケーションで並行性を高度に制御できるようになり、効率性が飛躍的に向上します。
ペシミスティック・トランザクションの一般的なパターンは、lock
->
read
->
write
->
unlock
となります。オプティミスティック・トランザクションの場合、そのシーケンスはread
->
lock
&
validate
->
write
->
unlock
となります。2フェーズ・コミットを考慮する場合、ロックが最初のフェーズになり、書込みが2番目のフェーズになります。個々のオブジェクトをロックすることにより、REPEATABLE_READ
分離セマンティクスが保証されます。ロックを削除することは、READ_COMMITTED
分離であることと同じことです。
分離と並行性方針を組み合せることで、アプリケーションはより高いトランザクション率を達成できます。たとえば、過度にペシミスティックな並行性方針では並行性が低下しますが、過度にオプティミスティックな方針では過剰なトランザクション・ロールバックが発生する場合があります。ペシミスティックに管理するエンティティとオプティミスティックに管理するエンティティをインテリジェントに決定することで、アプリケーションはトレードオフのバランスをとることができます。同様に、多くのトランザクションはあるエンティティに対しては強力な分離が必要ですが、その他のエンティティには弱い分離で済みます。必要な分離の度合いのみを使用すれば、競合が最小限に抑えられ、処理のスループットが向上します。
サービス層で最適に適用できる高度なトランザクション処理テクニックがいくつかあります。これらのテクニックを正しく使用すると、多少の労力を負担するだけで、スループット、待機時間、フォルト・トレランスを飛躍的に向上できます。
一般的な解決策は、ロックのニーズを最小限に抑えることに関係します。特に、順序付きロックのアルゴリズムを使用すると、必要なロック数を小さくしてデッドロックの可能性を排除することもできます。最も一般的な例は、子オブジェクトをロックする前に親オブジェクトをロックすることです。場合によっては、サービス層は親オブジェクトに対するロックに依存して子オブジェクトを保護することができます。これにより、ロックを効果的に粗くして(競合も多少大きくなります)、ロック・カウントを最小限に抑えることができます。
例13-2 順序付きロック・アルゴリズムの使用
public class OrderService { // ... public void executeOrderIfLiabilityAcceptable(Order order) { OrderId orderId = order.getId(); // lock the parent object; by convention, all accesses // will lock the parent object first, guaranteeing // "SERIALIZABLE" isolation with respect to the child // objects. m_cacheOrders.lock(orderId, -1); try { BigDecimal outstanding = new BigDecimal(0); // sum up child objects Collection lineItems = order.getLineItems(); for (Iterator iter = lineItems.iterator(); iter.hasNext(); ) { LineItem item = (LineItem)iter.next(); outstanding = outstanding.add(item.getAmount()); } // get the customer information; no locking, so // it is effectively READ_COMMITTED isolation. Customer customer = order.getCustomer(); // apply some business logic if (customer.isAcceptableOrderSize(outstanding)) { order.setStatus(Order.REJECTED); } else { order.setStatus(Order.EXECUTED); } // update the cache m_cacheOrders.put(order); } finally { m_cacheOrders.unlock(orderId); } } // ... }
子オブジェクトが共有されている場合(たとえば、2つの親オブジェクトが両方とも同じ子オブジェクトを参照している場合など)のベスト・プラクティスは、親オブジェクトで子オブジェクトのID(外部キー)のリストを保持することです。次に、NamedCache.get()
またはNamedCache.getAll()
メソッドを使用して子オブジェクトにアクセスします。多くの場合、参照されるオブジェクトに対して親オブジェクトのニア・キャッシュまたはレプリケーション・キャッシュを使用することが適切です(特に、それらの大部分が読取りまたは読取り専用の場合)。
子オブジェクトが読取り専用の場合(または古いデータにアクセスできる場合)、およびオブジェクト全体のグラフがしばしば要求される場合、子オブジェクトを親オブジェクトに含めると、キャッシュ・リクエストの数を削減するのに役立ちます。これは、参照されるオブジェクトがレプリケーションのようにすでにローカルにあったり、場合によってはニア・キャッシュであると、ローカル・キャッシュは非常に効率的であるので、あまり意味をなしません。また、子オブジェクトが大きい場合も意味をなしません。ただし、子オブジェクトを別のキャッシュからフェッチしてさらにネットワーク処理が発生する可能性がある場合、オブジェクト全体のグラフを瞬時にフェッチして遅延時間を小さくすることは、親オブジェクトの中に子オブジェクトを配列するコストより重要です。
オブジェクトが排他的に所有されている場合、いくつかの追加オプションがあります。とりわけ、オブジェクト・グラフはトップダウン(通常のアプローチ)、ボトムアップ、またはその両方で管理することができます。一般的には、トップダウンの管理が最も単純で効率的なアプローチです。
子オブジェクトがキャッシュに挿入された後に親オブジェクトが更新され(順序付き更新のパターン)、親オブジェクトの子リストが更新された後に子オブジェクトが削除される場合、アプリケーションは失われた子オブジェクトを見つけることができません。
同様に、すべてのサービス層の子オブジェクトへのアクセスで最初に親オブジェクトがロックされると、SERIALIZABLE形式の分離が(子オブジェクトに関して)非常に安価に提供できます。
子の依存性をボトムアップで管理する場合、各子に親IDのタグを付けます。そこで問合せを使用して(意味的には、"find children where parent = ?")子オブジェクトを検索します(さらに必要に応じてそれらを変更します)。問合せは非常に高速ですが、主キーのアクセスよりは低速です。このアプローチの主な利点は、(READ_COMMITTED
分離の制限内で)親オブジェクトの競合が低減することです。もちろん、親と子のオブジェクトを単一のコンポジット・オブジェクトにまとめて、カスタムの子の更新EntryProcessor
を使用することで、効率的な親子の階層の管理を実現できます。これにより、各コンポジット・オブジェクトに対して毎秒数百の更新が可能になります。
もう1つのオプションは、親子のリレーションシップを双方向で管理することです。この利点は、各子が自分の親を認識し、親は子のオブジェクトを認識していることです。これにより、グラフのナビゲーションが簡略化されます(たとえば、子オブジェクトはその兄弟を捜すことができます)。この最大のデメリットは、リレーションシップの状態が冗長であるということです。親子のリレーションシップがあれば、親子両方のオブジェクトのデータが存在します。これにより、リレーションシップ情報の回復力があるアトミック更新の確保が複雑化し、トランザクション管理がより困難になります。また、順序付きロック/更新の最適化も複雑化します。
排他的に所有されたオブジェクトは、通常のリレーションシップとして管理できます(NamedCache
メソッド周りのゲッター/セッターのラップ)。また、オブジェクトを直接埋め込むこともできます(概略はデータベース用語の非正規化と同様)。非正規化することで、データは冗長的に保存されず、柔軟性の小さい形式でのみ保存されます。ただし、キャッシュ・スキーマはアプリケーションの一部であり、永続コンポーネントではないので、効率的な一時問合せの要件がない場合、柔軟性が失われることは問題ではありません。アプリケーション層のキャッシュを使用すると、キャッシュ・スキーマは積極的に最適化して効率化できるようになり、永続(データベース)スキーマは柔軟で堅牢になります(一般的にある程度の効率性が犠牲になります)。
子オブジェクトの組込みの決定は、親および子オブジェクトに対して想定されるアクセス・パターンに依存します。キャッシュ・アクセスのバルクがオブジェクト・グラフ全体(またはその大部分)に対するものであれば、子オブジェクトを埋め込むと最適です(共通パスが最適化されます)。
オブジェクト・グラフの一部に対してアクセスを最適化するには(たとえば、単一の子オブジェクトを取得する、または親オブジェクトの属性を更新するなど)、EntryProcessor
を使用して極力サーバーに処理を移行し、必要なデータのみをネットワークに送信します。
アフィニティを使用して、親と子のオブジェクトの相互関係を最適化できます(オブジェクト・グラフ全体が常に単一のJVM内に配置されるようになります)。これにより、複数エンティティのリクエストの処理(問合せ、バルク操作など)に関連するサーバー数を最小限に抑えられます。アフィニティにより、アプリケーション設計に影響を及ぼすことなく、非正規化の恩恵を享受できます。ただし、非正規化構造によりさらに処理を合理化できます(たとえば、グラフのトラバースを単一のネットワーク操作にするなど)。
共有されたオブジェクトは、一般的な遅延ゲッター・パターンを使用して参照する必要があります。読取り専用データの場合、返されたオブジェクトは後続のアクセスのために一時(シリアライズされない)フィールドにキャッシュできます。通常は、複数エンティティの更新(たとえば、親子両方のオブジェクトの更新など)はサービス層で管理する必要があります。