通过缓存数据库结果提高PHP性能(二)
网络整理 - 08-10
将表添加到现有注册 前一部分介绍了如何使用更改通知服务使数据库在注册对象(在以上示例中为 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;
然后,返回清单 2,在以下代码行之后:
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:
?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 脚本两次:
?order_no=2408&table=ORDERS
?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']);
}
}
?>
如果存储在 ORDERS 和 ORDER_ITEMS 表中的数据很少更改并且应用程序频繁访问它,则以上方法可能很有用。
总结
如果 PHP 应用程序与 Oracle 数据库 10g 第 2 版交互,则可以利用“数据库更改通知特性”,通过该特性应用程序可以接收通知来响应对与发出的请求关联的对象进行的 DML 更改。使用该特性,您不必在特定时间段更新应用程序中的缓存。相反,仅当注册查询的结果集已经更改时才执行该操作。