PHPGangsta - Der praktische PHP Blog

PHP Blog von PHPGangsta


Archive for the ‘Zend_Cache’ tag

PHP beschleunigen mittels Caching: Zend_Cache

with 6 comments

Der Titel ist vielleicht nicht ganz korrekt: PHP selbst kann man durch Caching nicht direkt beschleunigen, aber PHP-Applikationen.

Sobald man mit Datenbanken und Objekten arbeitet, fällt einem schnell auf, dass man gern für vieles eigene Objekte bastelt. Häufig ist es so, dass es für fast jede Tabelle eine Klasse gibt, und jede Zeile einer Tabelle einem Objekt entspricht. Das artet recht schnell aus, sodass man sehr viele Objekte hat, die auch hier und dort mehrfach erstellt werden. Das kostet vor allem Rechenkapazität.

Hier mal einige Klassen, mit denen wir weiter unten arbeiten werden:

class App_User
{
	private $username;
	private $newsletter;
	
	public function __construct($id) {
		$db = Zend_Registry::get('Zend_Db');
		$data = $db->fetchAll('SELECT Username, Newsletter FROM User WHERE UserID='.$id);
		$this->setUsername($data['Username']);
		$this->setNewsletter($data['Newsletter']);
	}
	
	public function getUsername() {
		return $this->username;
	}
	
	public function setUsername($username) {
		$this->username = $username;
	}
	
	public function getNewsletter() {
		return $this->newsletter;
	}
	
	public function setNewsletter($newsletter) {
		$this->newsletter = $newsletter;
	}
	
	public static function getAllUsers() {
		$db = Zend_Registry::get('Zend_Db');
		$allIds = $db->fetchCol('SELECT UserID FROM User');
		
		$users = array();
		foreach ($allIds as $id) {
			$users[] = new App_User($id);
		}
		return $users;
	}
}

Wenn man nun zum Beispiel alle User der Webseite überprüfen will, ob sie den Newsletter empfangen wollen, tut man dies objektorientiert dann so:

$newsletterCounter = 0;
$allUsers = App_User::getAllUsers();
foreach ($allUsers as $user) {
	if ($user->getNewsletter()) {
		$newsletterCounter++;
	}
}

An anderer Stelle irgendwo anders im Code (möglicherweise tief in anderen Klassen versteckt) möchte man dann vielleicht noch alle User durchgehen und ihre Usernamen ausgeben:

$allUsers = App_User::getAllUsers();
foreach ($allUsers as $user) {
	echo $user->getUsername().'<br>';
}

Nehmen wir weiter an, wir haben 5000 User in unserer Datenbank. Was passiert nun? Richtig, es werden in beiden Fällen jeweils 5000 User-Objekte erzeugt, ein Attribut abgefragt, und dann braucht man sie nicht mehr. 10000 Datenbankabfragen + 10000 Objektinstanziierungen.

Was können wir dagegen tun? Es gibt mehrere Möglichkeiten. Wir können zum Beispiel nach dem ersten Aufruf der getAllUsers()-Funktion das Ergebnis in einer globalen Variablen speichern:

$allUsers = App_User::getAllUsers();
$GLOBALS['allUsers'] = $allUsers;

Das ist vergleichbar mit der Zend_Registry, es funktioniert intern ähnlich, ist aber weit schöner und ein Zugriff „aus Versehen“ wird vermieden:

$allUsers = App_User::getAllUsers();
Zend_Registry::set('allUsers', $allUsers);

Der Zugriff würde dann so aussehen:

Zend_Registry::get('allUsers');

Das würde zwar funktionieren, ist aber ziemlich unpraktisch, da man nie weiß, wo genau der erste Zugriff ist, man also nicht genau weiß, ob die Informationen bereits in der Zend_Registry sind oder nicht. Das bedeutet viele if-Abfragen und ist unhandlich. Also verschieben wir den „Cache“ etwas weiter nach „innen“, wir verändern die getAllUsers()-Funktion wie folgt:

public static function getAllUsers() {
	if (!Zend_Registry::isRegistered('allUsers')) {
		$db = Zend_Registry::get('Zend_Db');
		$allIds = $db->fetchCol('SELECT UserID FROM User');
		
		$users = array();
		foreach ($allIds as $id) {
			$users[] = new App_User($id);
		}
		Zend_Registry::set('allUsers', $users);
	}
	
	return Zend_Registry::get('allUsers');
}

Nun wird also beim Aufruf von getAllUsers() beim ersten Mal die Datenbank abgefragt, und das Ergebnis in der Zend_Registry gespeichert. Beim zweiten Aufruf wird nun das bereits gespeicherte Ergebnis genommen. Wir sparen uns also viele Datenbankabfragen und Objekterstellungen. Von „außen“ kann man die Funktion ganz normal verwenden, man merkt nicht, dass intern gecacht wird.

