この章では、Coherenceを一時的な記録システムとして使用するためにデータソースをキャッシュする方法について説明します。また、サンプルや実装に関する考慮事項についても解説します。
この章の内容は次のとおりです。
Coherenceでは、データベース、Webサービス、パッケージ化されたアプリケーション、ファイル・システムなど、任意のデータソースの透過的な読取りまたは書込みのキャッシングがサポートされますが、データベースが最も一般的に使用されます。ここでは、任意のバックエンド・データソースのことを、簡単にデータベースと呼びます。効率的なキャッシュでは、集中的な読取り専用操作および読取り/書込み操作をサポートし、読取り/書込み操作の際には、キャッシュとデータベースは完全に同期化されている必要があります。データソースのキャッシュを実現するために、Coherenceではリードスルー、ライトスルー、リフレッシュアヘッドおよびライトビハインド・キャッシュをサポートします。
|
注意: リードスルー/ライトスルー・キャッシング(および他の類似するキャッシング形態)は、パーティション(分散)キャッシュ(ニア・キャッシュも含む)トポロジで使用することのみを目的としています。ローカル・キャッシュでは、この機能のサブセットがサポートされます。レプリケーション・キャッシュとオプティミスティック・キャッシュは使用しないでください。 |
この項の内容は次のとおりです。
CacheStoreは、キャッシュと基礎となるデータソースの接続に使用されるアプリケーション固有のアダプタです。CacheStore実装では、データ・アクセス・メカニズム(Hibernate、Toplink Essentials、JPA、アプリケーション固有のJDBCコール、他のアプリケーション、メインフレーム、他のキャッシュなど)を使用してデータソースにアクセスします。CacheStoreでは、データソースからの取得データを使用したJavaオブジェクトの作成方法、データソースへのオブジェクトのマッピング方法や書込み方法、およびデータソースからのオブジェクトの削除方法が認識されます。
データソース接続方針およびデータソース-アプリケーション・オブジェクトのマッピング情報は、データソース・スキーマ、アプリケーション・クラス・レイアウト、および操作環境に固有です。そのため、このマッピング情報は、アプリケーション開発者がCacheStore実装の形式で用意する必要があります。詳細は、「CacheStore実装の作成」を参照してください。
アプリケーションが、たとえばkey Xのエントリをキャッシュに要求したときに、キャッシュにXがなかった場合、CoherenceではCacheStoreへの委任が自動的に実行され、基礎となるデータソースからのXのロードが要求されます。データソースにXが存在している場合、CacheStoreはそれをロードしてCoherenceに返し、Coherenceは将来の使用に備えてそれをキャッシュに置き、最終的にXを要求したアプリケーションに返します。これをリードスルー・キャッシングと呼びます。リフレッシュアヘッド・キャッシュ機能を使用すると、(認識されている待機時間を削減することにより)読取りパフォーマンスがさらに向上します。詳細は、「リフレッシュアヘッド・キャッシング」を参照してください。
Coherenceでは、2つの異なる方法でデータソースの更新を処理できます。1つ目の方法は、ライトスルーです。ライトスルーでは、アプリケーションでキャッシュのデータが更新されると(つまり、put(...)をコールしてキャッシュ・エントリを変更すると)、CacheStoreを介して基礎となるデータソースにデータが正常に保存されるまで操作は完了しません(つまり、putは返されません)。このままでは、データソースへの書込みにおいて待機時間が生じるため、書込みパフォーマンスはまったく向上しません。書き込みパフォーマンスを向上させるには、ライトビハインド・キャッシュ機能を使用します。詳細は、「ライトビハインド・キャッシング」を参照してください。
ライトビハインドのシナリオでは、構成された遅延(10秒、20分、1日、1週間、またはそれ以上)の経過後に、変更されたキャッシュ・エントリが非同期でデータソースに書き込まれます。これは、キャッシュの挿入と更新にのみ適用されることに注意する必要があります。キャッシュ・エントリの削除は、データソースと同期して実行されます。ライトビハインド・キャッシングでは、データソースで更新する必要のあるデータのライトビハインド・キューが保持されます。アプリケーションでキャッシュのXが更新されると、Xはライトビハインド・キューに追加され(Xがない場合。ある場合は置き換えられる)、指定されたライトビハインド遅延の経過後、CoherenceはCacheStoreをコールして基礎となるデータソースのXを最新の状態に更新します。ライトビハインド遅延は、一連の変更が始まる時点を基準としています。つまり、データソースのデータは、キャッシュのライトビハインド遅延を超えて遅れることはありません。
このため、一度読み取って構成された間隔で書き込む(つまり、更新頻度が少ない)シナリオになります。このアーキテクチャ・タイプには、主に4つの利点があります。
基礎となるデータソースへのデータ書込みを待つ必要がないため、アプリケーションのパフォーマンスが向上します(データは後で、別の実行スレッドによって書き込まれる)。
アプリケーションにおけるデータベース負荷が大幅に削減されます。読取り操作と書込み操作の量が両方とも減るため、データベース負荷も削減されます。他のキャッシング・アプローチと同様、キャッシングによって読取りが削減されます。一般的に、書込みはより負荷の高い操作ですが、多くの場合、その回数が減ります。ライトビハインド間隔の間における同一オブジェクトへの複数の変更操作が結合され、基礎となるデータソースに一度だけ書き込まれるからです(書込み結合)。また、複数のキャッシュ・エントリへの書込みは、CacheStore.storeAll()メソッドを使用して単一のデータベース・トランザクションにまとめることができます(書込み組合せ)。
アプリケーションは、データベース障害からある程度保護されます。書込みの失敗時にオブジェクトが書込みキューに再度配置されるように、ライトビハインド機能を構成できるからです。アプリケーションの使用データがCoherenceキャッシュにある場合、データベースを稼働せずに操作を継続できます。これは、Coherenceパーティション・キャッシュを使用すると、容易に実現できます。Coherenceパーティション・キャッシュでは、(ローカル記憶域が有効な)すべての参加クラスタ・ノードでキャッシュ全体をパーティション化できるため、巨大なキャッシュ・メモリーを使用できます。
線形スケーラビリティ: アプリケーションでより多くの同時ユーザー数を処理するには、クラスタ内のノード数を増加するだけで済みます。データベース負荷への影響は、ライトビハインド間隔を長くすることで調整できます。
ライトビハインド・キャッシングは、1つの構成設定を調整するだけで有効化できますが、ライトビハインドを期待どおりに動作させるのはより困難です。特に、アプリケーション設計では、いくつかの設計上の問題に前もって対処しておく必要があります。
データベースの更新がキャッシュ・トランザクションの外側で発生するため、そこにライトビハインド・キャッシングが最も作用します。つまり、キャッシュ・トランザクションはほとんどの場合、データベース・トランザクションが開始される前に完了します。これは、データベース・トランザクションが失敗してはならないことを意味します。これが保証できない場合、ロールバック機能を採用する必要があります。
ライトビハインドはデータベース更新の順序を並べ替えることができるため、参照整合性制約で順不同の更新をできるようにする必要があります。概念的には、これはデータベースをISAMスタイル(更新競合がないことが保証された主キー・ベースのアクセス)の記憶域として使用するようなものです。他のアプリケーションとデータベースを共有すると、新たな困難に直面します。つまり、ライトビハインド・トランザクションが外部の更新と競合しないことを保証する手段がなくなります。これは、ライトビハインドの競合がヒューリスティックに処理されるか、オペレータが手動で調整する必要があることを意味します。
通常は、論理データベース・トランザクションに各キャッシュ・エントリの更新をマッピングすると、最も単純なデータベース・トランザクションが保証されるため理想的です。
ライトビハインドは、(ライトビハインド・キューがディスクに書き込まれるまで)キャッシュを効率的に記録システムで保持するため、ビジネス規則ではクラスタに永続可能な(ディスクへの永続可能ではなく)データおよびトランザクションの記憶域を許可する必要があります。
リフレッシュアヘッドのシナリオでは、キャッシュ・ローダーから最近アクセスされた任意のキャッシュ・エントリを、失効する前に自動的かつ非同期的にリロード(リフレッシュ)するようにキャッシュを構成できます。その結果、アクセス頻度の高いエントリがキャッシュに入ると、失効後にエントリがリロードされるときに、アプリケーションは潜在的に低速なキャッシュ・ストアに対する読取りの影響を受けません。非同期リフレッシュは、有効期限が近いオブジェクトへのアクセスが発生したときにのみトリガーされます。オブジェクトへのアクセスが期限切れ後の場合、キャッシュ・ストアからの読取りが同期的に行われ、その値がリフレッシュされます。
リフレッシュアヘッド期限は、エントリの有効期間の割合として表されます。たとえば、キャッシュ・エントリの有効期間が60秒に設定されており、リフレッシュアヘッド係数が0.5に設定されているとします。キャッシュされたオブジェクトを60秒後にアクセスすると、キャッシュ・ストアから同期読取りが実行され、その値がリフレッシュされます。ただし、30秒を超える60秒未満のエントリに対するリクエストを実行すると、キャッシュの現在の値が返され、キャッシュ・ストアから非同期のリロードがスケジュールされます。
リフレッシュアヘッドは、多数のユーザーがオブジェクトにアクセスする場合に有用です。値はキャッシュで最新の状態に維持され、キャッシュ・ストアの大量のリロードに起因する待機時間が回避されます。
リフレッシュアヘッド係数の値は、coherence-cache-config.xmlファイルの<read-write-backing-map-scheme>要素の<refresh-ahead-factor>サブ要素で指定します。リフレッシュアヘッドでは、キャッシュのエントリの有効期間(<expiry-delay>)も設定されていることを前提としています。
例14-1では、ローカル・キャッシュのエントリのリフレッシュアヘッド係数を0.5で、有効期間を20秒で構成しています。エントリが有効期間の10秒以内にアクセスされると、キャッシュ・ストアから非同期にリロードされるようにスケジュールされます。
例14-1 リフレッシュアヘッド係数の指定
<distributed-scheme>
<scheme-name>categories-cache-all-scheme</scheme-name>
<service-name>DistributedCache</service-name>
<backing-map-scheme>
<read-write-backing-map-scheme>
<scheme-name>categoriesLoaderScheme</scheme-name>
<internal-cache-scheme>
<local-scheme>
<scheme-ref>categories-eviction</scheme-ref>
</local-scheme>
</internal-cache-scheme>
<cachestore-scheme>
<class-scheme>
<class-name>
com.demo.cache.coherence.categories.CategoryCacheLoader
</class-name>
</class-scheme>
</cachestore-scheme>
<refresh-ahead-factor>0.5</refresh-ahead-factor>
</read-write-backing-map-scheme>
</backing-map-scheme>
<autostart>true</autostart>
</distributed-scheme>
<local-scheme>
<scheme-name>categories-eviction</scheme-name>
<expiry-delay>20s</expiry-delay>
</local-scheme>
この項では、いくつかのキャッシング方針の利点を比較します。
クラスタ環境のキャッシュアサイド・パターンには、一般的に2つのアプローチがあります。1つ目のアプローチでは、キャッシュ・ミスをチェックし、データベースに問い合せ、キャッシュに移入して、アプリケーション処理を継続します。この場合、様々なアプリケーション・スレッドがこの処理を同時に実行すると、多数のデータベース・アクセスが発生することになります。もう1つのアプローチでは、アプリケーションでロックを再確認します(これは、チェックがキャッシュ・エントリに対する原子性を持っているため機能します)。ただし、キャッシュ・ミスまたはデータベース更新(クラスタ化されたロック、追加の読取り、およびクラスタ化されたロック解除 - 最大10の追加ネットワーク・ホップ、または一般的なギガビット・イーサネット接続で6から8ミリ秒、さらに追加処理のオーバーヘッドおよびキャッシュ・エントリのロック継続期間の増加)の際に大量のオーバーヘッドが生じます。
インライン・キャッシングを使用すると、(フォルト・トレランスを確保するためにデータがバックアップ・サーバーにコピーされている間)エントリは2つのネットワーク・ホップでのみロックされます。また、ロックはパーティション所有者によりローカルで維持されます。さらに、アプリケーション・コードは完全にキャッシュ・サーバーで管理されます。つまり、ノードの管理下にあるサブセットのみが直接データベースにアクセスします(そのため、負荷とセキュリティが予測可能になる)。加えて、これによりデータベース・ロジックからキャッシュ・クライアントが切り離されます。
リフレッシュアヘッドでは、リードスルーと比べて待機時間が減りますが、それは将来必要とされるキャッシュ・アイテムをキャッシュが正確に予測できる場合のみです。これらの予測が完全に正確であれば、リフレッシュアヘッドにより待機時間が削減され、余分なオーバーヘッドはありません。予測の正確性が低下すると、データベースに送信される不要なリクエストが増加するため、スループットへの影響も大きくなります。データベースのリクエスト処理が遅れ始めると、待機時間が長くなる可能性もあります。
ライトビハインド・キャッシングの要件が満たされると、ライトスルー・キャッシュと比較して非常に高いスループットが得られ、待機時間が削減されます。また、ライトビハインド・キャッシングによりデータベース(書込みの減少)およびキャッシュ・サーバー(キャッシュ値のデシリアライズの削減)の負荷が低減されます。
CacheStore実装はプラッガブルで、データソースのキャッシュの使用状況に応じて、次の2つのインタフェースのいずれかを実装する必要があります。
CacheLoader: 読取り専用キャッシュ。
CacheStore: CacheLoaderを拡張して、読取り/書込みキャッシュをサポートします。
これらのインタフェースは、com.tangosol.net.cacheパッケージにあります。CacheLoaderインタフェースには、load(Object key)およびloadAll(Collection keys)の2つのメイン・メソッドがあります。またCacheStoreインタフェースには、store(Object key, Object value)、storeAll(Map mapEntries)、erase(Object key)およびeraseAll(Collection colKeys)メソッドが追加されました。
CacheStoreの実装の例については、「CacheStoreのサンプル」および「制御可能なCacheStoreのサンプル」を参照してください。
CacheStoreモジュールにプラグインするには、distributed-scheme、backing-map-scheme、cachestore-scheme、またはread-write-backing-map-schemeキャッシュ構成要素内のCacheStore実装クラス名を指定します。
read-write-backing-map-schemeでは、com.tangosol.net.cache.ReadWriteBackingMapが構成されます。このバッキング・マップは、実際にデータをキャッシュする内部マップ(internal-cache-schemeを参照)とデータベースと相互作用するCacheStoreモジュール(cachestore-schemeを参照)という2つの主要な要素で構成されます。
例14-2は、CacheStoreモジュールを指定するキャッシュ構成を示しています。<init-params>要素には、CacheStoreコンストラクタに渡されるパラメータの順序付けられたリストが含まれます。{cache-name}構成マクロを使用してキャッシュ名をCacheStore実装に渡すことにより、これがデータベース表にマップされます。使用可能なすべてのマクロの一覧は、「パラメータ・マクロの使用方法」を参照してください。
ライトビハインドおよびリフレッシュアヘッドの構成方法の詳細は、read-write-backing-map-schemeを参照してください。その際には、write-batch-factor、refresh-ahead-factor、write-requeue-thresholdおよびrollback-cachestore-failures要素に注意してください。
例14-2 Cachestoreモジュールの例
<?xml version="1.0"?>
<cache-config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://xmlns.oracle.com/coherence/coherence-cache-config"
xsi:schemaLocation="http://xmlns.oracle.com/coherence/coherence-cache-config
coherence-cache-config.xsd">
<caching-scheme-mapping>
<cache-mapping>
<cache-name>com.company.dto.*</cache-name>
<scheme-name>distributed-rwbm</scheme-name>
</cache-mapping>
</caching-scheme-mapping>
<caching-schemes>
<distributed-scheme>
<scheme-name>distributed-rwbm</scheme-name>
<backing-map-scheme>
<read-write-backing-map-scheme>
<internal-cache-scheme>
<local-scheme/>
</internal-cache-scheme>
<cachestore-scheme>
<class-scheme>
<class-name>com.company.MyCacheStore</class-name>
<init-params>
<init-param>
<param-type>java.lang.String</param-type>
<param-value>{cache-name}</param-value>
</init-param>
</init-params>
</class-scheme>
</cachestore-scheme>
</read-write-backing-map-scheme>
</backing-map-scheme>
</distributed-scheme>
</caching-schemes>
</cache-config>
|
注意: スレッド・カウント:CacheStoreモジュールを使用すると、キャッシュ・サービス・スレッドの消費が著しく増加します(最速のデータベースを選択しても、インメモリー構造の更新より桁違いに処理が遅くなります)。そのため、キャッシュ・サービスのスレッド・カウントを増加する必要があります(通常は、10 - 100の範囲)。スレッド・プールが不十分な場合の最も顕著な症状は、(背後にあるデータベースの処理が増加せずに)キャッシュ・リクエストの待機時間が増加することです。 |
この項では、com.tangosol.net.cache.CacheStoreインタフェースの非常に基本的な実装について説明します。例14-3の実装では、JDBCによる単一のデータベース接続が使用され、バルク操作は使用されません。完全な実装では接続プールが使用されます。また、ライトビハインドが使用される場合は、CacheStore.storeAll()を実装してJDBCのバルク挿入およびバルク更新を行います。データベースのキャッシュ構成の例は、「データベースのキャッシュ」を参照してください。
|
ヒント: キャッシュをバルク・ロードすることによって、処理作業を軽減できます。次の例では、putメソッドを使用して、キャッシュ・ストアに値を書き込みます。多くの場合、putAllメソッドを使用してバルク・ロードを実行すると、処理作業およびネットワーク・トラフィックが軽減されます。バルク・ロードの詳細は、第20章「キャッシュの事前ロード」を参照してください。 |
例14-3 CacheStoreインタフェースの実装
package com.tangosol.examples.coherence;
import com.tangosol.net.cache.CacheStore;
import com.tangosol.util.Base;
import java.sql.DriverManager;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* An example implementation of CacheStore
* interface.
*
* @author erm 2003.05.01
*/
public class DBCacheStore
extends Base
implements CacheStore
{
// ----- constructors ---------------------------------------------------
/**
* Constructs DBCacheStore for a given database table.
*
* @param sTableName the db table name
*/
public DBCacheStore(String sTableName)
{
m_sTableName = sTableName;
configureConnection();
}
/**
* Set up the DB connection.
*/
protected void configureConnection()
{
try
{
Class.forName("org.gjt.mm.mysql.Driver");
m_con = DriverManager.getConnection(DB_URL, DB_USERNAME, DB_PASSWORD);
m_con.setAutoCommit(true);
}
catch (Exception e)
{
throw ensureRuntimeException(e, "Connection failed");
}
}
// ---- accessors -------------------------------------------------------
/**
* Obtain the name of the table this CacheStore is persisting to.
*
* @return the name of the table this CacheStore is persisting to
*/
public String getTableName()
{
return m_sTableName;
}
/**
* Obtain the connection being used to connect to the database.
*
* @return the connection used to connect to the database
*/
public Connection getConnection()
{
return m_con;
}
// ----- CacheStore Interface --------------------------------------------
/**
* Return the value associated with the specified key, or null if the
* key does not have an associated value in the underlying store.
*
* @param oKey key whose associated value is to be returned
*
* @return the value associated with the specified key, or
* <tt>null</tt> if no value is available for that key
*/
public Object load(Object oKey)
{
Object oValue = null;
Connection con = getConnection();
String sSQL = "SELECT id, value FROM " + getTableName()
+ " WHERE id = ?";
try
{
PreparedStatement stmt = con.prepareStatement(sSQL);
stmt.setString(1, String.valueOf(oKey));
ResultSet rslt = stmt.executeQuery();
if (rslt.next())
{
oValue = rslt.getString(2);
if (rslt.next())
{
throw new SQLException("Not a unique key: " + oKey);
}
}
stmt.close();
}
catch (SQLException e)
{
throw ensureRuntimeException(e, "Load failed: key=" + oKey);
}
return oValue;
}
/**
* Store the specified value under the specific key in the underlying
* store. This method is intended to support both key/value creation
* and value update for a specific key.
*
* @param oKey key to store the value under
* @param oValue value to be stored
*
* @throws UnsupportedOperationException if this implementation or the
* underlying store is read-only
*/
public void store(Object oKey, Object oValue)
{
Connection con = getConnection();
String sTable = getTableName();
String sSQL;
// the following is very inefficient; it is recommended to use DB
// specific functionality that is, REPLACE for MySQL or MERGE for Oracle
if (load(oKey) != null)
{
// key exists - update
sSQL = "UPDATE " + sTable + " SET value = ? where id = ?";
}
else
{
// new key - insert
sSQL = "INSERT INTO " + sTable + " (value, id) VALUES (?,?)";
}
try
{
PreparedStatement stmt = con.prepareStatement(sSQL);
int i = 0;
stmt.setString(++i, String.valueOf(oValue));
stmt.setString(++i, String.valueOf(oKey));
stmt.executeUpdate();
stmt.close();
}
catch (SQLException e)
{
throw ensureRuntimeException(e, "Store failed: key=" + oKey);
}
}
/**
* Remove the specified key from the underlying store if present.
*
* @param oKey key whose mapping is to be removed from the map
*
* @throws UnsupportedOperationException if this implementation or the
* underlying store is read-only
*/
public void erase(Object oKey)
{
Connection con = getConnection();
String sSQL = "DELETE FROM " + getTableName() + " WHERE id=?";
try
{
PreparedStatement stmt = con.prepareStatement(sSQL);
stmt.setString(1, String.valueOf(oKey));
stmt.executeUpdate();
stmt.close();
}
catch (SQLException e)
{
throw ensureRuntimeException(e, "Erase failed: key=" + oKey);
}
}
/**
* Remove the specified keys from the underlying store if present.
*
* @param colKeys keys whose mappings are being removed from the cache
*
* @throws UnsupportedOperationException if this implementation or the
* underlying store is read-only
*/
public void eraseAll(Collection colKeys)
{
throw new UnsupportedOperationException();
}
/**
* Return the values associated with each the specified keys in the
* passed collection. If a key does not have an associated value in
* the underlying store, then the return map does not have an entry
* for that key.
*
* @param colKeys a collection of keys to load
*
* @return a Map of keys to associated values for the specified keys
*/
public Map loadAll(Collection colKeys)
{
throw new UnsupportedOperationException();
}
/**
* Store the specified values under the specified keys in the underlying
* store. This method is intended to support both key/value creation
* and value update for the specified keys.
*
* @param mapEntries a Map of any number of keys and values to store
*
* @throws UnsupportedOperationException if this implementation or the
* underlying store is read-only
*/
public void storeAll(Map mapEntries)
{
throw new UnsupportedOperationException();
}
/**
* Iterate all keys in the underlying store.
*
* @return a read-only iterator of the keys in the underlying store
*/
public Iterator keys()
{
Connection con = getConnection();
String sSQL = "SELECT id FROM " + getTableName();
List list = new LinkedList();
try
{
PreparedStatement stmt = con.prepareStatement(sSQL);
ResultSet rslt = stmt.executeQuery();
while (rslt.next())
{
Object oKey = rslt.getString(1);
list.add(oKey);
}
stmt.close();
}
catch (SQLException e)
{
throw ensureRuntimeException(e, "Iterator failed");
}
return list.iterator();
}
// ----- data members ---------------------------------------------------
/**
* The connection.
*/
protected Connection m_con;
/**
* The db table name.
*/
protected String m_sTableName;
/**
* Driver class name.
*/
private static final String DB_DRIVER = "org.gjt.mm.mysql.Driver";
/**
* Connection URL.
*/
private static final String DB_URL = "jdbc:mysql://localhost:3306/CacheStore";
/**
* User name.
*/
private static final String DB_USERNAME = "root";
/**
* Password.
*/
private static final String DB_PASSWORD = null;
}
この項では、制御可能なキャッシュ・ストアの実装を紹介します。このシナリオでは、更新された値をデータ・ストアに書き込む時点をアプリケーションによって制御できます。このシナリオは、起動時にデータ・ストアからキャッシュへの初期移入を行う際に最も一般的に使用されます。起動時には、キャッシュの値を元のデータ・ストアに書き込む必要がありません。このような書込みはリソースの浪費になります。
例14-4のMain.javaファイルは、次のような制御可能なキャッシュ・ストアの2種類の操作方法を示しています。
制御可能なキャッシュ(別のサービスに配置されている必要があります)を使用して、キャッシュ・ストアの有効化または無効化を行います。これは、ControllableCacheStore1クラスで示されます。
CacheStoreAwareインタフェースを使用して、キャッシュに追加されたオブジェクトで記憶域が不要なことを示します。これは、ControllableCacheStore2クラスで示されます。
ControllableCacheStore1とControllableCacheStore2は両方とも、com.tangosol.net.cache.AbstractCacheStoreクラスを拡張します。このヘルパー・クラスによって、storeAll操作およびeraseAll操作の非最適実装が行われます。
CacheStoreAware.javaファイルは、キャッシュに追加されるオブジェクトをデータベースに格納しないことを示すことができるインタフェースです。
キャッシュ構成のサンプルについては、「データベースのキャッシュ」を参照してください。
例14-4は、Main.javaインタフェースの一覧を示しています。
例14-4 Main.java: 制御可能なCacheStoreとの相互作用
import com.tangosol.net.CacheFactory;
import com.tangosol.net.NamedCache;
import com.tangosol.net.cache.AbstractCacheStore;
import com.tangosol.util.Base;
import java.io.Serializable;
import java.util.Date;
public class Main extends Base
{
/**
* A cache controlled CacheStore implementation
*/
public static class ControllableCacheStore1 extends AbstractCacheStore
{
public static final String CONTROL_CACHE = "cachestorecontrol";
String m_sName;
public static void enable(String sName)
{
CacheFactory.getCache(CONTROL_CACHE).put(sName, Boolean.TRUE);
}
public static void disable(String sName)
{
CacheFactory.getCache(CONTROL_CACHE).put(sName, Boolean.FALSE);
}
public void store(Object oKey, Object oValue)
{
Boolean isEnabled = (Boolean) CacheFactory.getCache(CONTROL_CACHE).get(m_sName);
if (isEnabled != null && isEnabled.booleanValue())
{
log("controllablecachestore1: enabled " + oKey + " = " + oValue);
}
else
{
log("controllablecachestore1: disabled " + oKey + " = " + oValue);
}
}
public Object load(Object oKey)
{
log("controllablecachestore1: load:" + oKey);
return new MyValue1(oKey);
}
public ControllableCacheStore1(String sName)
{
m_sName = sName;
}
}
/**
* a valued controlled CacheStore implementation that
* implements the CacheStoreAware interface
*/
public static class ControllableCacheStore2 extends AbstractCacheStore
{
public void store(Object oKey, Object oValue)
{
boolean isEnabled = oValue instanceof CacheStoreAware ? !((CacheStoreAware) oValue).isSkipStore() : true;
if (isEnabled)
{
log("controllablecachestore2: enabled " + oKey + " = " + oValue);
}
else
{
log("controllablecachestore2: disabled " + oKey + " = " + oValue);
}
}
public Object load(Object oKey)
{
log("controllablecachestore2: load:" + oKey);
return new MyValue2(oKey);
}
}
public static class MyValue1 implements Serializable
{
String m_sValue;
public String getValue()
{
return m_sValue;
}
public String toString()
{
return "MyValue1[" + getValue() + "]";
}
public MyValue1(Object obj)
{
m_sValue = "value:" + obj;
}
}
public static class MyValue2 extends MyValue1 implements CacheStoreAware
{
boolean m_isSkipStore = false;
public boolean isSkipStore()
{
return m_isSkipStore;
}
public void skipStore()
{
m_isSkipStore = true;
}
public String toString()
{
return "MyValue2[" + getValue() + "]";
}
public MyValue2(Object obj)
{
super(obj);
}
}
public static void main(String[] args)
{
try
{
// example 1
NamedCache cache1 = CacheFactory.getCache("cache1");
// disable cachestore
ControllableCacheStore1.disable("cache1");
for(int i = 0; i < 5; i++)
{
cache1.put(new Integer(i), new MyValue1(new Date()));
}
// enable cachestore
ControllableCacheStore1.enable("cache1");
for(int i = 0; i < 5; i++)
{
cache1.put(new Integer(i), new MyValue1(new Date()));
}
// example 2
NamedCache cache2 = CacheFactory.getCache("cache2");
// add some values with cachestore disabled
for(int i = 0; i < 5; i++)
{
MyValue2 value = new MyValue2(new Date());
value.skipStore();
cache2.put(new Integer(i), value);
}
// add some values with cachestore enabled
for(int i = 0; i < 5; i++)
{
cache2.put(new Integer(i), new MyValue2(new Date()));
}
}
catch(Throwable oops)
{
err(oops);
}
finally
{
CacheFactory.shutdown();
}
}
}
例14-5は、CacheStoreAware.javaインタフェースの一覧を示しています。
CacheStoreを実装する場合は、次のことに注意してください。
すべてのCacheStore操作は、冪等になるように(つまり、望ましくない副作用なしに繰り返し処理できるように)設計する必要があります。ライトスルーおよびライトビハインド・キャッシュの場合、フェイルオーバー処理時にキャッシュ更新のデータベース部分を再試行することにより、低コストのフォルト・トレランスを提供できます。ライトビハインド・キャッシングの場合、冪等性によって、データ整合性に影響を与えずに、複数のキャッシュ更新を単一のCacheStoreの起動にまとめることができます。
アプリケーションにライトビハインド・キャッシングの要件があっても、書込み組合せを回避する必要がある場合(たとえば、監査のため)、バージョニングされたキャッシュ・キーを作成する必要があります(たとえば、シーケンスIDを持つ通常の主キーを組み合せて)。
Coherenceでは、複数のCacheStoreインスタンス間での2フェーズのCacheStore操作はサポートされません。つまり、2つのキャッシュ・エントリが更新され、別々のキャッシュ・サーバーにあるCacheStoreモジュールへのコールがトリガーされる場合、片方のデータベース更新は成功し、もう一方は失敗する可能性があります。この場合、アプリケーション・サーバーのトランザクション・マネージャで、キャッシュアサイド・アーキテクチャ(キャッシュとデータベースを、単一トランザクション内の2つの別個のコンポーネントとして更新する)を使用することをお薦めします。多くの場合、論理コミットの失敗がないように(当然、サーバー障害が発生しないようにもする)データベース・スキーマを設計することは可能です。ライトビハインド・キャッシングでは、データベースの動作によりputが影響を受けないため、この問題は回避されます(根本的な問題は設計プロセスの初期段階で解決しておく必要があります)。
キャッシュ問合せでは、キャッシュに格納されているデータのみを操作し、欠落データまたは欠落する可能性のあるデータをロードするためにCacheStoreをトリガーすることはありません。そのため、CacheStoreでバックアップされたキャッシュに問い合せるアプリケーションは、その問合せに必要なすべてのデータがあらかじめロードされていることを確認する必要があります。効率性を考慮して、アプリケーションの起動時にデータ・セットをデータベースから直接キャッシュに送信して、ほとんどのバルク・ロード操作を実行しておく必要があります(NamedCache.putAll()を使用して、データのブロックをキャッシュにまとめる)。ローダー処理は、制御可能なCachestoreパターンを使用して、データベースにバックアップされた循環更新を無効にする必要があります。CacheStoreは、起動サービス(クラスタ全体にエージェントを送信して、各JVMのローカル・フラグを変更する)を使用するか、レプリケーション・キャッシュ(別のキャッシュ・サービス)に値を設定して、CacheStoreメソッドの起動ごとにそれを読み取ることによって管理できます(一般的なデータベース操作と比較すると、オーバーヘッドが最低限に保たれる)。Coherenceのクラスタ化されたJMX機能により、カスタムMBeanも簡単に実装できます。
CacheStore実装では、ホスティングしているキャッシュ・サービスをコールバックしないでください。これには、内部的にCoherenceキャッシュ・サービスを参照するORMソリューションも含まれます。別のキャッシュ・サービス・インスタンスをコールすることも可能ですが、深くネストされたコールは避けるように注意する必要があります。コールのそれぞれがキャッシュ・サービス・スレッドを消費するため、キャッシュ・サービス・スレッド・プールを使用し尽くすと、デッドロックが発生する可能性があります。
キャッシュ・エントリのクラス(値オブジェクト、データ転送オブジェクトとも呼ばれる)は、キャッシュ・サーバー・クラスパスに置く必要があります(キャッシュ・サーバーは、キャッシュ・エントリをシリアライズ/デシリアライズしてCacheStoreモジュールと相互作用する必要があるため)。
キャッシュがライトビハインドとして構成され、<write-batch-factor>が構成されている場合に使用される可能性が最も高いのは、CacheStore.storeAllメソッドです。Coherenceでは、CacheLoader.loadAllメソッドも使用されます。同様の理由により、その最初の使用にはリフレッシュアヘッドを有効にする必要がある場合があります。