パフォーマンスを向上させるための汎用キャッシュサービスを開発する

同僚が世界のすべての国のリストを要求したとします。あなたは地理の専門家ではないので、国連のWebサイトにアクセスし、リストをダウンロードして、彼女のために印刷します。しかし、彼女はリストを調べたいだけです。彼女は実際にそれを持っていません。最後に必要なのは机の上の別の紙なので、リストをシュレッダーに送ります。

1日後、別の同僚が同じことを要求します。それは、世界のすべての国のリストです。リストを保持していないことで自分を罵倒し、再び国連のWebサイトに戻ってきます。このウェブサイトへの訪問で、あなたは国連が半年ごとに国リストを更新していることに気づきました。同僚のリストをダウンロードして印刷します。彼はそれを見て、ありがとう、そして再び、あなたにリストを残します。今回は、添付の付箋にメッセージを添えてリストを提出し、6か月後に破棄するように通知します。

案の定、今後数週間にわたって、同僚は何度も何度もリストを要求し続けます。 Webサイトからドキュメントを抽出するよりもすばやくドキュメントをファイリングキャビネットから抽出できるため、ドキュメントをファイリングしたことを祝福します。あなたのファイリングキャビネットのコンセプトが流行しています。すぐに誰もがあなたのキャビネットにアイテムを入れ始めます。キャビネットが乱雑になるのを防ぐために、キャビネットを使用するためのガイドラインを設定します。ファイリングキャビネットマネージャーとしての公式の立場で、すべてのドキュメントにラベルとポストイットノートを配置するように同僚に指示します。これにより、ドキュメントとその廃棄/有効期限が識別されます。ラベルは、同僚が探しているドキュメントを見つけるのに役立ちます。付箋は、情報が最新であるかどうかを示します。

ファイリングキャビネットは非常に人気が高くなり、すぐに新しいドキュメントをファイリングできなくなります。何を捨て、何を保持するかを決める必要があります。期限切れの書類はすべて捨てても、キャビネットはまだ紙で溢れています。どの期限の切れていないドキュメントを破棄するかをどのように決定しますか?最も古いドキュメントを破棄しますか?最も使用頻度の低い、または最も最近使用されていないものを破棄できます。どちらの場合も、各ドキュメントにアクセスしたときにリストされたログが必要になります。または、他の決定要因に基づいて、破棄するドキュメントを決定することもできます。決定は純粋に個人的なものです。

上記の現実世界のアナロジーをコンピュータの世界に関連付けるために、ファイリングキャビネットはキャッシュとして動作します。これは、時々メンテナンスが必要な高速メモリです。キャッシュ内のドキュメントはキャッシュされたオブジェクトであり、そのすべてがキャッシュマネージャーであるユーザーが設定した標準に準拠していますキャッシュをクリーンアップするプロセスは、パージと呼ばれます。キャッシュされたアイテムは一定の時間が経過するとパージされるため、キャッシュは時限キャッシュと呼ばれます。

この記事では、匿名のバックグラウンドスレッドを使用して期限切れのアイテムをパージする100%純粋なJavaキャッシュを作成する方法を学習します。さまざまな設計に伴うトレードオフを理解しながら、このようなキャッシュを設計する方法を説明します。

キャッシュを構築する

十分なファイリングキャビネットの例え:ウェブサイトに移りましょう。 Webサイトサーバーもキャッシュを処理する必要があります。サーバーは、他の要求と同じ情報の要求を繰り返し受信します。次のタスクでは、世界最大の企業の1つ向けにインターネットアプリケーションを構築する必要があります。多くの眠れない夜とあまりにも多くのJoltコーラを含む、4か月の開発の後、アプリケーションは1,000人のユーザーで開発テストに入ります。開発テストに続いて、5,000ユーザーの認定テストとそれに続く20,000ユーザーの本番環境への展開が行われます。ただし、200人のユーザーのみがアプリケーションをテストしているときにメモリ不足エラーが発生すると、開発テストは停止します。

パフォーマンス低下の原因を特定するには、プロファイリング製品を使用して、サーバーがデータベースResultSetの複数のコピーをロードし、それぞれに数千のレコードがあることを発見します。レコードは製品リストを構成します。さらに、製品リストはすべてのユーザーで同一です。製品リストがパラメーター化されたクエリの結果である場合のように、リストはユーザーに依存しません。リストの1つのコピーですべての同時ユーザーにサービスを提供できるとすぐに判断したので、それをキャッシュします。

