Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Advanced PHP Programming

.pdf
Скачиваний:
67
Добавлен:
14.04.2015
Размер:
7.82 Mб
Скачать

258 Chapter 10 Data Component Caching

Cache size maintenance is particularly necessary when you’re using shared memory. Unlike file-based caches or DBM files, shared memory segments cannot be grown dynamically.This means you need to take extra care to ensure that the cache does not overfill. In a C application, you would handle this by storing access information in shared memory and then using that information to perform cache maintenance.

You can do the same in PHP, but it’s much less convenient.The problem is the granularity of the shared memory functions. If you use the shm_get_var and shm_put_var functions (from the sysvshm extension), you are easily able to add variables and extract them. However, you are not able to get a list of all elements in the segment, which makes it functionally impossible to iterate over all elements in the cache. Also, if you wanted access statistics on the cache elements, you would have to implement that inside the elements themselves.This makes intelligent cache management close to impossible.

If you use the shmop functions (from the shmop extension), you have a lower-level interface that allows you to read, write, open, and close shared memory segments much as you would a file.This works well for a cache that supports a single element (and is similar to the suggested uses for a flat file), but it buys you very little if you want to store multiple elements per segment. Because PHP handles all memory management for the user, it is quite difficult to implement custom data structures on a segment returned from

shmop_open().

Another major issue with using System V IPC is that shared memory is not reference counted. If you attach to a shared memory segment and exit without releasing it, that resource will remain in the system forever. System V resources all come from a global pool, so even an occasional lost segment can cause you to quickly run out of available segments. Even if PHP implemented shared memory segment reference counting for you (which it doesn’t), this would still be an issue if PHP or the server it is running on crashed unexpectedly. In a perfect world this would never happen, but occasional segmentation faults are not uncommon in Web servers under load.Therefore, System V shared memory is not a viable caching mechanism.

Cookie-Based Caching

In addition to traditional server-side data caching, you can cache application data on the client side by using cookies as the storage mechanism.This technique works well if you need to cache relatively small amounts of data on a per-user basis. If you have a large number of users, caching even a small amount of data per user on the server side can consume large amounts of space.

A typical implementation might use a cookie to track the identity of a user and then fetch the user’s profile information on every page. Instead, you can use a cookie to store not only the user’s identity but his or her profile information as well.

For example, on a personalized portal home page, a user might have three customizable areas in the navigation bar. Interest areas might be

Cookie-Based Caching

259

nRSS feeds from another site

nLocal weather

nSports scores

nNews by location and category

You could use the following code to store the user’s navigation preferences in the table user_navigation and access them through the get_interests and set_interest methods:

<?php

require DB.inc; class User {

public $name; public $id;

public function _ _construct($id) { $this->id = $id;

$dbh = new DB_Mysql_Test; $cur = $dbh->prepare(SELECT

name FROM

users u WHERE

userid = :1); $row = $cur->execute($id)->fetch_assoc(); $this->name = $row[name];

}

public function get_interests() { $dbh = new DB_Mysql_Test(); $cur = $dbh->prepare(SELECT

interest,

position

FROM

user_navigation

WHERE

userid = :1);

$cur->execute($this->userid); $rows = $cur->fetchall_assoc(); $ret = array();

foreach($rows as $row) { $ret[$row[position]] = $row[interest];

}

return $ret;

}

public function set_interest($interest, $position) { $dbh = new DB_Mysql_Test;

260 Chapter 10 Data Component Caching

$stmtcur = $dbh->prepare(REPLACE INTO

user_navigation

SET

interest = :1 position = :2

WHERE

userid = :3); $stmt->execute($interest, $position, $this->userid);

}

}

?>

The interest field in user-navigation contains a keyword like sports-football or news-global that specifies what the interest is.You also need a generate_navigation_element() function that takes a keyword and generates the content for it.

For example, for the keyword news-global, the function makes access to a locally cached copy of a global news feed.The important part is that it outputs a complete HTML fragment that you can blindly include in the navigation bar.

With the tools you’ve created, the personalized navigation bar code looks like this:

<?php

$userid = $_COOKIE[MEMBERID]; $user = new User($userid); if(!$user->name) {

header(Location: /login.php);

}

$navigation = $user->get_interests(); ?>

<table>

<tr>

<td>

<table>

<tr><td>

<?= $user->name ?>s Home <tr><td>

<!-- navigation postion 1 -->

<?= generate_navigation_element($navigation[1]) ?> </td></tr>

<tr><td>

<!-- navigation postion 2 -->

<?= generate_navigation($navigation[2]) ?> </td></tr>

<tr><td>

<!-- navigation postion 3 -->

<?= generate_navigation($navigation[3]) ?>

TEAM

FLY

Cookie-Based Caching

261

 

</td></tr>

</table>

</td>

<td>

<!-- page body (static content identical for all users) -->

</td>

</tr>

</table>

