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中還原,在網絡上甚至異構平臺之間進行傳輸和識別。在電子商務領域中,這個特性是很是有用的。 程序員