Oracle + PHP

DeployPHP 系列第 1 部分:优化 PHP 和 Oracle
作者:John Lim

DeployPHP 系列的这个第一部分中,您将了解在 Oracle 上开发和部署强大的可伸缩 PHP 应用程序的相关事项和准备工作。

 

本文相关下载:
 Oracle 数据库 10g
 Oracle 即时客户端
 Oracle JDeveloper PHP 扩展

开放源语言 PHP:超文本预处理程序 (PHP) 为世界上一些知名的网站(如 Yahoo!、Lufthansa 和 Disney Online)提供支持。而 PHP 实现这一切使用的资源却很少,这更凸显这一成就的不同凡响。

假设 Web 服务器是一个处理 HTTP 请求的巨大虚拟工厂。而 PHP 解释程序则是头戴安全帽处理这些请求的工人。每个工人都封闭在他/她自己的工作空间内,无法与其他人交流或协调。这些工人非常健忘,对以前的事务忘记得一干二净。在对传入的 HTTP 请求开始处理之前,他们必须再次调用数据仓库中的所有会话信息。处理完请求后,工人们必须争先恐后地将所有会话信息返回到数据仓库中。

有点反常的是,这种工人们被孤立在单独的空间中并且记性很差的无状态环境却具有非常高的可伸缩性。彼此完全独立并且不维护状态的 PHP 进程有很高的可并行性。当然,您需要执行数据同步和会话存储,但您可以将这些操作部分交由给专门外部模块(通常是关系数据库,如 Oracle)处理。正如 Rasmus Lerdorf 在他的 OTN 文章“您了解 PHP 吗?”中指出的,“PHP 促使您把可伸缩性问题推向需要它的层上去。如果您需要共享的数据存储,那么就使用一个支持复制和能扩展到您所需要的级别的数据库。”

作为一名 PHP 开发人员,您深知数据库可能是您需要打道的最重要组件。在 DeployPHP 系列的第一部分中,我将介绍在 Oracle 上开发和部署强大的可伸缩 PHP 应用程序的重要事项(假定您对 PHP 和 SQL 有一定的了解,但对 Oracle 了解不多):

  1. 选择正确的扩展 API。
  2. 知道何时获取和提示。
  3. 巧妙地使用绑定。
  4. 使用类库。
  5. 了解创建优化程序提示和索引的技巧。
  6. 维护数据并管理业务逻辑。
  7. 时刻不忘“任务控制”。
本系列文章的下一篇会介绍有关如何在部署后优化 PHP 代码的详细信息:

1. 选择正确的扩展 API

PHP 提供了多个用于连接到 Oracle 的应用程序编程接口 (API),即 Oracle、OCI8,在 PHP 5.1 中为 PDO、扩展。您很快便会看到,PHP 提供了出色的功能,但命名规则却非常不一致。以下信息可指导你部署哪一种扩展:

  • 很奇怪,“Oracle”扩展是为 Oracle7 而设计的,因此应尽量避免使用它。它十分陈旧,并且缺少许多功能,如果大型对象 (LOB) 支持。
  • PHP 数据对象 (PDO) 是 PHP 5.1 版中新推出的可移植数据库 API。但它仍然不够成熟。
  • 在 Windows 上,可以选择通过开放数据库连接 (ODBC) 扩展查询 Oracle。ODBC 有一些优点,它提供了内置的连接池。但有限的 ODBC API 意味着,在存储过程中处理 LOB 和 IN OUT 参数可能会出现问题。
  • OCI8 扩展提供了最出色的功能,因为它可以非常贴切地映射到 Oracle 调用接口 (OCI)。OCI8 中的“8”会引起误解;此 API 可以用于 Oracle8/8i、Oracle9i 和 Oracle 10g。为获得最佳性能,建议您使用此 API。