When the user enters the page, his or her user ID is used to look up his or her record in the users table. If the user does not exist, the request is redirected to the login page, using a Location: HTTP header redirect. Otherwise, the user’s navigation bar preferences are accessed with the get_interests() method, and the page is generated.

This code requires at least two database calls per access. Retrieving the user’s name from his or her ID is a single call in the constructor, and getting the navigation interests is a database call; you do not know what generate_navigation_element() does internally, but hopefully it employs caching as well. For many portal sites, the navigation bar is carried through to multiple pages and is one of the most frequently generated pieces of content on the site. Even an inexpensive, highly optimized query can become a bottleneck if it is accessed frequently enough. Ideally, you would like to completely avoid these database lookups.

You can achieve this by storing not just the user’s name, but also the user’s interest profile, in the user’s cookie. Here is a very simple wrapper for this sort of cookie access:

class Cookie_UserInfo { public $name;

public $userid; public $interests;

public function _ _construct($user = false) { if($user) {

$this->name = $user->name; $this->interests = $user->interests();

}

else {

if(array_key_exists(USERINFO, $_COOKIE)) { list($this->name, $this->userid, $this->interests) =

unserialize($_cookie[USERINFO]);

}

else {

throw new AuthException(no cookie);

}

}

}

public function send() {

$cookiestr = serialize(array($this->name, $this->userid,

262 Chapter 10 Data Component Caching

$this->interests)); set_cookie(USERINFO, $cookiestr);

}

}

class AuthException { public $message;

public function _ _construct($message = false) { if($message) {

$this->message = $message;

}

}

}

You do two new things in this code. First, you have an infrastructure for storing multiple pieces of data in the cookie. Here you are simply doing it with the name, ID, and interests array; but because you are using serialize, $interests could actually be an arbitrarily complex variable. Second, you have added code to throw an exception if the user does not have a cookie.This is cleaner than checking the existence of attributes (as you did earlier) and is useful if you are performing multiple checks. (You’ll learn more on this in Chapter 13,“User Authentication and Session Security.”)

To use this class, you use the following on the page where a user can modify his or her interests:

$user = new User($name); $user->set_interest(news-global, 1);

$cookie = new Cookie_UserInfo($user);

$cookie->send();

Here you use the set_interest method to set a user’s first navigation element to global news.This method records the preference change in the database.Then you create a Cookie_UserInfo object.When you pass a User object into the constructor, the Cookie_UserInfo object’s attributes are copied in from the User object.Then you call send(), which serializes the attributes (including not just userid, but the user’s name and the interest array as well) and sets that as the USERINFO cookie in the user’s browser.

Now the home page looks like this:

try {

$usercookie = new Cookie_UserInfo();

}

catch (AuthException $e) { header(Location /login.php);

}

$navigation = $usercookie->interests;

?>

<table>

<tr>

Cookie-Based Caching

263

<td>

<table>

<tr><td>

<?= $usercookie->name ?> </td></tr>

<?php for ($i=1; $i<=3; $i++) { ?> <tr><td>

<!-- navigation position 1 -->

<?= generate_navigation($navigation[$i]) ?> </td></tr>

<?php } ?> </table>

</td>

<td>

<!-- page body (static content identical for all users) --> </td>

</tr>

</table>

Cache Size Maintenance

The beauty of client-side caching of data is that it is horizontally scalable. Because the data is held on the client browser, there are no concerns when demands for cache storage increase.The two major concerns with placing user data in a cookie are increased bandwidth because of large cookie sizes and the security concerns related to placing sensitive user data in cookies.

The bandwidth concerns are quite valid. A client browser will always attach all cookies appropriate for a given domain whenever it makes a request. Sticking a kilobyte of data in a cookie can have a significant impact on bandwidth consumption. I view this largely as an issue of self-control. All caches have their costs. Server-side caching largely consumes storage and maintenance effort. Client-side caching consumes bandwidth. If you use cookies for a cache, you need to make sure the data you cache is relatively small.

Byte Nazis

Some people take this approach to an extreme and attempt to cut their cookie sizes down as small as possible. This is all well and good, but keep in mind that if you are serving 30KB pages (relatively small) and have even a 1KB cookie (which is very large), a 1.5% reduction in your HTML size will have the same effect on bandwidth as a 10% reduction on the cookie size.

This just means that you should keep everything in perspective. Often, it is easier to extract bandwidth savings by trimming HTML than by attacking relatively small portions of overall bandwidth usage.

Cache Concurrency and Coherency

The major gotcha in using cookies as a caching solution is keeping the data current if a

264Chapter 10 Data Component Caching

user switches browsers. If a user uses a single browser, you can code the application so that any time the user updates the information served by the cache, his or her cookie is updated with the new data.

When a user uses multiple browsers (for example, one at home and one at work), any changes made via Browser A will be hidden when the page is viewed from Browser B, if that browser has its own cache. On the surface, it seems like you could just track what browser a user is using or the IP address the user is coming from and invalidate the cache any time the user switches.There are two problems with that:

