首页 > 开发 > PHP > 正文

通过缓存数据库结果提高PHP性能

2024-05-04 22:58:45
字体:
来源:转载
供稿:网友

 众所周知,缓存数据库查询的结果可以显著缩短脚本执行时间,并最大限度地减少数据库服务器上的负载。如果要处理的数据基本上是静态的,则该技术将非常有效。这是因为对远程数据库的许多数据请求最终可以从本地缓存得到满足,从而不必连接到数据库、执行查询以及获取结果。

  但当您使用的数据库与 web 服务器位于不同的计算机上时,缓存数据库结果集通常是一个不错的方法。不过,根据您的情况确定最佳的缓存策略却是一个难题。例如,对于使用最新数据库结果集比较重要的应用程序而言,时间触发的缓存方法(缓存系统常用的方法,它假设每次到达失效时间戳记时就重新生成缓存)可能并不是一个令人满意的解决方案。这种情况下,您需要采用一种机制,每当应用程序需要缓存的数据库数据发生更改时,该机制将通知该应用程序,以便该应用程序将缓存的过期数据与数据库保持一致。这种情况下使用“数据库更改通知”(一个新的 oracle 数据库 10g 第 2 版特性)将非常方便。

  “数据库更改通知”入门

  “数据库更改通知”特性的用法非常简单:创建一个针对通知执行的通知处理程序 – 一个 pl/sql 存储过程或客户端 oci 回调函数。然后,针对要接收其更改通知的数据库对象注册一个查询,以便每当事务更改其中的任何对象并提交时调用通知处理程序。通常情况下,通知处理程序将被修改的表的名称、所做更改的类型以及所更改行的行 id(可选)发送给客户端监听程序,以便客户端应用程序可以在响应中执行相应的处理。

  为了了解“数据库更改通知”特性的作用方式,请考虑以下示例。假设您的 php 应用程序访问 oe.orders 表中存储的订单以及 oe.order_items 中存储的订单项。鉴于很少更改已下订单的信息,您可能希望应用程序同时缓存针对 orders 和 order_items 表的查询结果集。要避免访问过期数据,您可以使用“数据库更改通知”,它可让您的应用程序方便地获知以上两个表中所存储数据的更改。

  您必须先将 change notification 系统权限以及 execute on dbms_changenotification 权限授予 oe 用户,才能注册对 orders 和 order_items 表的查询,以便接收通知和响应对这两个表所做的 dml 或 ddl 更改。为此,可以从 sql 命令行工具(如 sql*plus)中执行下列命令。

connect / as sysdba;
grant change notification to oe;
grant execute on dbms_change_notification to oe;

  确保将 init.ora 参数 job_queue_processes 设置为非零值,以便接收 pl/sql 通知。或者,您也可以使用下面的 alter system 命令:

alter system set "job_queue_processes"=2;  然后,在以 oe/oe 连接后,您可以创建一个通知处理程序。但首先,您必须创建将由通知处理程序使用的数据库对象。例如,您可能需要创建一个或多个数据库表,以便通知处理程序将注册表的更改记录到其中。在以下示例中,您将创建 nfresults 表来记录以下信息:更改发生的日期和时间、被修改的表的名称以及一个消息(说明通知处理程序是否成功地将通知消息发送给客户端)。

connect oe/oe;

create table nfresults (
operdate date, 
tblname varchar2(60), 
rslt_msg varchar2(100)
);
  在实际情况中,您可能需要创建更多表来记录通知事件以及所更改行的行 id 等信息,但就本文而言,nfresults 表完全可以满足需要。

  使用 utl_http 向客户端发送通知

  您可能还要创建一个或多个 pl/sql 存储过程,并从通知处理程序中调用这些存储过程,从而实现一个更具可维护性和灵活性的解决方案。例如,您可能要创建一个实现将通知消息发送给客户端的存储过程。“清单 1”是 pl/sql 过程 sendnotification。该过程使用 utl_httppl 程序包向客户端应用程序发送更改通知。

  清单 1. 使用 utl_http 向客户端发送通知