有关在 Linux、Windows 和 Mac OS X 上安装 Oracle 和 PHP 的详细说明,请访问 PHP 开发人员中心。如果您首选预打包的 Oracle 和 PHP 集成解决方案,则有必要了解一下即将发布的 Zend Core for Oracle。

2. 知道何时获取和提示

作为一名 PHP 开发人员,您最常见的任务就是从 Oracle 中检索和显示数据。在开始编码或部署 PHP 应用程序之前,需要了解几个重要的技巧。

首先是一些理论知识。将 SQL 发送到 Oracle 时,Oracle 的分析程序将检查此 SQL,看看是否可以使用现有的编译过的查询。如果存在匹配项,它将使用已编译查询( 软分析)。如果不存在匹配项,它将编译该查询( 硬分析),运行优化程序以选择最佳的查询执行计划。

分页。 通常情况下,您要将检索到的记录分页,然后只检索当前页面。为此,可以使用嵌套的 select 语句和特殊的 ROWNUM 变量。以下代码演示了如何检索一定范围内的记录以实现分页。假设 $currentPage 起始于第 0 页:

$firstRec = $currentPage * $recordsPerPage + 1;
$lastRec = ($currentPage+1) * $recordsPerPage;

$myquery = "SELECT * FROM customer WHERE type='HIGHVAL' ORDER BY name";

$sql = " SELECT * FROM 
( SELECT a.*, RowNum as rNum 
FROM ( $myquery ) a
WHERE RowNum <= $lastRec )
WHERE $firstRec <= rNum";
来自 Oracle 的每个记录均分配了一个行号,可以使用此行号中断记录检索。例如,可以将 $firstRec <= rNum 用作下限,将 RowNum<= $lastRec 用作上限。

计算记录数。 另一个实际上比乍看起来更困难的常见任务是维护所关注区域的记录数。同所有高性能数据库一样,Oracle 并不在返回的记录集中包含所有记录的计数。如果您要了解查询将返回的记录数,又不想运行开销可能比较大的查询,这是一个棘手的问题。

一个计数比较有用的示例是,您希望在分页显示记录集时显示一组滚动链接,如下所示:

 

 

最简单的解决方案是执行查询并在返回记录时进行计数。一个更好的解决方案是执行查询重写,并将原来的 SQL 由
SELECT id, msg_subject, msg_contents FROM largetable
更改为
SELECT COUNT(*) FROM (SELECT id, msg_subject, msg_contents FROM largetable)
如果数据非常大,或查询非常标准,则可以考虑使用一个特殊的计数器表和可用来更新该计数器表的 INSERT/DELETE 触发器。当您需要了解数目时,则可查询该计数器表。

提示。 尽管 Oracle 的查询优化程序是智能化的,但它不会始终选择最佳的计划。为尽量减少这些情况的发生,可以向优化程序提供提示。例如:

$sql = "SELECT * FROM (
SELECT  
                              
/*+ FIRST_ROWS INDEX(customer ctype) */
* FROM customer WHERE type='High Value' ORDER BY name ) WHERE ROWNUM <= $lastRecordToDisplay";
优化程序提示置于 SELECT 关键字之后,并放在由 /*+ 触发的注释中。在以上示例中,您将看到两个提示:

 

FIRST_ROWS 指示优化程序,我们不希望检索所有行,即只检索所需记录总数的一小部分,以加快速度。
INDEX(tablename indexname) 请求优化程序使用此索引。

以上是我最经常使用的提示,您可以在 Oracle 数据库 SQL 参考第 2 章的“注释”中找到许多其他提示。


预取。 最后,要介绍一个 OCI8 函数,该函数用于设置经由网络预取到 Oracle 客户端缓冲区的记录数,从而显著减少网络流量。默认值 1 太小,这是因为它表示检索到的每个记录都将产生较大的网络流量。应将它设置为通常的页面大小,例如,设置为 20 到 50 个记录。仅仅此优化一项便可以使查询速度提高 100% 到 200%。

在 PHP 中:

$conn = OCILogon($user,$pwd);
$stmt = OCIParse($conn,$sql);
OCISetPrefetch($stmt, 20);
OCIExecute($stmt);
While (OCIFetchInto($stmt,$arr)) {
        /* Process $arr */
}
3. 巧妙地使用绑定

