はじめに
スケーラブルで高性能な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:\download\tblemployee.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('machine1\\download\\ tblemployee.txt'); administrator.plfile('machine2\\download\\ 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ファーム配備にも応用できます。