PHP沉思录php
做者:左轻侯 html
建立时间:2007-05-10 09:47:29 最后修改时间:2010-12-31 23:19:57 mysql
本文发表于《程序员》5月号
是一个系列的第一篇,目前想到的其余一些主题是:
SQL注入问题
事件模型
AOP模型
UI Framework的实现
Template机制
PHP沉思录
工做模型
PHP的工做模型很是特殊。从某种程度上说,PHP和ASP、ASP.NET、JSP/Servlet等流行的Web技术,有着本质上的区别。
以Java为例,Java在Web应用领域,有两种技术:Java Servlet和JSP(Java Server Page)。Java Servlet是一种特殊类型的Java程序,它经过实现相关接口,处理Web服务器发送过来的请求,完成相应的工做。JSP在形式上是一种相似于PHP的脚本,可是事实上,它最后也被编译成Servlet。也就是说,在Java解决方案中,JSP和Servlet是做为独立的Java应用程序执行的,它们在初始化以后就驻留内存,经过特定的接口和Web服务器通讯,完成相应工做。除非被显式地重启,不然它们不会终止。所以,能够在JSP和Servlet中使用各类缓存技术,例如数据库链接池。
ASP.NET的机制与此相似。至于ASP,虽然也是一种解释型语言,可是仍然提供了Application对象来存放应用程序级的全局变量,它依托于ASP解释器在IIS中驻留的进程,在整个应用程序的生命期有效。
PHP却彻底不是这样。做为一种纯解释型语言,PHP脚本在每次被解释时进行初始化,在解释完毕后终止运行。这种运行是互相独立的,每一次请求都会建立一个单独的进程或线程,来解释相应的页面文件。页面建立的变量和其余对象,都只在当前的页面内部可见,没法跨越页面访问。在终止运行后,页面中申请的、没有被代码显式释放的外部资源,包括内存、数据库链接、文件句柄、Socket链接等,都会被强行释放。
也就是说,PHP没法在语言级别直接访问跨越页面的变量,也没法建立驻留内存的对象。见下例:
<?php
class StaticVarTester {
public static $StaticVar = 0;
}
function TestStaticVar() {
StaticVarTester :: $StaticVar += 1;
echo "StaticVarTester :: StaticVar = " . StaticVarTester :: $StaticVar;
}
TestStaticVar();
echo "<br/>";
TestStaticVar();
?>
在这个例子中,定义了一个名为StaticVarTester的类,它仅有一个公共的静态成员$StaticVar,并被初始化为0。而后,在TestStaticVar()函数中,对StaticVarTester :: $StaticVar进行累加操做,并将它打印输出。
熟悉Java或C++的开发者对这个例子应该并不陌生。$StaticVar做为StaticVarTester类的一个静态成员,只在类被装载时进行初始化,不管StaticVarTester类被实例化多少次,$StaticVar都只存在一个实例,并且不会被屡次初始化。所以,当第一次调用TestStaticVar()函数时,$StaticVar进行了累加操做,值为1,并被保存。第二次调用TestStaticVar()函数,$StaticVar的值为2。
打印出来的结果和咱们预料的同样:
StaticVarTester :: StaticVar = 1
StaticVarTester :: StaticVar = 2
可是,当浏览器刷新页面,再次执行这段代码时,不一样的状况出现了。在Java或C++里面,$StaticVar的值会被保存并一直累加下去,咱们将会看到以下的结果:
StaticVarTester :: StaticVar = 3
StaticVarTester :: StaticVar = 4
…
可是在PHP中,因为上文叙及的机制,当前页面每次都解释时,都会执行一次程序初始化和终止的过程。也就是说,每次访问时,StaticVarTester都会被从新装载,而下列这行语句
public static $StaticVar = 0;
也会被重复执行。当页面执行完成后,全部的内存空间都会被回收,$StaticVar这个变量(连同整个StaticVarTester类)也就不复存在。所以,不管刷新页面多少次,$StaticVar变量都会回到起点:先被初始化为0,而后在TestStaticVar()函数调用中被累加。因此,咱们看到的结果永远是这个:
StaticVarTester :: StaticVar = 1
StaticVarTester :: StaticVar = 2
PHP这种独特的工做模型的优点在于,基本上解决了使人头疼的资源泄漏问题。Web应用的特色是大量的、短期的并发处理,对各类资源的申请和释放工做很是频繁,很容易致使泄漏。同时,大量的动态html脚本的存在,使得追踪和调试的工做都很是困难。PHP的运行机制,以一种很是简单的方式避免了这个问题,同时也避免了将程序员带入到繁琐的缓冲池和同步等问题中去。在实践中,基于PHP的应用每每比基于Java或.NET的应用更加稳定,不会出现因为某个页面的BUG而致使整个站点崩溃的问题。(相比之下,Java或.NET应用可能由于缓冲池崩溃或其余的非法操做,而致使整个站点崩溃。)后果是,即便PHP程序员水平不高,也没法写出使整个应用崩溃的代码。PHP脚本能够一次调用极多的资源,从而致使页面执行极为缓慢,可是执行完毕后全部的资源都会被释放,应用仍然不会崩溃。甚至即便执行了一个死循环,也会在30秒(默认时间)后由于超时而停止。从理论上来讲,基于PHP的应用是不太可能崩溃的,由于它的运行机制决定它不存在常规的崩溃这个问题。在实践中,不少开发者也认为PHP是最稳定的Web应用。
可是,这种机制的缺点也很是明显。最直接的后果是,PHP在语言级别没法实现跨页面的缓冲机制。这种缓冲机制缺失形成的影响,能够分红两个方面:
一是对象的缓冲。如咱们所知,不少设计模式都依赖于对象的缓冲机制,对于须要频繁应付大量并发的服务端软件更是如此。所以,对象缓冲的缺失,理论上会极大地下降速度。可是,因为PHP自己的定位和工做机制等缘由,它在实际工做中的速度很是快。就做者本身的经验来看,在小型的Web应用中,PHP至少不比Java慢。在大型的应用中,为了榨干每一分硬件资源,即便PHP自己足够快,一个优秀的对象缓冲机制仍然是必要的。在这种状况下,可使用第三方的内存缓冲软件,如Memcached。因为Memcached自己的优异特性(高性能,支持跨服务器的分布式存储,和PHP的无缝集成等),在大型的PHP应用中,Memcached几乎已经成为不可或缺的基础设施了。比起使用PHP语言本身实现对象缓冲来,这种第三方解决方案彷佛更好一些。
二是数据库链接的缓冲。对MySQL,PHP提供了一种内置的数据库缓冲机制,使用起来很是简单,程序员须要作的只是用mysql_pconnect()代替mysql_connect()来打开数据库而已。PHP会自动回收被废弃的数据库链接,以供重复使用。具备讽刺意味的是,在实际应用中,这种持久性数据库链接每每会致使数据库链接的伪泄漏现象:在某个时间,并发的数据库链接过多,超过了MySQL的最大链接数,从而致使新的进程没法链接数据库。可是过一段时间,当并发数减小时,PHP会释放掉一些链接,网站又会恢复正常。出现这种现象的缘由是,当使用pconnect时,Apache的httpd进程会不释放connect,而当Apache的httpd进程数超过了mysql的最大链接数时,就会出现没法链接的状况。所以,须要当心地调整Apache和Mysql的配置,以使Apache的httpd进程数不会超出MySQL的最大链接数。在某些状况下,一些有经验的PHP程序员宁肯继续使用mysql_connect(),而不是mysql_pconnect()。
就做者所知,在PHP将来的roadmap中,对于工做模型这一部分,没有根本性的变更。这是PHP的缺点,也是PHP的优点,从本质上说,这就是PHP的独特之处。所以,咱们很难期待PHP在近期内会对这一问题作出重大的改变。可是,在对待这个问题带来的一系列后果时,咱们必须谨慎应对。
数据库访问接口
长期以来,PHP都缺少一个象ADO或JDBC那样的统一的数据库访问接口。PHP在访问不一样的数据库时,使用不一样的专门API。例如,使用mysql_connect函数链接MySQL,使用ora_logon函数链接Oracle。平心而论,这种方式并无象咱们想象的那样麻烦。在真实项目中,把系统从一种数据库彻底迁移到另外一种数据库的要求是比较少见的,特别是对于LAMP这样的小型项目而言。并且,只要将访问数据库的代码进行了良好的封装,迁移的工做量也会较少。另外,使用专门API,在效率上多少会有一些优点。
虽然如此,PHP的开发人员仍然在努力构建PHP的统一的数据库访问接口。从PHP 5.1开始,PHP的发行包内置了PDO(PHP Data Objects,PHP数据对象)。PDO具备以下特性:
统一的数据库访问接口。PDO为访问不一样的数据库提供了统一的接口,而且可以经过切换数据库驱动程序,方便地支持各类流行的数据库。
面向对象。PDO彻底基于PHP 5的对象机制,所以区别于基于过程的专用API。
高性能。PDO的底层用C编写,比起用纯PHP开发的其余相似解决方案,有更高的性能。
一个典型的PDO应用以下例:
$pdo = new PDO("mysql:host=localhost;dbname=justtest", " mysql_user ", " mysql_password");
$query = "SELECT id, username FROM userinfo ORDER BY ID";
foreach ($pdo->query($query) as $row) {
echo $row['id']." | ".$row['username']."<br/>";
}
可是,PDO还有一个更重要的问题没有解决,那就是对数据集的抽象。
不管是ADO仍是JDBC,除了提供统一的数据库访问接口之外,也提供了对数据集的抽象。也就是说,在经过ADO/JDBC取回数据集结果之后,这些数据集以统一的格式被存放在RecordSet/RowSet对象中,业务逻辑代码只须要与数据集对象进行交互便可。对数据集进行抽象的直接后果,是完全地分离了业务逻辑层和数据库访问层的代码,而且也在某种程度上起到了OR Mapping的效果。
自从ADO.NET出现后,数据集的抽象又有了一次不小的进步。和ADO相比,ADO.NET中的DataTable/DataSet类的主要新特性以下:
非链接性。在传统的ADO模型中,数据集须要占用一个数据库链接,直到全部工做完成。一旦链接被关闭,数据集的内容也就失效了。ADO.NET中的数据集是非链接的,也就是说,当链接被关闭后,数据集中的内容仍然保存。这种非链接性带来的直接后果是,数据库链接能够被最大限度地利用,由于一旦工做完成就能够将链接返回到数据库链接池中。(ADO也支持非链接的数据集,可是须要程序员本身实现,而ADO.NET的数据集在本质上就是非链接的。)
自描述性。ADO.NET中的数据集是彻底自我描述的,并且具备完备的metadata,其内容不但能够从任何特定的数据库生成,并且能够由代码动态生成。DataSet能够跟踪数据的变化,并完成相应的操做。
互操做性。因为非链接性和自描述性,ADO.NET中的数据集能够很是方便地在网络之间进行传输。DataSet能够序列化/反序列化为XML或其余特定的格式。这样,DataSet不但能够用于同一平台的分布式网络环境,并且能够用于异构网络环境。
Java从J2SE 5.0开始内置了CachedRowSet,其原理和ADO.NET的数据集相似。Borland开发的用于取代BDE的新一代数据库引擎dbExpress,其改进也与此相似。
与之对比,PHP中对数据集的支持显得很是原始。不管是传统的API仍是PDO,取回的数据仅仅表现为数组,而且没有任何缓存机制。这意味着,在全部须要访问数据集的地方,都必须频繁地使用直接访问数据库的API。下面是一个使用mysql API的例子:
$link = mysql_connect('localhost', 'mysql_user', 'mysql_password');
if (!$link) {
die('Could not connect: ' . mysql_error());
}
mysql_select_db("justtest");
$result = mysql_query("SELECT id, username FROM userinfo ORDER BY ID");
while ($row = mysql_fetch_array($result)) {
echo $row['id']." | ".$row['username']."<br/>";
}
mysql_close($link);
这样作的后果是业务逻辑和访问数据库的代码没法分离,在规模较大的系统中尤为严重。
就做者所知,PHP官方没有提供支持抽象数据集的计划。可是,本身实现这样一个数据集并非一件难事。做者参照ADO.NET的架构,使用纯PHP代码编写了一个规模很是小的数据抽象类库,姑且称之为MyPDO。MyPDO大约有1300行代码,在几个真实项目中工做得很好。因为篇幅所限,本文不列出它的全部代码,仅仅给出几个最主要的类的描述:
DataAdapter接口:定义了全部与数据库操做相关的方法。
ConceptDataAdapter类:实现了DataAdapter,封装了访问特定数据库的代码。如MySqlDataAdapter。
DataSet类:实现了自描述的抽象数据集,与具体数据库无关。能够自我跟踪数据的变化。
SqlCommandBuilder类:能够跟据DataSet类的内容,自动实现Insert、Update、Delete等操做。
下面是MyPDO的一个典型应用:
$Conn = new MySqlDataConnection(new ConnectionInfo("localhost", "mysql_username", "mysql_password", "justtest", "gbk"));
$Da = $Conn->GetDataAdapter();
$Da->SetSqlString("SELECT id, username FROM userinfo ORDER BY ID");
$Ds = new DataSet();
$Da->Fill($Ds);
$Conn->Disconnect(); // 关闭数据库链接,但$Ds仍然保存数据内容
echo $Ds; // 调用DataSet的__tostring()方法,格式化输出内容
经过替换MySqlDataConnection,能够以最小的成本实现不一样数据库之间的切换。
因为上文中讨论过的PHP的工做模型,经过MyPDO实现的缓存在性能上得到的好处有限。可是,在采用Memcached的解决方案中,MyPDO仍是可以带来很大的便利。由于只有基于非链接方式的数据集,才可能在Memcached这样的内存池中被缓存。
另外,因为MyPDO中的DataSet是自描述的,内置了WriteToXml和ReadFromXml方式,它无需程序员编码就能够保存为XML或从XML中还原,在网络上甚至异构平台之间进行传输和识别。在电子商务领域中,这个特性是很是有用的。 程序员