请看下面的 SQL 语句。

SELECT * FROM customer WHERE custid=1
SELECT * FROM customer WHERE custid=249
SELECT * FROM customer WHERE custid=6380
默认情况下,Oracle 将对每个语句使用不同的执行计划。由于以上所有语句都是同一查询的不同形式,因此该方法将浪费时间和资源。可以对以上的 SQL 进行重新格式化,使用绑定变量 :var,并将值 1、249 和 6380 绑定到该变量。这样,所有 SQL 将没有差别,可以使用相同的执行计划:

 

将值绑定到变量 SQL
将 1 绑定到 :var
将 249 绑定到 :var SELECT * FROM customer WHERE custid=:var
将 6380 绑定到 :var

将该语句转换为 PHP 代码,即将 $value 绑定到 :var,这样您将获得以下代码:

OCIParse($conn, 'SELECT * FROM customer WHERE custid=:var');
OciBindByName($stmt, ":var", $value,32); # 32 is an arbitrary large size

$value = 1; OCIExecute($stmt);
DisplayResults($stmt);

$value = 249; OCIExecute($stmt);
DisplayResults($stmt);

$value = 6380; OCIExecute($stmt);
DisplayResults($stmt);
绑定的另一个优点是降低了 SQL 注入攻击的风险,这是因为攻击者将无法以我们不知道的方式操控 SQL 语句。

游标共享。 即使有大量不使用绑定的原有 SQL,您仍然可以通过使用 CURSOR_SHARING 参数(在 Oracle8 和更高版本中提供)来减少编译开销。执行以下 SQL 语句将使用 Oracle 重写所有所有查询,以使用绑定变量:

ALTER SESSION SET CURSOR_SHARING='FORCE'
因此,SQL:
SELECT * FROM customer WHERE custid=6380
在传递到分析程序之前将自动转换为一个可绑定版本:
SELECT * FROM customer WHERE custid=:SYS_B_0
CURSOR_SHARING 允许共用和重用执行计划,但它存在一些限制。亲自使用绑定变量的效率要高很多。此外,您仍然容易受到 SQL 注入攻击。

在 Oracle9i 或更高版本中,您还可以设置 CURSOR_SHARING='SIMILAR'。该参数使用一个更智能的算法,即将表的统计信息列入考虑之中,以选择是否将文字转换为绑定变量。

ALTER SESSION 只影响当前会话,因此每次连接到数据库时都需要调用它。要把它固定下来,可以要求您的数据库管理员将其置于数据库参数文件(init.ora 或 spfile)中。

在下个部分中,我将展示使用和不使用绑定变量时的基准测试结果。这些基准测试结果详细说明了在 INSERT 语句方案中,使用绑定变量将如何使性能提高 100% 以上。

4. 使用类库

在开始对应用程序进行编码之前,您会发现,创建一组 PHP 函数以封装 OCI8 扩展的低级功能将很有用。您可以选择是创建您自己的库还是使用某个流行的开放源库。最流行的两个库是 PEAR DBADOdb。(我是后者的主要开发人员。)

例如,请考虑使用以下代码更新一个由三个字段组成的表:a、b 和 c。

$conn = ocilogon('scott','tiger'); 

$stmt = OCIParse("insert into abc (a,b,c) values (?,?,?)");
OCIBindByName($stmt, ":a", $a, 32);
OCIBindByName($stmt, ":b", $b, 32);
OCIBindByName($stmt, ":c", $c, 32);
for ($i=0; $i<TIMES; $i++) {
        $a = $i;
        $b = "b".rand();
        $c = "c".rand();
        ociexecute($stmt,OCI_DEFAULT);
}
ocicommit($conn);
将此代码与更简单的 PEAR DB 版本进行对比:
include('DB.php');
$DB = &DB::Connect('oci8://scott:tiger@/'); 

