php

位置:IT落伍者 >> php >> 浏览文章

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


发布日期:2023年12月11日
 
通过缓存数据库结果提高PHP性能

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

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

数据库更改通知入门

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

为了了解数据库更改通知特性的作用方式请考虑以下示例假设您的 PHP 应用程序访问 OEORDERS 表中存储的订单以及 OEORDER_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;

确保将 initora 参数 job_queue_processes 设置为非零值以便接收 PL/SQL 通知或者您也可以使用下面的 ALTER SYSTEM 命令

ALTER SYSTEM SET job_queue_processes=;

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

CONNECT oe/oe;CREATE TABLE nfresults (operdate DATE tblname VARCHAR() rslt_msg VARCHAR());

在实际情况中您可能需要创建更多表来记录通知事件以及所更改行的行 ID 等信息但就本文而言nfresults 表完全可以满足需要

使用 UTL_HTTP 向客户端发送通知

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

清单 使用 UTL_HTTP 向客户端发送通知

CREATE OR REPLACE PROCEDURE sendNotification(url IN VARCHAR tblname IN VARCHAR order_id IN VARCHAR) ISreq   UTL_HTTPREQ;resp  UTL_HTTPRESP;err_msg VARCHAR();tbl VARCHAR();BEGINtbl:=SUBSTR(tblname INSTR(tblname )+ );BEGINreq := UTL_HTTPBEGIN_REQUEST(url||order_id||&||table=||tbl);resp := UTL_HTTPGET_RESPONSE(req);INSERT INTO nfresults VALUES(SYSDATE tblname respreason_phrase);UTL_HTTPEND_RESPONSE(resp);EXCEPTION WHEN OTHERS THENerr_msg := SUBSTR(SQLERRM );INSERT INTO nfresults VALUES(SYSDATE tblname err_msg);END;COMMIT;END;/

清单 所示sendNotification 以 UTL_HTTPBEGIN_REQUEST 函数发出的 HTTP 请求的形式向客户端发送通知消息此 URL 包含 ORDERS 表中已更改行的 order_id然后它使用 UTL_HTTPGET_RESPONSE 获取客户端发出的响应信息实际上sendNotification 并不需要处理客户端返回的整个响应而是只获取一个在 RESP 记录的 reason_phrase 字段中存储的简短消息(描述状态代码)

创建通知处理程序

现在您可以创建一个通知处理程序它将借助于上面介绍的 sendNotification 过程向客户端发送更改通知来看一看清单 中的 PL/SQL 过程 orders_nf_callback

清单 处理对 OEORDERS 表所做更改的通知的通知处理程序

CREATE OR REPLACE PROCEDURE orders_nf_callback (ntfnds IN SYSCHNF$_DESC) IStblname VARCHAR();numtables NUMBER;event_type NUMBER;row_id VARCHAR();numrows NUMBER;ord_id VARCHAR();url VARCHAR() := ;BEGINevent_type := ntfndsevent_type;numtables := ntfndsnumtables;IF (event_type = DBMS_CHANGE_NOTIFICATIONEVENT_OBJCHANGE) THENFOR i IN numtables LOOPtblname := ntfndstable_desc_array(i)table_name;IF (bitand(ntfndstable_desc_array(i)opflags DBMS_CHANGE_NOTIFICATIONALL_ROWS) = ) THENnumrows := ntfndstable_desc_array(i)numrows;ELSEnumrows :=;END IF;IF (tblname = OEORDERS) THENFOR j IN numrows LOOProw_id := ntfndstable_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;/

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

为更改通知创建注册

创建通知处理程序后必须为其创建一个查询注册对于本示例而言您必须在注册过程中对 OEORDER 表执行查询并将 orders_nf_callback 指定为通知处理程序您还需要在 DBMS_CHANGE_NOTIFICATION 程序包中指定 QOS_ROWIDS 选项以便在通知消息中启用 ROWID 级别的粒度清单 是一个 PL/SQL 块它为 orders_nf_callback 通知处理程序创建查询注册

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

DECLAREREGDS SYSCHNF$_REG_INFO;regid NUMBER;ord_id NUMBER;qosflags NUMBER;BEGINqosflags := DBMS_CHANGE_NOTIFICATIONQOS_RELIABLE + DBMS_CHANGE_NOTIFICATIONQOS_ROWIDS;REGDS := SYSCHNF$_REG_INFO (orders_nf_callback qosflags );regid := DBMS_CHANGE_NOTIFICATIONNEW_REG_START (REGDS);SELECT order_id INTO ord_id FROM orders WHERE ROWNUM<;DBMS_CHANGE_NOTIFICATIONREG_END;END;/

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

UPDATE ORDERS SET order_mode = direct WHERE order_id=;UPDATE ORDERS SET order_mode = direct WHERE order_id=;COMMIT;

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

SELECT TO_CHAR(operdate ddmonyy hh:mi:ss) operdate tblname rslt_msg FROM nfresults;

结果应如下所示

OPERDATE              TBLNAME     RSLT_MSG mar ::    OEORDERS   Not Foundmar ::    OEORDERS   Not Found

从以上结果中可以清楚地看到orders_nf_callback 已经正常工作但未找到客户端脚本在该示例中出现这种情况并不意外这是因为您并未创建 URL 中指定的 dropResultsphp 脚本有关 dropResultsphp 脚本的说明请参阅本文后面的构建客户端 部分

将表添加到现有注册

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

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

SELECT regid table_name FROM user_change_notification_regs;

结果可能如下所示

REGID TABLE_NAME OEORDERS

获取注册 ID 后可以使用 DBMS_CHANGE_NOTIFICATIONENABLE_REG 函数将一个新对象添加到该注册如下所示

