ヘッダーをスキップ
Oracle® Coherenceスタート・ガイド
リリース3.7.1
B65028-01
  ドキュメント・ライブラリへ移動
ライブラリ
製品リストへ移動
製品
目次へ移動
目次

前
 
次
 

13 オブジェクト・モデルの管理

このドキュメントでは、Coherence名前付きキャッシュのコレクションで状態を管理する、オブジェクト・モデル管理のベスト・プラクティスについて説明します。一連のエンティティ・クラスとエンティティ・リレーションシップがある場合に、Coherence名前付きキャッシュのコレクション間でオブジェクト・モデルを表現および管理するための最適な方法について考えます。

13.1 キャッシュの使用パラダイム

クラスタ化されたキャッシング・ソリューションの価値は、その使用方法に依存します。それは単にアプリケーション・レイヤーのデータベースのデータをキャッシングして、瞬時にアクセスできるよう準備を整えておくことでしょうか。次の段階に進んで、トランザクション制御をアプリケーション・レイヤーに移行することでしょうか。あるいは、さらに進んで、トランザクション制御を積極的に最適化することでしょうか。

単純なデータ・キャッシング

特に並行処理制御が必要ない場合(コンテンツのキャッシングなど)やトランザクション制御がデータベースで管理されている場合(Hibernate製品やJDO製品のプラグイン・キャッシュなど)は、単純なデータのキャッシュが一般的です。このアプローチではアプリケーション設計への影響が最低限に抑えられるため、多くの場合、オブジェクト・リレーショナル・マッピング(ORM)レイヤーまたはアプリケーション・サーバー(EJBコンテナ、Springなど)で透過的に実装されます。ただし、これではデータベース・サーバーのオーバーロードの問題が完全には解決されません。特に非トランザクション読取りがアプリケーション・レイヤーで処理されている場合、すべてのトランザクション・データ管理にはデータベースとの相互作用が依然として必要です。

データ・アクセス要件が主キーによる単純なアクセス以上のことを必要とする場合、キャッシングは決して無関係な問題ではないということを認識することが重要です。つまり、キャッシングのメリットを確実に活用するためには、キャッシングを念頭においてアプリケーションを設計する必要があります。

トランザクション・キャッシング

データベースを単独で操作し、独自のスケーラビリティを必要とするアプリケーションは、データ管理のより大きな役割を担うことから始める必要があります。これには、Coherenceの読取りアクセス機能(リードスルー・キャッシング、キャッシュ問合せ、集計)、データベース・トランザクションの最小化機能(ライトビハインド)、および並行処理の管理機能(ロック、キャッシュ・トランザクション)を使用することがあります。

トランザクションの最適化

フォルト・トレランス、短い待機時間、および高いスケーラビリティのすべてを必要とするアプリケーションでは、一般に、トランザクションをより最適化する必要があります。従来のトランザクション制御では、Orderオブジェクトを管理する際にアプリケーションでSERIALIZABLE分離を必要とする場合がありました。分散環境の場合、この操作は非常にコストがかかることがあります。分散環境以外でも、ほとんどのデータベースおよびキャッシング製品では、多くの場合、これを実現するために表ロックを使用します。そのため、使用可能なハードウェア・リソースに関係なく、スケーラビリティに大きな制限がかかります。実際に、最新のハードウェアを使用してもトランザクション率が毎秒数百トランザクションに制限される場合もあります。ただし、慣例に基づくロックは、たとえば、すべてのアクセッサが親Orderオブジェクトのみをロックする必要がある場合などに役立ちます。それによって、ロックの範囲が表レベルからオーダー・レベルに縮小され、スケーラビリティが格段に高くなります(もちろん、アプリケーションによっては、複数のJMSキュー間でイベント処理をパーティション化して明示的な並行処理制御の必要性を回避することによって、同様の結果を実現しているものもあります)。さらに最適化するためには、EntryProcessorを使用してクラスタ化の調整を不要にすることなどがあります。これにより、特定のキャッシュ・エントリに対するトランザクション率を飛躍的に高めることができます。

13.2 オブジェクト・モデルの管理技術