$DB->autoCommit(false);
$stmt = $DB->prepare("insert into abc (a,b,c) values (?,?,?)");
for ($i=0; $i<TIMES; $i++) {
        $DB->execute($stmt,array($i,"b".rand(),"c".rand()));
}
$DB->commit();
或与很酷的 ADOdb 版本进行比较:
include('/path/to/adodb/adodb.inc.php');
$DB = NewADOConnection('oci8://scott:tiger@/');

$DB->BeginTrans();
$stmt = $DB->Prepare("insert into abc (a,b,c) values (:0,:1,:2)");
for ($i=0; $i<TIMES; $i++) {
        $DB->_Execute($stmt,array($i,"b".rand(),"c".rand()));
}       
$DB->CommitTrans();
您可以看到,ADOdb 库隐藏了 OCIBindByName 等细节。库还提供了一些调试方法,您可以使用这些方法记录和输出生成的 SQL 的信息,以便于问题诊断。

以下是我对以上代码进行的一些基准测试,其中 TIMES = 1000。我计算了事务从开始到结束的时间。我在 Windows XP 上使用了 PHP 4.3.11 和 Oracle 数据库版本 9.2。时间越短说明越好。

 

使用绑定的 OCI8 0.18 秒
ADOdb 0.27 秒
PEAR DB 0.35 秒
未使用绑定的 OCI8 0.41 秒

可以看到,在不使用绑定变量的情况下,开销很大,对于修改单个记录的 INSERTUPDATE 语句而言更是如此。使用库也需要一定的开销,要评估这一性能影响,必须要考虑到使用高级库时在代码的易用性和可维护性方面所得到的增益。

另一个有趣的问题是,如果在启用了自动提交(即在每个 INSERT 语句之后以无提示的方式执行提交)的情况下执行基准测试,则性能将显著降低。使用绑定的 OCI8 基准测试耗时 2.92 秒,即速度比原来慢了 15 倍!

使用 ADOdb 还有几个其他优点。ADOdb 已经针对 Oracle 进行了重大调整,例如:

  • ADOdb 自动设置 OCISetPreFetch。
  • ADOdb 在可能的地方插入 Oracle 优化程序提示,以加快代码的执行速度。
  • ADOdb 支持在存储过程和 LOB 中使用 IN OUT 参数。
  • ADOdb 允许您缓存常用查询的记录集。通过缓存很少更改的查找值,可以有效地减少数据库负载。
  • ADOdb 提供了会话管理实用程序,您可以将会话信息存储到字符 LOB 中。
  • ADOdb 提供了一个比 SGA Trace 更出色的调整基础架构。可以根据 HTTP 请求、开销以及执行频率捕获和交叉引用所有 SQL 查询。通过该方法,您可以快速查明 SQL 查询中的性能问题。(本文的后面部分将对此进行详细介绍。)
  • 默认 ADOdb 安装是完全可移植的 PHP。为获得更好的性能,可以安装可选的 ADOdb 扩展,为提高速度,这些 ADOdb 扩展中的部分采用 C 语言进行了重写。默认安装中未选中此扩展。

尽管这并不是针对 Oracle 进行的优化,但您还应安装一个 PHP 加速器缓存(如 Zend Accelerator、eAccelerator 或 APC)以加快 PHP 代码库的编译和加载速度。

 