Zwischenstand: Wir können innerhalb eines Scriptes viele Abfragen sparen, indem wir Ergebnisse und Objekte in der Zend_Registry speichern und diese bei Bedarf wiederverwenden.

Doch wir können noch mehr an Performance gewinnen, indem wir prozessübergreifend cachen. Wenn also 10 Besucher innerhalb von 5 Sekunden auf unserer Webseite unterwegs sind, sollen diese wenn möglich die selben Daten teilen, sodass für diese 10 Besucher nur einmal die 5000 Datensätze abgefragt und die entsprechenden Objekte erstellt werden müssen. Das geht nun nicht mehr mit globalen Variablen bzw. Zend_Registry, man muß mittels gemeinsamen Speichers (zB Memcached-Server, Festplatte, Netzspeicher) diese Daten austauschen. Diese gemeinsamen Daten sollen allerdings nach einer gewissen Zeit „ungültig“ werden, sodass regelmäßig frische und aktuelle Daten aus der Datenbank geholt werden. Genau das alles kann Zend_Cache.

Zend_Cache besteht grundlegend aus zwei Schichten: Dem Frontend und dem Backend. Das Backend definiert man nur einmal am Anfang, indem man den gewünschten Storage wählt und spezifiziert. Zur Auswahl stehen derzeit: File, Sqlite, Memcached, Apc, Xcache, ZendPlatform, TwoLevels, ZendServer_Disk
Die gebräuchlichsten dürften die ersten vier sein.

Wenn wir nun beispielsweise Zend_Cache_Backend_File wählen, müssen wir nur den Dateipfad angeben, die anderen Einstellungen können wir vorerst vernachlässigen.

Das Frontend ist die Schicht, über die wir den eigentlichen Cache ansprechen. Hier stehen uns mehrere Möglichkeiten zur Verfügung: Wir können beispielsweise einfache Variablen cachen, aber auch ganze Funktionen, Klassen, Dateien oder Seiten. Wir wollen uns hier erstmal nur um Variablen kümmern, die anderen Dinge könnt ihr euch ja im ZF-Manual nachlesen.

Nun aber Butter bei die Fische:

$frontendOptions = array(
   'lifetime' => 60, // cache lifetime of 1 minute
   'automatic_serialization' => true
);

$backendOptions = array(
    'cache_dir' => './tmp/' // Directory where to put the cache files
);

// getting a Zend_Cache_Core object
$cache = Zend_Cache::factory('Core',
                             'File',
                             $frontendOptions,
                             $backendOptions);
Zend_Registry::set('Zend_Cache', $cache);

Hier haben wir nun den Cache erstellt. Hier sieht man auch, dass man eine Lifetime definieren kann. Liegt ein Element länger als eine Minute im Cache, wird es gelöscht und muß dementsprechend neu aus der Datenbank geholt werden.

public static function getAllUsers() {
$cache = Zend_Registry::get(‚Zend_Cache‘);
// see if a cache already exists:
if(!$allUsers = $cache->load(‚allUsers‘)) {
// cache miss; connect to the database
$db = Zend_Registry::get(‚Zend_Db‘);
$allIds = $db->fetchCol(‚SELECT UserID FROM User‘);

$allUsers = array();
foreach ($allIds as $id) {
$allUsers[] = new App_User($id);
}
$cache->save($allUsers, ‚allUsers‘);
}

return $allUsers;
}
Wie man sieht, es ist der Zend_Registry Lösung sehr ähnlich. Es braucht nicht mehr als 20 Zeilen, um Caching zu aktivieren und zu nutzen. Nun haben wir die Freiheit, das Backend zu wählen, die Lebensdauer der Elemente zu spezifizieren, und bei Bedarf kann man auch Tags setzen. Tags sind vor allem dazu da, alle Elemente mit einem bestimmten Tag gleichzeitig zu löschen. Näheres dazu auch im ZF-Manual.

Ich hoffe man sieht, dass man durch Caching ordentlich Performance gewinnen kann, und sowohl die Besucher als auch die Hardware schonen kann. Natürlich macht Caching nicht überall Sinn (bei einem Ajax-Chat wäre es wohl eher hinderlich), aber die meisten Inhalte ändern sich nicht sekündlich, sondern eher in größeren Zeitabständen, und wenn ein Besucher einige Minuten „veraltete“ Inhalte zu sehen bekommt, ist das nicht unbedingt schlimm.

Morgen poste ich einen Artikel, der auch zu diesem Themenkomplex passt, aber das ganze von der anderen Seite betrachtet.

Written by Michael Kliewe

Juli 9th, 2009 at 6:50 pm

Posted in PHP

Tagged with , ,