リレーションシップという用語は、オブジェクト間の関連性を表します。たとえば、Orderオブジェクトには、一連のLineItemオブジェクト(のみ)が含まれています。また、そのOrderオブジェクトに関連付けられたCustomerオブジェクトを指す場合もあります。

データ・アクセス・レイヤーは、一般に、データ・アクセス・オブジェクト(DAO)データ転送オブジェクト(DTO)という2つの主要コンポーネント(つまり動作と状態)に分類できます。DAOはデータ・アクセスの動作を制御し、通常はデータベースまたはキャッシュを管理するロジックを格納します。DTOには、DAOで使用するデータ(Orderレコードなど)が格納されます。また、(一部のアプリケーションでは)1つのオブジェクトがDTOとDAOの両方として機能する場合もあります。これらの用語は使用パターンを表します。使用パターンはアプリケーションにより異なりますが、基本となる原理は応用できます。わかりやすくするため、このドキュメント内のサンプルでは、DAOとDTOを組み合せたアプローチ(動作が豊富なオブジェクト・モデル)に従っています。

エンティティ・リレーションシップの管理は、特にスケーラビリティおよびトランザクション性が必要な場合に難しい作業になることがあります。中心的な問題は、理想的なソリューションとして、開発者の作業を最小限に抑え、このようなエンティティ間リレーションシップの複雑さを管理可能にする必要があることにあります。概念的には、リレーションシップ・モデル(XMLやJavaソースなど、いくつかの任意の形式で表現可能なもの)を使用し、その記述に準拠した実行時動作をいかに実現するかということにあります。

現在使用されているソリューションは、次のグループに分類できます。

コードの生成

コードの生成は、.javaまたは.classファイルの生成に関連した一般的なオプションです。このアプローチは、通常、いくつかの管理および監視、AOPおよびORMツール(AspectJ、Hibernate)で使用されます。このアプローチにおける主な課題はアーティファクトの生成にあり、ソフトウェア構成管理(SCM)システムでの管理が必要になることがあります。

バイトコードのインスツルメント化

このアプローチでは、ClassLoaderのインターセプションを使用して、JVMへのロード時にクラスをインスツルメント化します。このアプローチは、通常、AOPツール(AspectJ、JBossCacheAop、TerraCotta)および一部のORMツール(JDO実装でよく見られる)で使用されます。実行時のコード変更に関連する、認知されたリスクまたは実際のリスクがあるため(アプリケーション・サーバーでのホット・デプロイ・オプションのブレーク傾向を含む)、多くの組織ではこのオプションを実行できません。したがって、これは初期オプションではありません。

開発者実装クラス

最も柔軟なオプションは、実行時問合せエンジンを使用することです。ORM製品では、この処理の大半をデータベース・サーバーに委任しています。問合せエンジンをアプリケーション層の内部で提供する方法もありますが、これは完全なデータベース・サーバーの管理性とスケーラビリティを制限することと同じ複雑さをもたらします。

Coherenceでの推奨される手法は、DAOメソッドについて明示的かつ綿密に計画を立てることです。これにより、追加の開発作業をある程度行うことで、動作を確定的にする(動的に評価される問合せを回避する)ことができます。この追加作業の量は、リレーションシップ・モデルの複雑さに正比例します。小規模から中規模のモデル(Coherenceで管理するエンティティのタイプが最大50)では、わずかな作業で済みます。大規模なモデル(特に複雑なリレーションシップがあるモデル)では、膨大な開発作業が必要になる場合があります。

ベスト・プラクティスとしては、すべての状態トランザクション、リレーションシップ・トランザクションおよびアトミック・トランザクションをオブジェクト・モデルで処理してください。より高度なトランザクション制御では、並行性を調整する(構成可能なトランザクションに対応した)追加のサービス・レイヤーが必要になります。

構成可能なトランザクション:

Coherenceでのトランザクションの使用の詳細は、『Oracle Coherence開発者ガイド』のトランザクションの実行に関する項を参照してください。

13.3 ドメイン・モデル

NamedCacheには1種類のエンティティを格納するようにします(データベース表に1種類のエンティティを格納するのと同じです)。唯一の一般的な例外は、多くの場合に任意の値を格納する、ディレクトリタイプのキャッシュです。