create or replace procedure sendnotification(url in varchar2, 
tblname in varchar2, order_id in varchar2) is
req   utl_http.req;
resp  utl_http.resp;
err_msg varchar2(100);
tbl varchar(60);
begin
tbl:=substr(tblname, instr(tblname, '.', 1, 1)+1, 60);
begin
req := utl_http.begin_request(url||order_id||'&'||'table='||tbl);
resp := utl_http.get_response(req);
insert into nfresults values(sysdate, tblname, resp.reason_phrase);
utl_http.end_response(resp);
exception when others then
err_msg := substr(sqlerrm, 1, 100);
insert into nfresults values(sysdate, tblname, err_msg);
end;
commit;
end;
/

  如“清单 1”所示,sendnotification 以 utl_http.begin_request 函数发出的 http 请求的形式向客户端发送通知消息。此 url 包含 orders 表中已更改行的 order_id。然后,它使用 utl_http.get_response 获取客户端发出的响应信息。实际上,sendnotification 并不需要处理客户端返回的整个响应,而是只获取一个在 resp 记录的 reason_phrase 字段中存储的简短消息(描述状态代码)。

  创建通知处理程序

  现在,您可以创建一个通知处理程序,它将借助于上面介绍的 sendnotification 过程向客户端发送更改通知。来看一看“清单 2”中的 pl/sql 过程 orders_nf_callback。

  清单 2. 处理对 oe.orders 表所做更改的通知的通知处理程序

create or replace procedure orders_nf_callback (ntfnds in sys.chnf$_desc) is
tblname varchar2(60);
numtables number;
event_type number;
row_id varchar2(20);
numrows number;
ord_id varchar2(12);
url varchar2(256) := 'http://webserverhost/phpcache/dropresults.php?order_no=';
begin
event_type := ntfnds.event_type;
numtables := ntfnds.numtables;
if (event_type = dbms_change_notification.event_objchange) then
for i in 1..numtables loop
tblname := ntfnds.table_desc_array(i).table_name;
if (bitand(ntfnds.table_desc_array(i).opflags, 
dbms_change_notification.all_rows) = 0) then
numrows := ntfnds.table_desc_array(i).numrows;
else
numrows :=0;
end if;
if (tblname = 'oe.orders') then
for j in 1..numrows loop
row_id := ntfnds.table_desc_array(i).row_desc_array(j).row_id;
select order_id into ord_id from orders where rowid = row_id;
sendnotification(url, tblname, ord_id); 
end loop;
end if;
end loop;
end if;
commit;
end;
/

  如“清单 2”所示,此通知处理程序将 sys.chnf$_desc 对象用作参数,然后使用它的属性获取该更改的详细信息。在该示例中,此通知处理程序将只处理数据库为响应对注册对象进行的 dml 或 ddl 更改(也就是说,仅当通知类型为 event_objchange 时)而发布的通知,并忽略有关其他数据库事件(如实例启动或实例关闭)的通知。从以上版本开始,处理程序可以处理针对 oe.orders 表中每个受影响的行发出的更改通知。在本文后面的“将表添加到现有注册”部分中,您将向处理程序中添加几行代码,以便它可以处理针对 oe.order_items 表中被修改的行发出的通知。
 为更改通知创建注册

  创建通知处理程序后,必须为其创建一个查询注册。对于本示例而言,您必须在注册过程中对 oe.order 表执行查询并将 orders_nf_callback 指定为通知处理程序。您还需要在 dbms_change_notification 程序包中指定 qos_rowids 选项,以便在通知消息中启用 rowid 级别的粒度。“清单 3”是一个 pl/sql 块,它为 orders_nf_callback 通知处理程序创建查询注册。

  清单 3. 为通知处理程序创建查询注册

declare
regds sys.chnf$_reg_info;
regid number;
ord_id number;
qosflags number;
begin
qosflags := dbms_change_notification.qos_reliable + 
dbms_change_notification.qos_rowids;
regds := sys.chnf$_reg_info ('orders_nf_callback', qosflags, 0,0,0);
regid := dbms_change_notification.new_reg_start (regds);
select order_id into ord_id from orders where rownum<2;
dbms_change_notification.reg_end;
end;
/

  本示例针对 orders 表创建了一个注册,并将 orders_nf_callback 用作通知处理程序。现在,如果您使用 dml 或 ddl 语句修改 orders 表并提交事务,则将自动调用 orders_nf_callback 函数。例如,您可能针对 orders 表执行下列 update 语句并提交该事务:

