为了创建可扩展、高性能的基于web的应用,asp.net提供一个称为数据缓存(data caching)的特性。数据缓存支持将频繁访问的数据对象可编程地存放在内存中。这一特性可扩展以广泛地提高查询oracle数据库中数据的asp.net应用的性能。本文讲述一个策略,可用于采用web farm环境中的asp.net web应用缓存oracle数据库数据。这个技巧允许在内存中缓存频繁访问的oracle数据库数据,而不是频繁访问数据库来取数据。这可以帮助避免到oracle数据库服务器的不必要的远路。进一步的,文章提出了一个保持缓存数据以使其始终与oracle数据同步的实现。
asp.net中的数据缓存
asp.net中的数据缓存由cache类和system.web.caching命名空间中的cachedependency类支持。cache类提供向缓存插入和从中取出数据的方法。cachedependency类允许为缓存中数据项的指定其依赖项。当我们用insert和add方法将项目加入缓存中,可以指定一个项目的过期(expiration)策略。我们可以用insert方法的absoluteexpiration属性来定义缓存中一个项目的生命期。这个属性允许你指定相应数据项过期的准确时间。也可以使用slidingexpiration属性来指定项目过期的流逝时间(基于它被访问的时间)。一旦一个项目过期,它从缓存中被清除。除非它再次被加入缓存中,否则再试图访问,将返回一个空值。
设定缓存依赖
asp.net使我们可以基于一个外部文件、目录或另一个缓存项来定义一个缓存项的依赖,即所谓文件依赖与键依赖。若一个依赖项改变,缓存项自动失效并被从缓存中清除。当相应的数据源改变时,我们可以用这种方法来从缓存中删除项目。例如,若我们的应用从一个xml文件中取数据并显示在一个表格(grid)中,我们可以把文件中的数据存放到缓存中,并设定缓存依赖于那个xml文件。当xml文件被更新,数据项就从缓存中被清除出去。这一事件发生时,应用重新读入xml文件,最新的数据项副本被再一次插入缓存中。进一步的,回调事件处理器可被设定为一个监听者,当缓存项被删除时得到通知。这使得我们不需要反复轮询缓存来确定数据项是否已无效。
oracle数据库上的asp.net缓存依赖
现在考虑这样一个情景:数据存放于oracle数据库中,一个asp.net应用通过ado.net来访问。进一步,我们假设数据库表中的数据一般是静态的,并被这个web应用频繁访问。表上的dml操作很少而对数据有很多select。这种情况是数据缓存技术的理想应用。但不幸的是,asp.net并不允许设定一个缓存项依赖于存放在数据库表中的数据。进一步,现实世界中,基于web的系统,web服务器和oracle数据库服务器总是会运行在不同的机器上,使得缓存无效操作更有挑战性。另外,多数基于web的应用采用web farms,同一个应用的实例在不同的web服务器上跑以负载均衡。这种情况使得数据库缓存问题稍稍复杂一些。
为了进一步研究上述问题的解决方案,我们举一个web应用的例子来说明如何实现。例子中,我们使用vb.net实现的asp.net应用,通过oracle data provider for .net (odp)来访问 oracle 9i数据库。
这个例子使用oracle数据库中一个名为employee的表。我们为该表上insert, update, delete设定触发器。这些触发器调用一个封装了一个java存储过程的pl/sql函数。这个java存储过程负责更新缓存依赖的文件。
asp.net tier的vb.net实现
我们设计了含一个回调方法的监听类来处理缓存项无效时的通知。这个回调方法removedcallback用一个代理(delegate)函数来注册。回调方法onremove的声明必须与cacheitemremovedcallback代理声明又相同的签名。
dim onremove as cacheitemremovedcallback = nothing
onremove = new cacheitemremovedcallback(addressof removedcallback)
监听事件处理方法removedcallback负责处理数据库触发器的通知,其定义如下。若缓存项失效,可用数据库方法调用getrecordfromdatabase()从数据库取出数据。参数”key”指从缓存中删除的项的索引位置。参数”value”指从缓存中删除的数据对象。参数”cacheitemremovedreason”指从缓存中删除数据项的原因。
publicsub removedcallback(byval key asstring, byval value asobject, 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)
endsub
方法getrecordfromdatabase()负责查询数据库表employee并返回一个dataview对象引用。它使用一个名为getemployee的存储过程来抽象从employee表中取数据的sql。这个方法有一个名为p_empid的参数,表示employee的主键。
publicfunction 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
thrownew exception(“exception in database tier method getrecordfromdatabase () ” + ex.message, ex)
finally
try
cmd.dispose()
catch ex as exception
finally
cmd = nothing
endtry
try
con.close()
catch ex as exception
finally
con = nothing
endtry
endtry
endfunction
函数getdatabaseconnection接受一个连接字符串(connection stirng)为参数,返回一个oracleconnection对象引用。
publicfunction 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
thrownew exception(“exception in database tier method getoracleconnection() ” + ex.message, ex)
endtry
endfunction
oracle数据库tier实现
定义employee表上dml事件的触发器体如下。这个触发器简单的调用一个pl/sql包裹函数来更新名为tblemployee.txt的操作系统文件。文件副本在两台机器(机器1和机器2)上更新。两台机器运行同一个web应用的不同实例来均衡负载。这里administrator指oracle数据库的方案(schema)对象所有者。
begin
administrator.plfile(machine1\\download\\ tblemployee.txt);
administrator.plfile(machine2\\download\\ tblemployee.txt);
end;
为更新缓存依赖文件,我们需要写一个c函数或java存储过程。我们的例子中选择了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 farm部署中的oracle数据缓存
正如我们讨论的例子中所示,web服务器1和机器2构成了一个web farm来为我们的web应用提供负载均衡。每台机器运行同一个web应用的一个实例。在这个情况下,每个实例可以拥有自己的存放在cache对象中的缓存数据副本。当employee表改变,相应的数据库触发器更新两台机器上的文件tblemployee.txt。每个实例都指定一个到tblemployee.txt的缓存依赖,web farm的两个实例都可以正确更新,使得两个实例上的数据缓存可以和数据库表employee保持同步。
结论
数据缓存是优化oracle数据库上asp.net应用的有效技巧。尽管asp.net不允许设定缓存的数据库依赖,oracle触发器协同java存储过程可以扩展asp.net缓存的威力从而允许oracle数据库缓存。这个技巧也可以适用于web farm部署。