追加の各NamedCacheで消費されるメモリーは、クラスタに参加している1メンバー当たり数十バイトにすぎません。これは、バッキング・マップに応じて異なります。透過的なデータベース統合に使用する<read_write_backing_map_scheme>で構成されたキャッシュは、ライトビハインド・キャッシュが有効な場合はさらにリソースを消費しますが、これは名前付きキャッシュが数百にも増大しないかぎり影響要因とはなりません。

可能であれば、ビジネス・トランザクションを単一のキャッシュ・エントリ更新にマップするように、キャッシュ・レイアウトを設計します。これによりトランザクションの制御が簡素化され、結果的にスループットが大幅に向上する可能性があります。

ほとんどのキャッシュでは、意味のあるキーを使用する必要があります(単なるID管理目的で使用されるリレーショナル・システムでは、一般に意味のないキーが使用されます)。このデメリットの1つは問合せのサポートが制限されることです(Coherenceの問合せは、現在、エントリ・キーではなくエントリ値のみに適用されます)。キーの属性に対する問合せを実行するには、値を属性に複製する必要があります。

13.3.1 Coherenceでのデータ・アクセス・オブジェクトに関するベスト・プラクティス

DAOオブジェクトでは、NamedCacheへのアクセスにおいてgetter、setter、queryメソッドを実装する必要があります。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");
    }

13.4 サービス・レイヤー

コンポジット・トランザクションを必要とするアプリケーションでは、サービス・レイヤーを使用する必要があります。これにより次のことが可能になります。第1に、ACID特性を損なうことなく、複数のエンティティを単一のトランザクションに適切に構成できます。第2に、並行処理制御の一元管理を実現することで、トランザクション管理を積極的に最適化できます。

13.4.1 トランザクションの自動管理

基本的なトランザクション管理は、(分離レベルに基づく)正確な読取りと、(同時アクセス方針に基づく)一貫性のあるアトミック更新を保証することにあります。これらの問題は、トランザクション・フレームワークAPIによって自動的に処理されます(このAPIにはJ2CAアダプタまたはプログラムを介してアクセスできます)。

Oracle Coherence開発者ガイド』のトランザクションの実行に関する項を参照してください。

13.4.2 トランザクションの明示的な管理

残念ながら、データベース・トランザクションに共通するトランザクション特性(トランザクション全体の分離レベルと同時アクセス方針の組合せとして記述される)で得られるのは、きわめて粗密な制御にすぎません。一般に、キャッシュではトランザクション率が通常よりはるかに高くなる傾向にあるため、こうした粗密な制御は、多くの場合、キャッシュには適しません。トランザクションを手動で制御することによって、アプリケーションでの同時アクセスに対する制御を大幅に強化し、効率を飛躍的に向上させることができます。

ペシミスティック・トランザクションの一般的なパターンは、lock -> read -> write -> unlockです。オプティミスティック・トランザクションの場合、この順序はread -> lock & validate -> write -> unlockになります。2フェーズ・コミットを考慮する場合は、ロックが第1フェーズになり、書込みが第2フェーズになります。個々のオブジェクトをロックすることによって、REPEATABLE_READ分離セマンティクスが保証されます。ロックを解除することは、READ_COMMITTED分離に相当します。

分離と同時アクセス方針を組み合わせることによって、アプリケーションでのトランザクション率を高めることができます。たとえば、同時アクセス方針が過度にペシミスティックだと並行性が低下し、逆に過度にオプティミスティックだとトランザクションのロールバックが過剰発生することがあります。ペシミスティックな管理対象のエンティティと、オプティミスティックな管理対象のエンティティをアプリケーションでインテリジェントに決定することによって、このトレードオフのバランスをとることができます。同様に、エンティティに応じて、強力な分離を必要としたり、それよりもはるかに弱い分離を必要としたりするトランザクションも多数あります。必要な分離レベルのみを使用することによって、競合を最小化し、処理のスループットを高めることができます。

13.4.3 トランザクション処理の最適化