nHaving to look up the user’s information in the database to perform this comparison is exactly the work you are trying to avoid.

nIt just doesn’t work.The proxy servers that large ISPs (for example, AOL, MSN) employ obscure both the USER_AGENT string sent from the client’s browser and the IP address the user is making the request from.What’s worse, the apparent browser type and IP address often change in midsession between requests.This means that it is impossible to use either of these pieces of information to authenticate the user.

What you can do, however, is time-out user state cookies based on reasonable user usage patterns. If you assume that a user will take at least 15 minutes to switch computers, you can add a timestamp to the cookie and reissue it if the cookie becomes stale.

Integrating Caching into Application Code

Now that you have a whole toolbox of caching techniques, you need to integrate them into your application. As with a real-world toolbox, it’s often up to programmer to choose the right tool. Use a nail or use a screw? Circular saw or hand saw? File-based cache or DBM-based cache? Sometimes the answer is clear; but often it’s just a matter of choice.

With so many different caching strategies available, the best way to select the appropriate one is through benchmarking the different alternatives.This section takes a realworld approach by considering some practical examples and then trying to build a solution that makes sense for each of them.

A number of the following examples use the file-swapping method described earlier in this chapter, in the section “Flat-File Caches.”The code there is pretty ad hoc, and you need to wrap it into a Cache_File class (to complement the Cache_DBM class) to make your life easier:

<?php

class Cache_File { protected $filename; protected $tempfilename; protected $expriration; protected $fp;

Integrating Caching into Application Code

265

public function _ _construct($filename, $expiration=false) { $this->filename = $filename;

$this->tempfilename = $filename..getmypid(); $this->expiration = $expiration;

}

public function put($buffer) {

if(($this->fp = fopen($this->tempfilename, w)) == false) { return false;

}

fwrite($this->fp, $buffer); fclose($this->fp); rename($this->tempfilename, $this->filename); return true;

}

public function get() { if($this->expiration) {

$stat = @stat($this->filename); if($stat[9]) {

if(time() > $modified + $this->expiration) { unlink($this->filename);

return false;

}

}

}

return @file_get_contents($this->filename);

}

public function remove() { @unlink($filename);

}

}

?>

Cache_File is similar to Cache_DBM.You have a constructor to which you pass the name of the cache file and an optional expiration.You have a get() method that performs expiration validation (if an expiration time is set) and returns the contents of the cache files.The put() method takes a buffer of information and writes it to a temporary cache file; then it swaps that temporary file in for the final file.The remove() method destroys the cache file.

Often you use this type of cache to store the contents of a page from an output buffer, so you can add two convenience methods, begin() and end(), in lieu of put() to capture output to the cache:

public function begin() {

if(($this->fp = fopen($this->tempfilename, w)) == false) {

return false;

266 Chapter 10 Data Component Caching

}

ob_start();

}

public function end() { $buffer = ob_get_contents(); ob_end_flush(); if(strlen($buffer)) {

fwrite($this->fp, $buffer); fclose($this->fp); rename($this->tempfilename, $this->filename); return true;

}

else { flcose($this->fp);

unlink($this->tempfilename); return false;

}

}

To use these functions to cache output, you call begin() before the output and end() at the end:

<?php

require_once Cache/File.inc;

$cache = Cache_File(/data/cachefiles/index.cache); if($text = $cache->get()) {

print $text;

}

else { $cache->begin();

?>

<?php

// do page generation here

?>

<?php

$cache->end();

}

?>

Caching Home Pages

This section explores how you might apply caching techniques to a Web site that allows users to register open-source projects and create personal pages for them (think pear.php.net or www.freshmeat.net).This site gets a lot of traffic, so you would like

Integrating Caching into Application Code

267

to use caching techniques to speed the page loads and take the strain off the database. This design requirement is very common; the Web representation of items within a store, entries within a Web log, sites with member personal pages, and online details for

financial stocks all often require a similar templatization. For example, my company allows for all its employees to create their own templatized home pages as part of the company site.To keep things consistent, each employee is allowed certain customizable data (a personal message and resume) that is combined with other predetermined personal information (fixed biographic data) and nonpersonalized information (the company header, footer, and navigation bars).

You need to start with a basic project page. Each project has some basic information about it, like this:

class Project {

// attributes of the project public $name;

public $projectid;

public $short_description; public $authors;

public $long_description; public $file_url;

The class constructor takes an optional name. If a name is provided, the constructor attempts to load the details for that project. If the constructor fails to find a project by that name, it raises an exception. Here it is:

public function _ _construct($name=false) {

if($name) {

$this->_fetch($name);

}

}

And here is the rest of Project:

protected function _fetch($name) { $dbh = new DB_Mysql_Test;

$cur = $dbh->prepare( SELECT

*

FROM projects

WHERE

name = :1); $cur->execute($name);

$row = $cur->fetch_assoc(); if($row) {

$this->name = $name;

$this->short_description = $row[short_description];

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]