肖理達(dá) (KrazyNio AT hotmail.com), 2005.06.07, 轉(zhuǎn)載請(qǐng)注明出處
一直想寫一篇關(guān)于動(dòng)態(tài)頁面 cache 的文章,但每次“提筆”卻又放棄,,因?yàn)榭偸怯X得準(zhǔn)備得還不夠充分,。今天埋頭寫下,只是希望對(duì)自己的工作做一些筆錄,。
1,、問題起源
我們經(jīng)常會(huì)在一個(gè)動(dòng)態(tài)頁面中加入很多個(gè)人信息,以 CMS 首頁為例,,用戶登錄之前顯示登錄框,,登錄之后顯示其用戶名,并根據(jù)權(quán)限顯示其可用模塊的鏈接,。由于每個(gè)用戶登錄之后,,顯示出來的動(dòng)態(tài)信息都是不一樣的,,所以這部分無法進(jìn)行 cache,我們將這部分信息定義為“[u]個(gè)人信息[/u]”,,它的特性是根據(jù)登錄用戶進(jìn)行動(dòng)態(tài)改變,。
現(xiàn)在問題來了,就是一個(gè) CMS 的首頁,,訪問者的登錄概率并不是百分百的,,應(yīng)該說有一大部分人訪問首頁是沒有登錄的,這個(gè)時(shí)候的首頁是一個(gè)公共的頁面,,沒有任何個(gè)人信息,,或者說這時(shí)候首頁的任何動(dòng)態(tài)信息都是可以轉(zhuǎn)換成靜態(tài)的,也就是說這部分是可 cache 的,。
2,、使用 JavaScript 分離個(gè)人信息
解決這個(gè)問題的方法有很多種,一種是將個(gè)人信息和其他信息進(jìn)行分離,,如在 CMS 首頁中加入一個(gè)外部的 JavaScript 文件,,而這個(gè)文件的內(nèi)容實(shí)際上是由 PHP 動(dòng)態(tài)生成的。CMS 首頁 index.php 代碼片段:
<script language="JavaScript" src="/js/personal.php"></script>
….
<script language="JavaScript">
document.write(sUser);
document.write(sLinks);
</script>
personal.php 代碼片段:
<?php
session_start();
header("Cache-Control: no-store, no-cache, must-revalidate");
?>
sUser = "<?php echo $_SESSION['USER']; ?>";
sLinks = "<?php echo addslashes($_SESSION['LINKS']); ?>";
這種方法比較適合個(gè)人信息較少,,易于集中顯示的情況,。通過 JavaScript 外掛代碼實(shí)現(xiàn)個(gè)人信息分離之后,personal.php 是永不 cache 的,,這樣也就可以放心地對(duì) CMS 首頁進(jìn)行 cache 了,,具體的 cache 方法可采用 304 HTTP 頭與 Cache_Lite 相結(jié)合的方式,這在后邊有詳細(xì)代碼示例,。
3,、選擇性分離個(gè)人信息
一旦個(gè)人信息較多,很難對(duì)其進(jìn)行分離的時(shí)候,,上述方法實(shí)現(xiàn)起來就會(huì)比較麻煩了,。接下來介紹一種比較放寬的 cache 方式,就是只對(duì)用戶未登錄的 CMS 首頁進(jìn)行 cache,,一旦發(fā)現(xiàn)用戶處于登錄狀態(tài),,則跳過 cache 部分,直接運(yùn)行相關(guān)代碼,,我把這種方法稱為“[u]選擇性 cache[/u]”,。這種方法中,我們犧牲了一部分可 cache 的情況,,但很大程度上提高了個(gè)人信息的可擴(kuò)充性,,也就是說個(gè)人信息的多少、顯示的位置等等都不再受到限制,,這是值得的,,畢竟修改服務(wù)端代碼要比修改大量的 JavaScript 代碼要來得方便,,而且 JavaScript 的調(diào)試也會(huì)比較麻煩。
分析一下這種 cache 方式,,概要流程圖如下:
代碼片段如下:
<?php
session_cache_limiter("must-revalidate");
session_start();
if (empty($_SESSION['USER'])) { //未登錄時(shí)才使用 cache
…. //輸出 cache
} else {
// 直接輸出頁面內(nèi)容,,此條件下與未使用 cache 時(shí)一樣
$data =& get_data();
echo $data;
} //end if
?>
這里需要說明的一點(diǎn)是,由于 PHP 在使用 SESSION 時(shí),,php.ini 中默認(rèn)設(shè)置的 session.cache_limiter 為 nocache,,所以需要修改 cache_limiter,設(shè)置成 must-revalidate,,使得客戶端再次瀏覽當(dāng)前頁時(shí)必須發(fā)送相關(guān) HTTP 頭信息到服務(wù)器進(jìn)行驗(yàn)證,然后才決定是否加載客戶端本地 cache,。不要把客戶端本地 cache 與服務(wù)器端 cache 搞混,,之后的代碼片段中會(huì)充分利用客戶端 cache 和 服務(wù)器端 cache 機(jī)制達(dá)到緩存的目的。關(guān)于 must-revalidate,,請(qǐng)參考 HTTP 規(guī)格說明書 RFC 2612 的 14.9.4 章節(jié),。
上邊的代碼并不完整,接下來是更加深入的探討客戶端 cache 機(jī)制了,,利用客戶端 cache,,可以有效地減輕服務(wù)器端負(fù)載。首先了解一下 HTTP 頭:Last-Modified 與 If-Modified-Since,。簡單的說,,Last-Modified 與If-Modified-Since 都是用于記錄頁面最后修改時(shí)間的 HTTP 頭信息,只是 Last-Modified 是由服務(wù)器往客戶端發(fā)送的 HTTP 頭,,而 If-Modified-Since 則是由客戶端往服務(wù)器發(fā)送的頭,,其工作原理圖如下:
可以看到,再次請(qǐng)求本地存在的 cache 頁面時(shí),,客戶端會(huì)通過 If-Modified-Since 頭將先前服務(wù)器端發(fā)過來的 Last-Modified 最后修改時(shí)間戳發(fā)送回去,,這是為了讓服務(wù)器端進(jìn)行驗(yàn)證,通過這個(gè)時(shí)間戳判斷客戶端的頁面是否是最新的,,如果不是最新的,,則返回新的內(nèi)容,如果是最新的,,則返回 304 告訴客戶端其本地 cache 的頁面是最新的,,于是客戶端就可以直接從本地加載頁面了,這樣在網(wǎng)絡(luò)上傳輸?shù)臄?shù)據(jù)就會(huì)大大減少,,同時(shí)也減輕了服務(wù)器的負(fù)擔(dān),。想要詳細(xì)查看 HTTP 頭信息,可以在 Firefox 中安裝 LiveHTTPHeaders 插件,,安裝完成之后按 Alt+L 就可以在 Sidebar 中看到了,。
現(xiàn)在再來完善之前的 index.php 代碼:
<?php
session_cache_limiter("must-revalidate");
session_start();
function &get_data()
{
//此函數(shù)用于獲取本頁面的輸出內(nèi)容
//….
} //end function
if (empty($_SESSION['USER'])) { //未登錄時(shí)才使用 cache
//====================================================
// 1. 檢查 HTTP 頭是否符合 304 的條件
//====================================================
//get_last_modified() 函數(shù)需要另外單獨(dú)實(shí)現(xiàn),,此函數(shù)用于獲取服務(wù)器端 cache 文件的最后修改時(shí)間,可將時(shí)間戳保存在數(shù)據(jù)庫中,。
$last_modified = get_last_modified();
$headers = getallheaders();
if (strtotime($headers['If-Modified-Since']) == $last_modified) {
// 返回 304 并結(jié)束程序運(yùn)行
header('HTTP/1.1 304 Not Modified');
exit;
} //end if
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $last_modified) . ' GMT');
//====================================================
// 2. 檢查 cache 文件是否存在
//====================================================
require_once 'Cache/Lite.php';
// 參數(shù)設(shè)置
$options = array(
'cacheDir' => './cache/',
'lifeTime' => 86400, //最大 cache 一天時(shí)間
'fileNameProtection' => false //使用 CMS 自身提供的 id 作為名字
);
$cache = new Cache_Lite($options); //創(chuàng)建 Cache_Lite 對(duì)象
$id = md5($_SERVER['REQUEST_URI']); //生成對(duì)應(yīng)于 cache 文件的 ID
if ($data = $cache->get($id)) { //存在 cache 文件,,獲取內(nèi)容,直接輸出
echo $data;
} else {
$data =& get_data();
echo $data;
flush();
$cache->save($data); //保存 cache
} //end if
} else {
$data =& get_data();
echo $data;
} //end if
?>
測試時(shí)可以使用 LivHTTPHeaders 插件,,你將會(huì)看到第一次訪問時(shí)是返回 200,,第二次到第N次訪問時(shí)則返回了 304,而登錄之后,,則一直都返回 200,,因?yàn)槲覀冞x擇性 cache 之后,對(duì)登錄之后一律運(yùn)行程序輸出,,而不使用 cache,,如果之后需要對(duì)輸出的個(gè)人信息進(jìn)行修改,只需要改函數(shù) get_data() 即可,,也避免了 JavaScript 的調(diào)試,。
總結(jié)
除此之外,還有其它方法可以實(shí)現(xiàn)分離個(gè)人信息,,緩存動(dòng)態(tài)頁面的目的,。而且為了提高服務(wù)器運(yùn)行效率,還可以使用數(shù)據(jù)庫 cache,、Squid 反向代理等,,如 ADOdb 的 cache。目前用的比較多的 Drupal 應(yīng)用的就是本文中提到的第二種方法,。
參考資料
HTTP Caching & Cache-Busting for Content Publishers
Hypertext Transfer Protocol — HTTP/1.1
PHP Anthology, Volume 2: Applications. Chapter 5: Caching
Caching Tutorial for Web Authors and Webmasters
Drupal 源代碼