update orders set order_mode = 'direct' where order_id=2421;
update orders set order_mode = 'direct' where order_id=2422;
commit;

  要确保数据库发布了通知来响应以上事务,您可以检查 nfresults 表:

select to_char(operdate, 'dd-mon-yy hh:mi:ss') operdate, 
tblname, rslt_msg from nfresults;
  结果应如下所示:

operdate              tblname     rslt_msg
--------------------- ----------- ---------
02-mar-06 04:31:28    oe.orders   not found
02-mar-06 04:31:29    oe.orders   not found

  从以上结果中可以清楚地看到,orders_nf_callback 已经正常工作,但未找到客户端脚本。在该示例中出现这种情况并不意外,这是因为您并未创建 url 中指定的 dropresults.php 脚本。

  将表添加到现有注册

  前一部分介绍了如何使用更改通知服务使数据库在注册对象(在以上示例中为 orders 表)发生更改时发出通知。但从性能角度而言,客户端应用程序可能更希望缓存 order_items 表而非 orders 表本身的查询结果集,这是因为它在每次访问订单时,不得不从 orders 表中只检索一行,但同时必须从 order_items 表中检索多个行。在实际情况中,订单可能包含数十个甚至数百个订单项。

  由于您已经对 orders 表注册了查询,因此不必再创建一个注册来注册对 order_items 表的查询了。相反,您可以使用现有注册。为此,您首先需要检索现有注册的 id。可以执行以下查询来完成此工作:

select regid, table_name from user_change_notification_regs;  结果可能如下所示:

regid table_name
----- --------------
241 oe.orders

  获取注册 id 后,可以使用 dbms_change_notification.enable_reg 函数将一个新对象添加到该注册,如下所示:

declare
ord_id number;
begin
dbms_change_notification.enable_reg(241);
select order_id into ord_id from order_items where rownum < 2;
dbms_change_notification.reg_end;
end;
/

  完成了!从现在开始,数据库将生成一个通知来响应对 orders 和 order_items 所做的任何更改,并调用 orders_nf_callback 过程来处理通知。因此,下一步就是编辑 orders_nf_callback,以便它可以处理因对 order_items 表执行 dml 操作而生成的通知。但在重新创建 orders_nf_callback 过程之前,您需要创建以下将在更新过程中引用的表类型:

create type rdesc_tab as table of sys.chnf$_rdesc;  然后,返回清单,在以下代码行之后:

if (tblname = 'oe.orders') then
for j in 1..numrows loop
row_id := ntfnds.table_desc_array(i).row_desc_array(j).row_id;
select order_id into ord_id from orders where rowid = row_id;
sendnotification(url, tblname, ord_id); 
end loop;
end if;

  插入以下代码:

if (tblname = 'oe.order_items') then
for rec in (select distinct(o.order_id) o_id from 
table(cast(ntfnds.table_desc_array(i).row_desc_array as rdesc_tab)) t,
orders o, order_items d where t.row_id = d.rowid and d.order_id=o.order_id)
loop
sendnotification(url, tblname, rec.o_id); 
end loop;
end if;

  重新创建 orders_nf_callback 后,您需要测试它能否正常工作。为此,您可以针对 order_items 表执行下列 update 语句并提交该事务:

update order_items set quantity = 160 where order_id=2421 and line_item_id=1;
update order_items set quantity = 160 where order_id=2421 and line_item_id=2;
commit;

  然后,检查 nfresults 表,如下所示:

select to_char(operdate, 'dd-mon-yy hh:mi:ss') operdate, 
rslt_msg from nfresults where tblname = 'oe.order_items';  输出可能如下所示:

operdate            rslt_msg
------------------- --------------
03-mar-06 12:32:27  not found

  您可能很奇怪为什么只向 nfresults 表中插入了一行 – 毕竟您更新了 order_items 表中的两行。实际上,这两个更新了的行具有相同的 order_id – 即它们属于同一订单。此处,我们假设客户端应用程序将使用一个语句选择订单的所有订单项,因此它并不需要确切知道已经更改了某个订单的哪些订单项。相反,客户端需要知道其中至少修改、删除或插入了一个订单项的订单 id。
 构建客户端

  现在,您已经针对 orders 和 order_items 表创建了注册,下面我们将了解一下访问这些表中存储的订单及其订单项的客户端应用程序如何使用更改通知。为此,您可以构建一个 php 应用程序,它将缓存针对以上表的查询结果,并采取相应的操作来响应有关对这些表所做更改的通知(从数据库服务器中收到这些通知)。一个简单的方法是使用 pear::cache_lite 程序包,它为您提供了一个可靠的机制来使缓存数据保持最新状态。尤其是,您可以使用 cache_lite_function 类(pear::cache_lite 程序包的一部分),通过该类您可以缓存函数调用。

  例如,您可以创建一个函数来执行下列任务:建立数据库连接、针对该数据库执行 select 语句、获取检索结果并最终以数组形式返回结果。然后,您可以通过 cache_lite_function 实例的 call 方法缓存由该函数返回的结果数组,以便可以从本地缓存而不是从后端数据库读取这些数组,这样可以显著提高应用程序的性能。然后,在收到缓存数据更改的通知时,您将使用 cache_lite_function 实例的 drop 方法删除缓存中的过期数据。

  回过头来看看本文的示例,您可能要创建两个函数,用于应用程序与数据库交互:第一个函数将查询 orders 表并返回具有指定 id 的订单,而另一个函数将查询 order_items 表并返回该订单的订单项。“清单 4”显示了包含 getorderfields 函数(该函数接受订单 id 并返回一个包含所检索到订单的某些字段的关联数组)的 getorderfields.php 脚本。

  清单 4. 获取指定订单的字段

<?php //file:getorderfields.php require_once 'connect.php'; function getorderfields($order_no) { if (!$rsconnection = getconnection()){ return false;    } $strsql = "select to_char(order_date) order_date, customer_id,  order_total from orders where order_id =:order_no"; $rsstatement = oci_parse($rsconnection,$strsql); oci_bind_by_name($rsstatement, ":order_no", $order_no, 12); if (!oci_execute($rsstatement)) { $err = oci_error(); print $err['message']; trigger_error('query failed:' . $err['message']); return false;    } $results = oci_fetch_assoc($rsstatement); return $results;  } ?> 


  “清单 5”是 getorderitems.php 脚本。该脚本包含 getorderitems 函数,该函数接受订单 id 并返回一个二维数组,该数组包含表示订单的订单项的行。

  清单 5. 获取指定订单的订单项

<?php //file:getorderitems.php require_once 'connect.php'; function getorderitems($order_no) { if (!$rsconnection = getconnection()){ return false;    } $strsql = "select * from order_items where  order_id =:order_no order by line_item_id"; $rsstatement = oci_parse($rsconnection,$strsql); oci_bind_by_name($rsstatement, ":order_no", $order_no, 12); if (!oci_execute($rsstatement)) { $err = oci_error(); trigger_error('query failed:' . $err['message']); return false;    } $nrows = oci_fetch_all($rsstatement, $results); return array ($nrows, $results);  } ?> 


  注意,以上两个函数都需要 connect.php 脚本,该脚本应包含返回数据库连接的 getconnection 函数。清单 6 就是 connect.php 脚本:

  清单 6. 获取数据库连接

<?php //file:connect.php function getconnection() { $dbhost = "dbserverhost"; $dbhostport="1521"; $dbservicename = "orclr2"; $usr = "oe"; $pswd = "oe"; $dbconnstr = "(description=(address=(protocol=tcp)(host=".$dbhost.") (port=".$dbhostport."))(connect_data=(service_name=".$dbservicename.")))"; if(!$dbconn = oci_connect($usr,$pswd,$dbconnstr)) { $err = oci_error(); trigger_error('failed to connect ' .$err['message']); return false;       } return $dbconn;   } ?> 


  现在,您已经创建了与数据库通信所需的所有函数,下面我们将了解一下 cache_lite_function 类的工作方式。清单 7 是 testcache.php 脚本,该脚本使用 cache_lite_function 类缓存以上函数的结果。

  清单 7. 使用 pear::cache_lite 缓存