BLOB 管理。 存储大量数据(任何超过 4,000 字节的数据)要求您使用 Oracle 的令人生畏的 LOB API。ADOdb 为您提供了更简单的 UpdateBlobUpdateClob 函数:
$db->Execute("insert into aTable (id, name, ablob) 
values (999, 'Name', null)");

$db->UpdateBlob('aTable', 'ablob', $blobValue, 'id=999');

以上代码将一个新记录插入到表 ATABLE 中,然后调用 UpdateBlob,以将 $blobValue 存储到字段 ablob(其中, id=999)中。

5. 了解创建优化程序提示和索引的技巧

开始测试应用程序并进入初始部署阶段后,您可能会发现意外的瓶颈和问题。最终用户很有可能在最意想不到的位置出现最奇怪的错误。找出 SQL 速度慢的原因的最佳方法之一是记录所有 SQL 语句和执行次数信息,然后查看开销最高的 SQL 的执行计划。

 

假设已经对数据进行了正确地规范化,提高查询性能的技巧主要体现在向查询优化程序提供提示,并可以添加更好的索引以供查询使用。

 

Oracle 使您可以使用 EXPLAIN PLAN 命令查看执行计划的高级版本。要为 $sql 运行 EXPLAIN PLAN,请选择任意 $id 并运行以下语句:
$id = 'MyID';
$explainSQL = "EXPLAIN PLAN SET STATEMENT_ID='$id' FOR $sql";
$stmt = OCIParse($conn, $explainSQL);
OCIExecute($stmt);
此执行计划保存到该给定 statement_id 的特殊 PLAN_TABLE 中。
CREATE TABLE PLAN_TABLE (
STATEMENT_ID                    VARCHAR2(30),
TIMESTAMP                       DATE,
REMARKS                         VARCHAR2(80),
OPERATION                       VARCHAR2(30),
OPTIONS                         VARCHAR2(30),
OBJECT_NODE                     VARCHAR2(128),
OBJECT_OWNER                    VARCHAR2(30),
OBJECT_NAME                     VARCHAR2(30),
OBJECT_INSTANCE                 NUMBER(38),
OBJECT_TYPE                     VARCHAR2(30),
OPTIMIZER                       VARCHAR2(255),
SEARCH_COLUMNS                  NUMBER,
ID                              NUMBER(38),
PARENT_ID                       NUMBER(38),
POSITION                        NUMBER(38),
COST                            NUMBER(38),
CARDINALITY                     NUMBER(38),
BYTES                           NUMBER(38),
OTHER_TAG                       VARCHAR2(255),
PARTITION_START                 VARCHAR2(255),
PARTITION_STOP                  VARCHAR2(255),
PARTITION_ID                    NUMBER(38),
OTHER                           LONG,
DISTRIBUTION                    VARCHAR2(30)
);
正如我在前面提到的,使用 ADOdb 可以记录和捕获所有已执行的 SQL ,并在 Web 浏览器显示 EXPLAIN PLAN。可以通过调用 LogSQL 方法在 ADOdb 中启用日志记录:
$DB->LogSQL(true);
启用日志记录后,所有 SQL 连同执行次数和 HTTP 请求的 URL 路径一起转储到特殊的 ADODB_LOGSQL 表中。可以使用 ADOdb 性能监控用户界面 (UI)(使用以下代码调用)检查已记录的 SQL:
<?php 
include_once('/path/to/adodb.inc.php');
session_start(); # session variables required for monitoring
$conn = ADONewConnection('oci8');
$conn->Connect($tnsname,$user,$pwd);
$perf =& NewPerfMonitor($conn);
$perf->UI($pollsecs=5);
?>
以下是此用户界面的屏幕截图:

 

 

例如,假设您单击第一个 SQL 语句,使 ADOdb 对以下 SQL 运行 EXPLAIN PLAN
select * from (SELECT /*+FIRST_ROWS*/ * FROM myhist WHERE accno ='7314028790' 
ORDER BY POSTDT desc) where rownum <= :adodb_offset
ADOdb 将从 PLAN_TABLE 中读取 EXPLAIN PLAN 的结果并显示它们:
<table>
  <tr>
<td>OPERATION</td>
<td>OBJECT_NAME</td>
<td>COST</td>
<td>CARDINALITY</td>
<td>BYTES</td>
  </tr>
  <tr>
<td>SELECT STATEMENT</td>
    <td> </td>
    <td>63</td>
    <td>456</td>
    <td>39216</td>
  </tr>
  <tr>
<td>--COUNT STOPKEY</td>
    <td> </td>
    <td> </td>
    <td> </td>
    <td> </td>
  </tr>
  <tr>
<td>----VIEW</td>
    <td> </td>
    <td>63</td>
    <td>456</td>
    <td>39216</td>
  </tr>
  <tr>
<td>------SORT ORDER BY STOPKEY</td>
    <td> </td>
    <td>63</td>
    <td>456</td>
    <td>39216</td>
  </tr>
  <tr>
<td>--------TABLE ACCESS FULL</td>
    <td> </td>
    <td>55</td>
    <td>456</td>
    <td>39216</td>
  </tr>
</table>
Scripts Affected:
4   localhost/juris/z/tloan/myloan.php
在以上示例中您将看到,尽管您只选择一个帐号 (ACCNO) 的记录,但此查询将执行 TABLE ACCESS FULL 而不检查索引。此行为暗示着需要对 MYHIST.ACCNO 创建索引。

 

COUNT STOPKEY 表示当 rownum <= :adodb_offset 时,该查询将停止执行。还要注意,该示例显示了在其上执行 SQL 的页面 (myloan.php),以及 SQL 的执行次数:在本示例中为四次。“Cardinality”是预计受影响记录数,“Bytes”是预计处理字节数。

 

要对 ACCNO 创建索引,请使用:
CREATE INDEX ON myhist(accno)
创建索引后,请再次运行 EXPLAIN PLAN。现在,Oracle 将对 ACCNO 使用新建的索引:
<table>
  <tr>
<td>OPERATION</td>
<td>OBJECT_NAME</td>
<td>COST</td>
<td>CARDINALITY</td>
<td>BYTES</td>
  </tr>
  <tr>
<td>SELECT STATEMENT</td>
    <td> </td>
    <td>10</td>
    <td>456</td>
    <td>39216</td>
  </tr>
  <tr>
<td>--COUNT STOPKEY</td>
    <td> </td>
    <td> </td>
    <td> </td>
    <td> </td>
  </tr>
  <tr>
<td>----VIEW</td>
    <td> </td>
    <td>10</td>
    <td>456</td>
    <td>39216</td>
  </tr>
  <tr>
<td>------SORT ORDER BY STOPKEY</td>
    <td> </td>
    <td>2</td>
    <td>456</td>
    <td>39216</td>
  </tr>
  <tr>
<td>--------TABLE ACCESS FULL</td>
    <td> </td>
    <td>1</td>
    <td>182</td>
    <td>39216</td>
  </tr>
</table>
另人鼓舞的是,由于有了索引,预计的查询成本从 63 减少为 10。

 

6. 维护数据并管理业务逻辑

 

开始编码或部署之前,要精心设计数据库结构。维护数据时,最重要的事情是正确地组织数据。以下是几个技巧:

 

使用自动递增字段。自动递增字段在作为主键时很有用。在 Oracle 中,自动递增字段是使用触发器和序列(它们是用于生成单调递增数字的特殊函数)实现的。

 

下面,我们将为表 ATABLE 创建一个自动递增字段 A_ID。
CREATE SEQUENCE aSEQUENCE;

CREATE OR REPLACE TRIGGER A_ID
BEFORE INSERT ON ATABLE
FOR EACH ROW
BEGIN
SELECT aSEQUENCE.nextVal INTO :NEW.A_ID FROM dual;
END;
每次创建记录时,触发器都将激活一个新值并将其插入到 A_ID 中。要检索 PHP 中 aSEQUENCE 的当前值,通常需要获取上次插入的值(使用 ADOdb),请运行:
$id = $DB->GetOne('SELECT aSEQUENCE.currVal FROM DUAL');
当您要调用函数并以结果集的形式检索结果时,您要使用 DUAL 这一特殊的 Oracle 表。

 

规范化数据。规范化超出了本文的范畴,但你应该意识到应对数据进行设计以让所有记录都具有逻辑主键。此外,还应将单个表中任何包含一对多关系的记录分解为两个或更多表。

 

例如,如果表 AUTHOR 具有以下字段:
AUTHOR:AuthorName Address Title1 ISBN1 Title2 ISBN2 Title3 ISBN3
字段 Book1、Book2 和 Book3 与 AuthorName 保持一对多的关系,这在以后会产生问题,如处理其名称对应三个标题的作者。

 

为避免这些问题,应将该表分解为 AUTHOR 和 BOOK:
AUTHOR:AuthorName Address
BOOK:ISBN AuthorName Title
使用索引。请谨慎选择索引。索引将最大限度地提高性能。

 

初学者通常犯的错误是为每个重要字段分别创建索引。但如果最常用的查询使用的使用三个字段有三个不同的索引,Oracle 将很有可能选择只使用一个索引。这种情况下,创建一个包含所有三个字段的复合索引可能更有意义。

 

使用 EXPLAIN PLAN 和 SQL 日志记录功能(详见上个部分所述)检查创建索引后查询的执行情况。

 

使用索引编排表。如果是频繁访问数据的方式是以集群模式来检索记录的,则应考虑使用索引编排表 (IOT),也称作集群索引。在特殊表中,主键索引与数据存储在两个不同的文件中。在 IOT 中,数据存储在主键索引中。由于索引叶按顺序存储,因此基于主键检索连续记录数据的速度非常快。

 

以下是一个有关 IOT 的适用情形的示例:您计划用 PHP 编写一个主题论坛,其中显示所有帖子及其相关回复。将帖子和相关答复一起存储到硬盘上是很有意义的。这样,只需读取几个磁盘块便可以检索整个主题。为此,可以使用以下复合键创建 IOT:
(id of the first post of the thread, the actual id of the post)
管理业务逻辑。您完全可以用 PHP 编写您的关键业务逻辑,但改用 PL/SQL 编写它具有以下优点:
  • 对关键业务逻辑进行集中控制。如果运行服务器场,则不必更新所有服务器上的 PHP 脚本便可以完成全面更改。
  • 由于 PL/SQL 与 Oracle 的数据库之间的紧密结合,因此与用 PHP 进行编码相比,可能会实现更好的性能
  • 具有 Oracle 接口的其他语言(如 Java、Python 和 Visual Basic)现在可以访问同一函数。
例如,假设您正在开发一个购物车应用程序。您拥有大量产品,且某些产品在捆绑出售的情况下可以获得特殊的折扣价格。由于某些客户有资格获得批量折扣价格,因此定价进一步复杂化。

 

您有一个名为 CalcShoppingCartTotal($cartID) 的 PHP 函数,用于捕获此业务逻辑。它返回购物车的总价值。随着时间的推移,此函数成为性能瓶颈,因为您希望运行基于 Java 的软件的用户也可以使用该函数。这是一个可以移植到 PL/SQL 的理想函数:
CREATE OR REPLACE FUNCTION CalcShoppingCartTotal(
cartid IN number) RETURN number
IS
begin
/* some code */
end;
用 PL/SQL 重写此过程后,使用 ADOdb 调用它是一项简单的操作:
$total = $DB->GetOne("SELECT CalcShoppingCartTotal(:0) FROM DUAL",array($cartid));
现在,假设业务要求已经更改,CalcShoppingCartTotal() 必须返回多个值,即购物代码和总值。因此,您需要将它改写为一个具有两个新 OUT 参数的过程:
CREATE OR REPLACE PROCEDURE CalcShoppingCartTotal(
cartid IN number, 
shipcode OUT number,
total OUT number)
IS
begin
/* some code */
end;
可以在 ADOdb 中使用以下代码执行此存储过程:
$sql = "begin CalcShoppingCartTotal(:cartid, :shipcode, :total); end;";
$stmt = $DB->PrepareSP($sql);
$DB->InParameter($stmt, $cartid, 'cartid');
$DB->OutParameter($stmt, $shipCode, 'shipcode');
$DB->OutParameter($stmt, $cartTotal, 'total');
$DB->Execute($stmt);
print "<p>total = $cartTotal, shipping code=$ shipCode</p>";
7. 时刻不忘“任务控制”:持久连接和 Apache/IIS

 

就所需的时间和资源而言,数据库登录是一个开销很高的过程。鉴于网页通常在瞬间执行,因此减少此开销是很关键的。

 

PHP 尝试通过使用持久连接避免此开销。PHP 维护一个持久数据库连接池,该连接池在需要时将连接分配给 PHP 脚本。当您调用 OCI8 函数 OCIPLogon 或 OCINLogon,或调用 ADOdb 函数 PConnect 或 NConnect 时,将回收这些持久连接。

 

持久连接导致 PHP 生成大量数据库连接,其数目与以下内容成正比:
number of Apache or FastCGI processes * number Oracle user id's used
因此,如果在 PHP 中使用两个 Oracle 用户 ID 连接到数据库,并且 Apache 配置为运行 256 个子进程,则每个 Web 服务器的数据库连接数最多为 256 * 2 = 512。

 

随着服务器场中 Web 服务器数量的增加,可以预想将要创建大量连接。默认情况下,Oracle 为每个连接分配大量资源(通常为 2MB 到 3MB),因此这可能会严重影响数据库性能。

 

另一个问题是,如果重新启动数据库服务器,则还需要重新启动 Web 服务器,这是因为现有的持久性连接会失效。

 

一个解决方案是停止在 PHP 端使用持久连接,并使用中间件或数据库端连接池。要在 PHP 中使用非持久性连接,请使用 OCI8 的 OCILogon 或 ADOdb 的 Connect 函数连接。

 

ODBC 等中间件或开放源软件 SQLRelay 提供了连接池,用于通过更具智能化的方式回收数据库连接。这两个驱动程序都可以确定一个持久性连接数上限。但该方法可能需要对 ODBC 或 SQLRelay API 进行大规模的重新编码。

 

另一个出色的解决方案是使用 Oracle 的共享服务器技术(以前称作 MTS)。该方法创建一个共享数据库进程池,当网络请求呼入时,Oracle 将回收该进程池。该方法通过让 Oracle(而非 PHP)回收它的数据库服务器资源来插入 PHP 持久性连接。您可能需要咨询 Oracle 数据库管理员以实现上述过程的顺利进行。当您处理大量连接时,将极大的提升性能。

 

最后建言

 

对于那些将 Apache 或 IIS 用作 Web 服务器的用户而言,我发现,当 Apache 在预派生的模式下运行或当您运行 IIS(将 PHP 配置为使用 FastCGI 运行)时,PHP 和 Oracle 最佳的性能。

 

在这些情况下,每个 PHP 解释程序在独立的进程中运行(详见本文开头所述)。PHP 还可以在多线程模式下运行,但高粒度锁定以及某些 PHP 扩展并非多线程安全并不意味着较低的性能和可靠性。

 

默认情况下,Apache 1.3 在 UNIX 上以预派生模式运行。如果您使用的是 Apache 2.0,请参考您的文档以确保以预派生模式运行。如果在 Windows 上运行,则可以从 phplens.com/phpeverywhere/fastcgi-php 中获取有关 FastCGI 和 PHP 的信息。Zend 的 WinEnabler 是适用于 Windows 的商业 FastCGI 安装程序。

 

祝大家好运,并希望大家的查询速度得到提高!

 


John Lim 居住在马来西亚。他是 ADOdb(一个用于 PHP 的流行数据库)的开发人员。从 1995 年开始,John 的生活就与 Oracle 紧密相联。

 

返回到 DeployPHP 系列索引

将您的意见发送给我们