はじめに
スケーラブルで高性能なWebベースアプリケーションを構築するために、ASP.NETには「データキャッシング」の機能が用意されています。データキャッシングとは、アクセス頻度の高いデータオブジェクトをメモリ内に記憶しておく機能です。この機能を、Oracleデータベース内のデータをクエリするASP.NETアプリケーションで利用すれば、大幅なパフォーマンスの改善が見込めます。本稿では、Webファーム環境に配備されるASP.NET Webアプリケーションを前提として、Oracleデータベースデータのキャッシング戦略を考えます。アクセス頻度の高いデータをメモリにキャッシュしておくので、データが必要となるたびにOracleデータベースまでデータを取りにいかなくても済み、当然、Oracleデータベースサーバーへの往来のかなりの部分が不必要になります。本稿では、さらに、キャッシュ内のデータとOracleデータベース内の対応データの常時同期のアイデアを提案し、そのための実装例を示します。
ASP.NETでのデータキャッシング
ASP.NETでのデータキャッシングには、System.Web.Caching名前空間のCacheクラスとCacheDependencyクラスを使用します。Cacheクラスには、キャッシュにデータを蓄えるメソッドと、そこからデータ取り出すメソッドがあります。CacheDependencyクラスでは、キャッシュに記憶されたデータ項目に対して依存関係を指定できます。まず、ある項目をInsertメソッドまたはAddメソッドでキャッシュに追加するときに、期限ポリシーを指定できます。キャッシュ内の項目の寿命を指定するには、InsertメソッドのabsoluteExpirationパラメータを使用して、当該データ項目が期限切れとなる正確な日時を指定することができるほか、slidingExpirationパラメータを使用して、当該項目の最終アクセス日時に基づき、そこからの経過時間によって期限切れの日時を指定することもできます。期限切れとなった項目はキャッシュから取り除かれ、その後にアクセスが試みられると、null値が返されます(もちろん、それ以前に同じ項目が再びキャッシュに追加されている場合は別です)。
キャッシュに対する依存関係の指定
ASP.NETでは、キャッシュ内の項目の依存関係を、外部ファイル、ディレクトリ、または別のキャッシュ内項目に基づいて定義できます。これをファイル依存関係やキー依存関係と呼び出します。依存関係が変化すると、キャッシュ内の項目は自動的に無効化され、キャッシュから取り除かれます。したがって、データソースに変化があったときは、この方法でキャッシュから対応項目を除去できます。たとえば、XMLファイルからデータを取り出し、それをグラフに表示するアプリケーションを書くとします。その際、ファイルから取り出したデータをキャッシュに保管し、XMLファイルに対するキャッシュ依存関係を指定しておくと、XMLファイルが更新されたときに、当該データ項目がキャッシュから取り除かれます。XMLファイルの更新というイベントが発生すると、アプリケーションは再びXMLファイルを読みにいき、データ項目の最新コピーが再度キャッシュに挿入されます。さらに、コールバックイベントハンドラを、データ項目がキャッシュから除去されるときに通知を受け取るためのリスナーとして指定することもできます。これにより、データ項目が無効化されたかどうかをキャッシュのポーリングで調べる手間が不要になります。
Oracleデータベースに対するASP.NETキャッシュ依存関係
Oracleデータベースに格納されているデータに、ASP.NETアプリケーションからADO.NETを用いてアクセスするシナリオを考えてみましょう。このデータベーステーブル内のデータは概ね静的ですが、Webアプリケーションから頻繁に参照されるとします。つまり、テーブルに対するDML操作はきわめて少ないものの、データに対するSelect操作は非常に多いものと仮定します。これは、データキャッシングには理想的なシナリオです。しかし、残念ながら、キャッシュ項目がデータベーステーブル内のデータに依存するような依存関係は、ASP.NETでは許されていません。また、現実世界のWebベースシステムでは、WebサーバーとOracleデータベースサーバーが異なるマシン上で稼動している可能性もあり、その場合、この方式によるキャッシュの無効化はなかなか難しくなります。さらに、ほとんどのWebベースアプリケーションはWebファームに配備されており、負荷分散のため、同一アプリケーションの複数インスタンスが複数のWebサーバーで実行されています。そのため、データベースキャッシングの問題はなかなか複雑です。
この問題の解決策を探るために、1つのWebアプリケーション例を使い、いったいどう実装できるものかを考えてみることにしましょう。使用する例は、VB.NETで実装したASP.NETアプリケーションで、これがOracle Data Provider for .NET(ODP.NET)を用いてOracle 9iデータベースと通信します。
さて、この例では、Oracleデータベース内にある「Employee」という名前のテーブルを使用することとし、その「Employee」テーブルに対する挿入・更新・削除のためのトリガーを定義します。このトリガーはPL/SQL関数を呼び出しますが、それはJavaストアドプロシージャの単なるラッパーにすぎず、そのJavaストアドプロシージャがキャッシュ依存関係ファイルの更新を担当します。
VB.NETを用いたASP.NET層の実装
ASP.NET層に置くリスナークラスには、キャッシュ項目の無効化通知を処理するコールバックメソッドを用意します。
RemovedCallbackコールバックメソッドの登録には、デリゲートを使用します。onRemoveコールバックメソッドの宣言のシグネチャは、CacheItemRemovedCallbackデリゲート宣言と同じでなければなりません。
Dim onRemove As CacheItemRemovedCallback = Nothing
onRemove = New CacheItemRemovedCallback(AddressOf RemovedCallback)
データベーストリガーからの通知を処理するRemovedCallbackリスナーイベントハンドラメソッドの定義を下に示します。キャッシュ項目が無効化されると、getRecordFromdatabase()データベースメソッドが呼び出され、データをデータベースから取り除きます。keyパラメータはキャッシュから取り除く項目のインデックス位置であり、valueパラメータはキャッシュから取り除くデータオブジェクトです。CacheItemRemovedReasonパラメータは、キャッシュからそのデータ項目を取り除く理由です。
Public Sub RemovedCallback(ByVal key As String, _
ByVal value As Object, _
ByVal reason As CacheItemRemovedReason)
Dim Source As DataView
Source = getRecordFromdatabase()
Cache.Insert("employeeTable ", Source, New _
System.Web.Caching.CacheDependency( _
"d:downloadblemployee.txt"), _
Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration, _
CacheItemPriority.Normal, onRemove)
End Sub
getRecordFromdatabase()メソッドは、「Employee」データベーステーブルをクエリし、DataViewオブジェクト参照を返します。このとき、getEmployeeというストアドプロシージャを使用してSQLを抽象化し、「Employee」テーブルからデータを取り出します。このメソッドは、「Employee」テーブルのプライマリキーを表すp_empidというパラメータを要求します。
Public Function getRecordFromdatabase (ByVal p_empid As Int32) _
As DataView
Dim con As OracleConnection = Nothing
Dim cmd As OracleCommand = Nothing
Dim ds As DataSet = Nothing
Try
con = getDatabaseConnection( _
"UserId=scott;Password=tiger;Data Source=testingdb;")
cmd = New OracleCommand("Administrator.getEmployee", con)
cmd.CommandType = CommandType.StoredProcedure
cmd.Parameters.Add(New OracleParameter( _
"employeeId", OracleDbType.Int64)).Value = p_empid
Dim param AsNew OracleParameter("RC1", OracleDbType.RefCursor)
cmd.Parameters.Add(param).Direction = ParameterDirection.Output
Dim myCommand AsNew OracleDataAdapter(cmd)
ds = New DataSet
myCommand.Fill(ds)
Dim table As DataTable = ds.Tables(0)
Dim index As Int32 = table.Rows.Count
Return ds.Tables(0).DefaultView
Catch ex As Exception
Throw New Exception("Exception in Database Tier Method " _
+"getRecordFromdatabase () " + ex.Message, ex)
Finally
Try
cmd.Dispose()
Catch ex As Exception
Finally
cmd = Nothing
End Try
Try
con.Close()
Catch ex As Exception
Finally
con = Nothing
End Try
End Try
End Function
getDatabaseConnection関数はコネクション文字列を引数として受け入れ、OracleConnectionオブジェクト参照を返します。
Public Function getDatabaseConnection( _
ByVal strconnection as string) As OracleConnection
Dim con As Oracle.DataAccess.Client.OracleConnection = Nothing
Try
con = New Oracle.DataAccess.Client.OracleConnection
con.ConnectionString = strconnection
con.Open()
Return con
Catch ex As Exception
Throw New Exception( _
"Exception in Database Tier Method getOracleConnection() " _
+ ex.Message, ex)
End Try
End Function
Oracleデータベース層の実装
「Employee」テーブルで起こるDMLイベントのために定義されたトリガーの本体を、下に示します。このトリガーはPL/SQLラッパー関数を呼ぶだけのもので、この関数が「tblemployee.txt」というオペレーティングシステムファイルを更新します。「tblemployee.txt」ファイルは、負荷分散のために同じWebアプリケーションのインスタンスをそれぞれ別々に実行しているmachine1とmachine2という2台のマシン上にあり、その両方が更新されます。次のコードでadministratorとあるのは、Oracleデータベース内にあるスキーマオブジェクトの所有者を指しています。
begin
administrator.plfile(’machine1download tblemployee.txt’);
administrator.plfile(’machine2download tblemployee.txt’);
end;
キャッシュ依存関係ファイルの更新には、C関数かJavaストアドプロシージャを書かなければなりません。今回は、OracleデータベースサーバーにJVMが組み込まれていて、Javaストアドプロシージャを簡単に書けるという理由から、後者を選びました。Oracleインスタンスのシステムグローバルエリア(SGA)に、Javaプールとして十分なメモリを割り振っておいてください。updateFile静的メソッドは絶対パス名をパラメータとして受け入れ、適切なディレクトリにキャッシュ依存関係ファイルを作成します。旧ファイルが存在するときは、それを削除して、新しく作り直します。
import java.io.*;
public class UpdFile {
public static void updateFile(String filename) {
try {
File f = new File(filename);
f.delete();
f.createNewFile();
}
catch (IOException e)
{
// log exception
}
}
};
pl/sqlラッパーの実装を下に示します。このラッパー関数はファイル名をパラメータとして受け取り、JavaストアドプロシージャのupdateFileメソッドを呼び出します。
(p_filename IN VARCHAR2)
AS LANGUAGE JAVA
NAME ’UpdFile.updateFile (java.lang.String)’;
Webファーム配備でのデータベースキャッシング
上の例では、machine1とmachine2という2台のWebサーバーがWebファームを構成し、Webアプリケーションの負荷分散を行っていました。どちらのマシンで稼動しているのも、同じWebアプリケーションのインスタンスです。このシナリオでは、どちらのインスタンスも、Cacheオブジェクトに格納されているキャッシュデータの独自コピーを持っています。「Employee」テーブルに変更があると、対応データベーストリガーが両方のマシンにある「tblemployee.txt」ファイルを更新します。どちらのWebアプリケーションインスタンスも、ローカルの「tblemployee.txt」ファイルに対してキャッシュ依存関係を定めているので、Webファームを構成する両インスタンスのキャッシュが正しく更新され、どちらのインスタンスのデータキャッシュも「Employee」データベーステーブルとの同期を維持できます。
まとめ
Oracleデータベースを使用するASP.NETアプリケーションの最適化には、データキャッシングが効果的です。ASP.NETでは、キャッシュに対してデータベース依存関係を指定することが許されていませんが、OracleトリガーとJavaストアドプロシージャを併用することで、ASP.NETキャッシュの威力をOracleデータベースキャッシングにまで拡大できます。この手法は、Webファーム配備にも応用できます。