DECLAREord_id NUMBER;BEGINDBMS_CHANGE_NOTIFICATIONENABLE_REG();SELECT order_id INTO ord_id FROM order_items WHERE ROWNUM < ;DBMS_CHANGE_NOTIFICATIONREG_END;END;/

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

CREATE TYPE rdesc_tab AS TABLE OF SYSCHNF$_RDESC;

然后返回清单 在以下代码行之后

IF (tblname = OEORDERS) THENFOR j IN numrows LOOProw_id := ntfndstable_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 = OEORDER_ITEMS) THENFOR rec IN (SELECT DISTINCT(oorder_id) o_id FROM TABLE(CAST(ntfndstable_desc_array(i)row_desc_array AS rdesc_tab)) torders o order_items d WHERE trow_id = drowid AND dorder_id=oorder_id)LOOPsendNotification(url tblname reco_id); END LOOP;END IF;

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

UPDATE ORDER_ITEMS SET quantity = WHERE order_id= AND line_item_id=;UPDATE ORDER_ITEMS SET quantity = WHERE order_id= AND line_item_id=;COMMIT;

然后检查 nfresults 表如下所示

SELECT TO_CHAR(operdate ddmonyy hh:mi:ss) operdate rslt_msg FROM nfresults WHERE tblname = OEORDER_ITEMS;

输出可能如下所示

OPERDATE            RSLT_MSG mar ::  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 表并返回该订单的订单项清单 显示了包含 getOrderFields 函数(该函数接受订单 ID 并返回一个包含所检索到订单的某些字段的关联数组)的 getOrderFieldsphp 脚本

清单 获取指定订单的字段

<?php//File:getOrderFieldsphprequire_once connectphp;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 );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; }?>

清单 是 getOrderItemsphp 脚本该脚本包含 getOrderItems 函数该函数接受订单 ID 并返回一个二维数组该数组包含表示订单的订单项的行

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

<?php//File:getOrderItemsphprequire_once connectphp;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 );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); }?>

注意以上两个函数都需要 connectphp 脚本该脚本应包含返回数据库连接的 GetConnection 函数清单 就是 connectphp 脚本

清单 获取数据库连接

<?php//File:connectphpfunction GetConnection() {$dbHost = dbserverhost;$dbHostPort=;$dbServiceName = orclR;$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 类的工作方式清单 是 testCachephp 脚本该脚本使用 Cache_Lite_Function 类缓存以上函数的结果

清单 使用 PEAR::Cache_Lite 缓存

<?php//File:testCachephprequire_once getOrderItemsphp;require_once getOrderFieldsphp;require_once Cache/Lite/Functionphp;$options = array( cacheDir => /tmp/ lifeTime =>   ); 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 <h>ORDER #$order_no</h>\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 <h>LINE ITEMS IN ORDER #$order_no</h>;print <table border=>;print <tr>\n;while (list($key $value) = each($orderitems)) {print <th>$key</th>\n;  }print </tr>\n;for ($i = ; $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); }?>

清单 中的 testCachephp 脚本应与 order_no URL 参数(代表 OEORDER 表中存储的订单 ID)一起被调用例如要检索与 ID 为 的订单相关的信息需要在浏览器中输入如下所示的 URL

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

ORDER #

DATE:

JUN AM

CUST_ID:

TOTAL:

ORDER_ID

LINE_ITEM_ID

PRODUCT_ID

UNIT_PRICE

QUANTITY

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

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

验证缓存机制正常工作后可以接着创建一个 PHP 来处理从数据库服务器收到的更改通知清单 是 dropResultphp 脚本数据库服务器将调用该脚本来响应 ORDERS 和 ORDER_ITEMS 表的更改

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

<?php//File:dropResultsphprequire_once Cache/Lite/Functionphp;$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]);   }  } ?>

创建 dropResultphp 脚本后请确保在通知处理程序中指定的 URL(如清单 所示)正确然后在 SQL*Plus 或类似工具中以 OE/OE 连接并执行 UPDATE 语句这些语句将影响本部分先前通过 testCachephp 脚本访问的同一订单(此处是 ID 为 的订单)

UPDATE ORDERS SET order_mode = direct WHERE order_id=;UPDATE ORDER_ITEMS SET quantity = WHERE order_id= AND line_item_id=;UPDATE ORDER_ITEMS SET quantity = WHERE order_id= AND line_item_id=;COMMIT;

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

;table=ORDERS;table=ORDER_ITEMS

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

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

但如果您希望 dropResultphp 脚本在收到更改通知后立即刷新缓存则可以在调用 drop 方法后调用 Cache_Lite_Function 实例的 call 方法并为这两个调用指定相同的参数在该情形下还应确保包含 getOrderFieldsphp 和 getOrderItemsphp 脚本以便 dropResultsphp 可以调用 getOrderFields 和 getOrderItems 函数来刷新缓存清单 是修改后的 dropResultphp 脚本

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

<?php//File:dropResultsphprequire_once Cache/Lite/Functionphp;require_once getOrderItemsphp;require_once getOrderFieldsphp;$options = array(cacheDir => /tmp/lifeTime =>   );$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 数据库 g 第 版交互则可以利用数据库更改通知特性通过该特性应用程序可以接收通知来响应对与发出的请求关联的对象进行的 DML 更改使用该特性您不必在特定时间段更新应用程序中的缓存相反仅当注册查询的结果集已经更改时才执行该操作

               

上一篇:Oracle PHP 故障诊断常见问题以及解答

下一篇:用Java+MySQL+PHP轻松构建跨平台的搜索引擎