ただし、次のような複雑さを含む多くの質問が発生します。

  • 製品リストが変更された場合はどうなりますか?キャッシュはどのようにしてリストを期限切れにすることができますか?製品リストが期限切れになる前にキャッシュに保持する期間をどのように知ることができますか?
  • 2つの異なる製品リストが存在し、2つのリストが異なる間隔で変更される場合はどうなりますか?各リストを個別に期限切れにすることはできますか、それともすべて同じ保存期間が必要ですか?
  • キャッシュが空で、2人のリクエスターがまったく同時にキャッシュを試行した場合はどうなりますか?彼らは両方ともそれが空であることに気付いたとき、彼ら自身のリストを作成し、それから彼らのコピーをキャッシュに入れようとしますか?
  • アイテムがアクセスされずに数か月間キャッシュにある場合はどうなりますか?彼らは記憶を食い尽くしませんか?

これらの課題に対処するには、ソフトウェアキャッシュサービスを構築する必要があります。

ファイリングキャビネットの例えでは、人々は文書を検索するときに常に最初にキャビネットをチェックしました。ソフトウェアは同じ手順を実装する必要があります。リクエストは、データベースから新しいリストをロードする前に、キャッシュサービスをチェックする必要があります。ソフトウェア開発者としてのあなたの責任は、データベースにアクセスする前にキャッシュにアクセスすることです。製品リストがすでにキャッシュにロードされている場合は、有効期限が切れていない限り、キャッシュされたリストを使用します。製品リストがキャッシュにない場合は、データベースからロードしてすぐにキャッシュします。

注:キャッシングサービスの要件とコードに進む前に、以下のサイドバー「キャッシングとプーリング」を確認することをお勧めします。関連する概念あるプーリングについて説明します。

要件

優れた設計原則に沿って、この記事で開発するキャッシングサービスの要件リストを定義しました。

  1. すべてのJavaアプリケーションがキャッシュサービスにアクセスできます。
  2. オブジェクトはキャッシュに配置できます。
  3. オブジェクトはキャッシュから抽出できます。
  4. キャッシュされたオブジェクトは、いつ期限切れになるかを自分で判断できるため、最大限の柔軟性が得られます。同じ有効期限式を使用してすべてのオブジェクトを期限切れにするキャッシュサービスは、キャッシュされたオブジェクトの最適な使用を提供できません。たとえば、商品リストは毎日変更される可能性がありますが、店舗の場所のリストは月に1回しか変更されない可能性があるため、このアプローチは大規模システムでは不十分です。
  5. 低優先度で実行されるバックグラウンドスレッドは、期限切れのキャッシュされたオブジェクトを削除します。
  6. キャッシングサービスは、最も使用頻度の低い(LRU)または使用頻度の低い(LFU)パージメカニズムを使用して、後で拡張できます。

実装

要件1を満たすために、100%純粋なJava環境を採用しています。キャッシングサービスでパブリックメソッドgetsetメソッドを提供することで、要件2と3も満たします。

要件4の説明に進む前に、キャッシュマネージャーで匿名スレッドを作成することで要件5を満たすことを簡単に説明します。このスレッドは静的ブロックで始まります。また、LRUおよびLFUアルゴリズムを実装するために後でコードが追加されるポイントを特定することにより、要件6を満たします。これらの要件については、この記事の後半で詳しく説明します。

ここで、要件4に戻ります。ここで、物事が面白くなります。キャッシュされたすべてのオブジェクトが期限切れかどうかを自分で判断する必要がある場合は、オブジェクトに期限切れかどうかを確認する方法が必要です。つまり、キャッシュ内のオブジェクトはすべて特定のルールに準拠している必要があります。あなたはインターフェースを実装することによってJavaでそれを達成します。

キャッシュに配置されるオブジェクトを管理するルールから始めましょう。

  1. すべてのオブジェクトにisExpired()は、ブール値を返す、というパブリックメソッドが必要です。
  2. すべてのオブジェクトにgetIdentifier()は、キャッシュ内の他のすべてのオブジェクトと区別するオブジェクトを返す、と呼ばれるパブリックメソッドが必要です。