<?php //file:testcache.php require_once 'getorderitems.php'; require_once 'getorderfields.php'; require_once 'cache/lite/function.php'; $options = array(  'cachedir' => '/tmp/',  'lifetime' => 86400   );  if (!isset($_get['order_no'])) { die('the order_no parameter is required');   }  $order_no=$_get['order_no']; $cache = new cache_lite_function($options); if ($orderfields = $cache->call('getorderfields', $order_no)){ print "<h3>order #$order_no</h3>/n"; print "<table>"; print "<tr><td>date:</td><td>".$orderfields['order_date']."</td></tr>"; print "<tr><td>cust_id:</td><td>".$orderfields['customer_id']."</td></tr>"; print "<tr><td>total:</td><td>".$orderfields['order_total']."</td></tr>"; print "</table>"; } else { print "some problem occurred while getting order fields!/n"; $cache->drop('getorderfields', $order_no);  } if (list($nrows, $orderitems) = $cache->call('getorderitems', $order_no)){ //print "<h3>line items in order #$order_no</h3>"; print "<table border=1>"; print "<tr>/n"; while (list($key, $value) = each($orderitems)) { print "<th>$key</th>/n";   } print "</tr>/n"; for ($i = 0; $i < $nrows; $i++) { print "<tr>"; print "<td>".$orderitems['order_id'][$i]."</td>"; print "<td>".$orderitems['line_item_id'][$i]."</td>"; print "<td>".$orderitems['product_id'][$i]."</td>"; print "<td>".$orderitems['unit_price'][$i]."</td>"; print "<td>".$orderitems['quantity'][$i]."</td>"; print "</tr>";   } print "</table>"; } else { print "some problem occurred while getting order line items"; $cache->drop('getorderitems', $order_no);  } ?> 


  “清单 7”中的 testcache.php 脚本应与 order_no url 参数(代表 oe.order 表中存储的订单 id)一起被调用。例如,要检索与 id 为 2408 的订单相关的信息,需要在浏览器中输入如下所示的 url:

http://webserverhost/phpcache/testcache.php?order_no=2408

  结果,浏览器将生成以下输出:

  order #2408

date: 29-jun-99 06.59.31.333617 am
cust_id: 166
total: 309

order_id line_item_id product_id unit_price quantity
2408 1 2751 61 3
2408 2 2761 26 1
2408 3 2783 10 10

 现在,如果您单击浏览器中的 reload 按钮,testcache.php 脚本将不会再次调用 getorderfields 和 getorderitems 函数。相反,它将从本地缓存中读取它们的结果。因此,从现在起的 24 小时(因为 lifetime 设置为 86400 秒)以内,本地缓存即可满足使用 order_no=2108 的每个 getorderfields 或 getorderitems 调用的需要。但请注意,cache_lite_function 类未提供 api 来测试具有给定参数的给定函数是否存在可用缓存。因此,要确定每次使用相同参数调用函数时应用程序是实际上读取缓存还是仍执行该函数可能有点棘手。例如,在以上示例中,要确保缓存机制正常工作,您可以临时更改 connect.php 脚本中指定的连接信息,以便它无法建立数据库连接;比如指定一个错误的数据库服务器主机名称,然后再次使用 order_no=2108 运行 testcache.php 脚本。如果缓存正常工作,浏览器的输出应与先前的一样。

  此外,您还可以检查缓存目录,该目录作为 cachedir 选项的值(在该示例中为 /tmp)传递给 cache_lite_function 类的构造函数。在该目录中,您将找到两个刚创建的缓存文件,这些文件的名称类似于:cache_7b181b55b55aee36ad5e7bd9d5a091ec_3ad04d3024f4cd54296f75c92a359154。注意,如果您是一位 windows 用户,则可能要使用 %systemdrive%/temp 目录保存缓存文件。如果是这样,则必须将 cachedir 选项设置为 /temp/。

  验证缓存机制正常工作后,可以接着创建一个 php 来处理从数据库服务器收到的更改通知。“清单 8”是 dropresult.php 脚本。数据库服务器将调用该脚本来响应 orders 和 order_items 表的更改。

  清单 8. 处理从数据库服务器收到的更改通知