サービス・レイヤーでの使用に最適な、高度なトランザクション処理技術がいくつかあります。これらの技術を適切に使用すると、多少の追加作業を行うだけで、スループット、待機時間およびフォルト・トレランスを大幅に改善できます。

最も一般的なソリューションは、ロックの必要性の最小化に関連します。具体的には、順序付きロック・アルゴリズムを使用することで、必要なロック数を削減し、さらにデッドロックの可能性を排除できます。最も一般的な例は、親オブジェクトをロックしてから子オブジェクトをロックすることです。サービス・レイヤーでは、親オブジェクトをロックすることによって子オブジェクトを保護できる場合があります。この場合、ロックが効果的に粗密化され(競合が若干増加)、ロック数が大幅に制限されます。

例13-2 順序付きロック・アルゴリズムの使用

public class OrderService
    {
    // ...

    public void executeOrderIfLiabilityAcceptable(Order order)
        {
        OrderId orderId = order.getId();

        // lock the parent object; by convention, all accesses
        // lock the parent object first, guaranteeing
        // "SERIALIZABLE" isolation for 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);
            }
        }

    // ...
    }

13.5 子オブジェクトのコレクションの管理

13.5.1 共有された子オブジェクト

共有された子オブジェクト(2つの親オブジェクトが同じ子オブジェクトを参照している場合など)に関するベスト・プラクティスは、親オブジェクトで子オブジェクトの識別子(外部キー)のリストを維持することです。そして、NamedCache.get()またはNamedCache.getAll()メソッドを使用して、子オブジェクトにアクセスします。多くの場合、親オブジェクトについてはニア・キャッシュを使用し、参照先オブジェクトについてはレプリケートされたキャッシュを使用する(特に、参照先オブジェクトが読取り専用または読取りが大半の場合)という方法をとるのが妥当と考えられます。

子オブジェクトが読取り専用の場合(または失効データが許容される場合)に、オブジェクト・グラフ全体が頻繁に必要なら、親オブジェクトに子オブジェクトを組み込むと、キャッシュ・リクエスト数の削減に役立つことがあります。参照先オブジェクトがローカルにある場合(レプリケートされたキャッシュや、場合によってはニア・キャッシュにある場合)、ローカル・キャッシュ・リクエストは非常に効率的なため、前述の方法は意味がありません。また、子オブジェクトのサイズが大きい場合も、あまり意味がありません。しかし、別のキャッシュからの子オブジェクトのフェッチによって追加のネットワーク処理が発生する可能性がある場合には、オブジェクト・グラフ全体のフェッチにおける待機時間の短縮は、親オブジェクト内に子オブジェクトをインライン化する手間を上回るメリットをもたらす可能性があります。

13.5.2 所有された子オブジェクト

オブジェクトが排他的に所有されている場合は、いくつかの追加オプションがあります。具体的には、オブジェクト・グラフをトップダウン(通常のアプローチ)、ボトムアップまたはその両方によって管理できます。一般に、トップダウン管理が最も単純で、最も効率的なアプローチです。

子オブジェクトが親オブジェクトの更新(順序付き更新パターン)前にキャッシュに挿入され、親オブジェクト内の子オブジェクト・リストの更新後に削除された場合、消失した子オブジェクトはアプリケーションからは見えません。

同様に、サービス・レイヤーから子オブジェクトへのすべてのアクセスで親オブジェクトが最初にロックされる場合、(子オブジェクトに対して)SERIALIZABLEスタイルの分離をきわめて低コストで実現できます。

13.5.3 子オブジェクトのボトムアップ管理

子オブジェクトの依存性をボトムアップで管理するには、各子オブジェクトに親の識別子のタグを付けます。そして、問合せ(意味的には、"find children where parent = ?")を使用して子オブジェクトを検索します(その後、必要に応じて変更します)。問合せは非常に高速ですが、主キーへのアクセスに比べると低速です。このアプローチの主な利点は、親オブジェクトに対する競合が(READ_COMMITTED分離の制限内で)軽減されることです。もちろん、親オブジェクトと子オブジェクトを単一のコンポジット・オブジェクトに結合し、カスタムの子の更新EntryProcessorを使用することによって、親子階層を効率的に管理することも可能です。この場合、各コンポジット・オブジェクトに対して1秒間に数百もの更新を実行できます。