Note: Before jumping straight into the code, you must understand that you can implement a cache in many ways. I have found more than a dozen different implementations. Enhydra and Caucho provide excellent resources that contain several cache implementations.

You'll find the interface code for this article's caching service in Listing 1.

Listing 1. Cacheable.java

/** * Title: Caching Description: This interface defines the methods, which must be implemented by all objects wishing to be placed in the cache. * * Copyright: Copyright (c) 2001 * Company: JavaWorld * FileName: Cacheable.java @author Jonathan Lurie @version 1.0 */ public interface Cacheable { /* By requiring all objects to determine their own expirations, the algorithm is abstracted from the caching service, thereby providing maximum flexibility since each object can adopt a different expiration strategy. */ public boolean isExpired(); /* This method will ensure that the caching service is not responsible for uniquely identifying objects placed in the cache. */ public Object getIdentifier(); } 

Any object placed in the cache -- a String, for example -- must be wrapped inside an object that implements the Cacheable interface. Listing 2 is an example of a generic wrapper class called CachedObject; it can contain any object needed to be placed in the caching service. Note that this wrapper class implements the Cacheable interface defined in Listing 1.

Listing 2. CachedManagerTestProgram.java

/** * Title: Caching * Description: A Generic Cache Object wrapper. Implements the Cacheable interface * uses a TimeToLive stategy for CacheObject expiration. * Copyright: Copyright (c) 2001 * Company: JavaWorld * Filename: CacheManagerTestProgram.java * @author Jonathan Lurie * @version 1.0 */ public class CachedObject implements Cacheable { // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ /* This variable will be used to determine if the object is expired. */ private java.util.Date dateofExpiration = null; private Object identifier = null; /* This contains the real "value". This is the object which needs to be shared. */ public Object object = null; // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ public CachedObject(Object obj, Object id, int minutesToLive) { this.object = obj; this.identifier = id; // minutesToLive of 0 means it lives on indefinitely. if (minutesToLive != 0) { dateofExpiration = new java.util.Date(); java.util.Calendar cal = java.util.Calendar.getInstance(); cal.setTime(dateofExpiration); cal.add(cal.MINUTE, minutesToLive); dateofExpiration = cal.getTime(); } } // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ public boolean isExpired() { // Remember if the minutes to live is zero then it lives forever! if (dateofExpiration != null) { // date of expiration is compared. if (dateofExpiration.before(new java.util.Date())) { System.out.println("CachedResultSet.isExpired: Expired from Cache! EXPIRE TIME: " + dateofExpiration.toString() + " CURRENT TIME: " + (new java.util.Date()).toString()); return true; } else { System.out.println("CachedResultSet.isExpired: Expired not from Cache!"); return false; } } else // This means it lives forever! return false; } // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ public Object getIdentifier() { return identifier; } // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ } 

The CachedObject class exposes a constructor method that takes three parameters:

public CachedObject(Object obj, Object id, int minutesToLive) 

The table below describes those parameters.

Parameter descriptions of the CachedObject constructor
Name Type Description
Obj Object The object that is shared. It is defined as an object to allow maximum flexibility.
Id Object Id contains a unique identifier that distinguishes the obj parameter from all other objects residing in the cache. The caching service is not responsible for ensuring the uniqueness of the objects in the cache.
minutesToLive Int The number of minutes that the obj parameter is valid in the cache. In this implementation, the caching service interprets a value of zero to mean that the object never expires. You might want to change this parameter in the event that you need to expire objects in less than one minute.

The constructor method determines the expiration date of the object in the cache using a time-to-live strategy. As its name implies, time-to-live means that a certain object has a fixed time at the conclusion of which it is considered dead. By adding minutesToLive, the constructor's int parameter, to the current time, an expiration date is calculated. This expiration is assigned to the class variable dateofExpiration.

ここで、isExpired()メソッドはdateofExpiration、が現在の日時の前か後かを単純に判別する必要があります。日付が現在の時刻より前であり、キャッシュされたオブジェクトが期限切れであると見なされる場合、isExpired()メソッドはtrueを返します。日付が現在の時刻より後の場合、キャッシュされたオブジェクトは期限切れにならず、isExpired()falseを返します。もちろん、dateofExpirationがnullの場合(minutesToLiveゼロの場合)、isExpired()メソッドは常にfalseを返し、キャッシュされたオブジェクトが永久に存続することを示します。