<?php //file:dropresults.php require_once 'cache/lite/function.php'; $options = array( 'cachedir' => '/tmp/'  ); $cache = new cache_lite_function($options); if (isset($_get['order_no'])&& isset($_get['table'])) { if($_get['table']=='order_items'){ $cache->drop('getorderitems', $_get['order_no']);    } if ($_get['table']=='orders'){ $cache->drop('getorderfields', $_get['order_no']);    }   }  ?> 


  创建 dropresult.php 脚本后,请确保在通知处理程序中指定的 url(如清单 2 所示)正确。然后,在 sql*plus 或类似工具中以 oe/oe 连接,并执行 update 语句,这些语句将影响本部分先前通过 testcache.php 脚本访问的同一订单(此处是 id 为 2408 的订单):

update orders set order_mode = 'direct' where order_id=2408; update order_items set quantity = 3 where order_id=2408 and line_item_id=1; update order_items set quantity = 1 where order_id=2408 and line_item_id=2; commit; 


  为响应以上更新,本文前面介绍的通知处理程序将逐个使用下列 url 运行 dropresults.php 脚本两次:

http://webserverhost/phpcache/dropresults.php?order_no=2408&table=orders http://webserverhost/phpcache/dropresults.php?order_no=2408&table=order_items 


  从“清单 8”中您可以清楚地看到,dropresult.php 脚本在从数据库服务器收到更改通知后并未刷新缓存。它只是删除了包含过期数据的缓存文件。因此,如果现在检查缓存目录,则将看到在使用 order_no=2408 运行 testcache.php 脚本时创建的缓存文件已经消失。这实际上意味着,testcache.php 在下次请求与 id 为 2408 的订单相关的数据时将从后端数据库而非本地缓存中获取该数据。

  您会发现,在应用程序请求的结果集很有可能在应用程序使用它之前更改的情况下该方法将很有用。就本文的示例而言,这意味着与特定订单相关的数据可能在 testcache.php 访问该订单之前多次更改。这样,应用程序会因在从数据库服务器收到更改通知后立即刷新它的缓存而做了大量不必要的工作。

  但如果您希望 dropresult.php 脚本在收到更改通知后立即刷新缓存,则可以在调用 drop 方法后调用 cache_lite_function 实例的 call 方法,并为这两个调用指定相同的参数。在该情形下,还应确保包含 getorderfields.php 和 getorderitems.php 脚本,以便 dropresults.php 可以调用 getorderfields 和 getorderitems 函数来刷新缓存。“清单 9”是修改后的 dropresult.php 脚本。

  清单 9. 在收到更改通知后立即刷新缓存

<?php //file:dropresults.php require_once 'cache/lite/function.php'; require_once 'getorderitems.php'; require_once 'getorderfields.php'; $options = array( 'cachedir' => '/tmp/', 'lifetime' => 86400   ); $cache = new cache_lite_function($options); if (isset($_get['order_no'])&& isset($_get['table'])) { if($_get['table']=='order_items'){ $cache->drop('getorderitems', $_get['order_no']); $cache->call('getorderitems', $_get['order_no']);    } if ($_get['table']=='orders'){ $cache->drop('getorderfields', $_get['order_no']); $cache->call('getorderfields', $_get['order_no']);    }   }  ?> 


  如果存储在 ordersorder_items 表中的数据很少更改并且应用程序频繁访问它,则以上方法可能很有用。

  总结

  如果 php 应用程序与 oracle 数据库 10g 第 2 版交互,则可以利用“数据库更改通知特性”,通过该特性应用程序可以接收通知来响应对与发出的请求关联的对象进行的 dml 更改。使用该特性,您不必在特定时间段更新应用程序中的缓存。相反,仅当注册查询的结果集已经更改时才执行该操作

发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表