13.5.4 子オブジェクトの双方向管理

もう1つのオプションは親子関係を双方向で管理することです。この場合の利点は、各子オブジェクトがその親を認識し、親もその子オブジェクトを認識するため、グラフのナビゲーションが簡素化されることです(たとえば、子オブジェクトはその兄弟オブジェクトを見つけることができます)。最大の欠点は、リレーションシップの状態が冗長化することです。それぞれの親子関係で親と子両方のオブジェクトにデータが存在することになります。それにより、リレーションシップ情報でのレジリエンスのあるアトミック更新の保証が複雑化し、トランザクションの管理がより難しくなります。また、順序付きロック/更新の最適化も複雑化します。

13.6 所有されたオブジェクトの関連付け

13.6.1 非正規化

排他的に所有されたオブジェクトは、通常のリレーションシップとして管理するか(NamedCacheのメソッドをgetter/setterでラップ)、または直接埋め込むことができます(大まかに言えば、データベース用語における非正規化に類似)。非正規化では、データが冗長保存されるのではなく、単に柔軟性の低い形式で保存されます。しかし、キャッシュ・スキーマはアプリケーションの一部であり、永続コンポーネントではないため、効率的な非定型問合せの要件がないかぎり、柔軟性の喪失は問題とはなりません。アプリケーション層のキャッシュを使用することで、効率向上のためにキャッシュ・スキーマを積極的に最適化できるとともに、永続的な(データベースの)スキーマを柔軟で堅牢なものにすることができます(通常、その代償として、ある程度効率が低下します)。

子オブジェクトをインライン化するかどうかの決定は、親オブジェクトと子オブジェクトに対する予測されるアクセス・パターンに依存します。キャッシュ・アクセスの大部分が、オブジェクト・グラフ全体(またはその大半)に対するものなら、子オブジェクトを埋め込むのが最適と考えられます(共通パスが最適化されます)。

オブジェクト・グラフの一部に対するアクセス(単一子オブジェクトの取得や、親オブジェクトの1つの属性の更新など)を最適化するには、EntryProcessorを使用して、できるかぎり多くの処理をサーバーに移動して、必要なデータのみをネットワーク経由で送るようにします。

13.6.2 アフィニティ

アフィニティでは、親と子のオブジェクトの関連付けが最適化されます(オブジェクト・グラフ全体が常に単一のJVMに確実に配置されます)。それによって、複数のエンティティのリクエスト(問合せ、一括操作など)の処理に関与するサーバーの数が最小化されます。アフィニティは、アプリケーション設計にまったく影響を及ぼすことなく、非正規化のメリットの多くを提供します。ただし、構造の非正規化のほうが、処理がさらに合理化されます(たとえば、グラフのトラバースを単一のネットワーク操作にするなど)。

13.7 共有オブジェクトの管理

共有オブジェクトは、標準的な遅延ゲッター・パターンによって参照する必要があります。読取り専用データについては、返されたオブジェクトを後続のアクセス用に一時的な(シリアライズ不可)フィールドにキャッシュできます。通常と同じように、複数のエンティティの更新(たとえば、親子両方のオブジェクトの更新など)は、サービス・レイヤーで管理する必要があります。

例13-3 遅延ゲッター・パターンの使用

public class Order
    {
    // ...

    public Customer getCustomer()
        {
        return (Customer)m_cacheCustomers.get(m_customerId);
        }

    // ...
    }

13.8 既存のDAOのリファクタ

通常、既存のDAOをリファクタするときのパターンでは、既存のDAOをインタフェースと2つの実装(元のデータベース・ロジックと新しいキャッシュ対応ロジック)に分割します。アプリケーションでは、(抽出した)インタフェースを引き続き使用します。データベース・ロジックはCacheStoreモジュールに移動します。キャッシュ対応DAOでは、(データベースDAOを基盤とする)NamedCacheにアクセスします。NamedCacheにマッピングできないDAO操作はすべて、データベースDAOに直接渡します。

図13-1 DAOのリファクタ・プロセス

この図については前の文で説明しています。