1. Code
  2. PHP

Hinzufügen von Caching zu einer Datenzugriffsschicht

Scroll to top
18 min read

German (Deutsch) translation by Federicco Ancie (you can also view the original English article)

Dynamische Webseiten sind großartig. Sie können die resultierende Seite an Ihren Benutzer anpassen, die Aktivitäten anderer Benutzer anzeigen, Ihren Kunden basierend auf ihrem Navigationsverlauf verschiedene Produkte anbieten und so weiter. Aber je dynamischer eine Website ist, desto mehr Datenbankabfragen müssen Sie wahrscheinlich durchführen. Leider beanspruchen diese Datenbankabfragen den größten Teil Ihrer Laufzeit.

In diesem Tutorial werde ich einen Weg zeigen, um die Leistung zu verbessern, ohne zusätzliche unnötige Abfragen auszuführen. Wir werden ein Abfrage-Caching-System für unsere Datenschicht mit geringen Programmier- und Bereitstellungskosten entwickeln.

1. Die Datenzugriffsschicht

Das transparente Hinzufügen einer Caching-Ebene zu einer Anwendung ist aufgrund des internen Designs häufig schwierig. Mit objektorientierten Sprachen (wie PHP 5) ist es viel einfacher, aber es kann immer noch durch schlechtes Design kompliziert werden.

In diesem Lernprogramm legen wir unseren Ausgangspunkt in einer Anwendung fest, die den gesamten Datenbankzugriff über eine zentralisierte Klasse ausführt, von der alle Datenmodelle die grundlegenden Datenbankzugriffsmethoden erben. Das Skelett für diese Startklasse sieht folgendermaßen aus:

1
class model_Model {
2
3
    protected static $DB = null;
4
5
    function __construct () {}
6
7
    protected function doStatement ($query) {}
8
9
    protected function quoteString ($value) {}
10
}

Lassen Sie es uns Schritt für Schritt implementieren. Zunächst der Konstruktor, der die PDO-Bibliothek als Schnittstelle zur Datenbank verwendet:

1
    function __construct () {
2
        
3
        // connect to the DB if needed

4
        if ( is_null(self::$DB) ) {
5
           
6
            $dsn = app_AppConfig::getDSN();
7
            $db_user = app_AppConfig::getDBUser();
8
            $db_pass = app_AppConfig::getDBPassword();
9
           
10
            self::$DB = new PDO($dsn, $db_user, $db_pass);
11
           
12
            self::$DB->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
13
        }
14
    }

Wir stellen über die PDO-Bibliothek eine Verbindung zur Datenbank her. Für die Datenbankanmeldeinformationen verwende ich eine statische Klasse mit dem Namen "app_AppConfig", die die Konfigurationsinformationen der Anwendung zentralisiert.

Zum Speichern der Datenbankverbindung verwenden wir ein statisches Attribut ($DB). Wir verwenden ein statisches Attribut, um dieselbe Verbindung mit allen Instanzen von "model_Model" zu teilen. Aus diesem Grund ist der Verbindungscode mit einem if geschützt (wir möchten nicht mehr als einmal eine Verbindung herstellen).

In der letzten Zeile des Konstruktors legen wir das Ausnahmefehlermodell für PDO fest. In diesem Modell wird für jeden Fehler, den das PDO findet, eine Ausnahme (Klasse PDOException) ausgelöst, anstatt Fehlerwerte zurückzugeben. Dies ist Geschmackssache, aber der Rest des Codes kann mit dem außergewöhnlichen Modell, das für dieses Tutorial gut ist, sauberer gehalten werden.

Das Ausführen von Abfragen kann sehr komplex sein, aber in dieser Klasse haben wir einen einfachen Ansatz mit einer einzigen doStatement() -Methode gewählt:

1
    protected function doStatement ($query) {
2
        $st = self::$DB->query($query);
3
        if ( $st->columnCount()>0 ) {
4
            return $st->fetchAll(PDO::FETCH_ASSOC);
5
        } else {
6
            return array();
7
        }
8
    }

Diese Methode führt die Abfrage aus und gibt ein assoziatives Array mit der gesamten Ergebnismenge zurück (falls vorhanden). Beachten Sie, dass wir die statische Verbindung verwenden (self::$DB). Beachten Sie auch, dass diese Methode geschützt ist. Dies liegt daran, dass der Benutzer keine beliebigen Abfragen ausführen soll. Stattdessen stellen wir dem Benutzer konkrete Modelle zur Verfügung. Wir werden dies später sehen, aber bevor wir die letzte Methode implementieren:

1
    protected function quoteString ($value) {
2
        return self::$DB->quote($value,PDO::PARAM_STR);
3
    }

Die Klasse "model_Model" ist eine sehr einfache, aber bequeme Klasse für die Datenschichtung. Obwohl es einfach ist (es kann mit erweiterten Funktionen wie vorbereiteten Anweisungen erweitert werden, wenn Sie möchten), erledigt es die grundlegenden Dinge für uns.

Um den Konfigurationsteil unserer Anwendung abzuschließen, schreiben wir die statische Klasse "app_Config":

1
class app_AppConfig {
2
3
    static public function getDSN () {
4
        return "mysql:host=localhost;dbname=test";
5
    }
6
7
    static public function getDbUser ()  {
8
        return "test";
9
    }
10
11
    static public function getDbPassword () {
12
        return "MyTest";
13
    }
14
}

Wie bereits erwähnt, werden wir konkrete Modelle für den Zugriff auf die Datenbank bereitstellen. Als kleines Beispiel verwenden wir dieses einfache Schema: eine Dokumententabelle und einen invertierten Index, um zu suchen, ob ein Dokument ein bestimmtes Wort enthält oder nicht:

1
CREATE TABLE documents (
2
    id        integer primary key,
3
    owner    varchar(40) not null,
4
    server_location    varchar(250) not null
5
);
6
7
CREATE TABLE words (
8
    word        char(30),
9
    doc_id    integer not null references documents(id),
10
11
    PRIMARY KEY (word,doc_id)
12
)

Aus der grundlegenden Datenzugriffsklasse (model_Model) leiten wir so viele Klassen ab, wie für das Daten-Design unserer Anwendung erforderlich sind. In diesem Beispiel können wir diese beiden selbsterklärenden Klassen ableiten:

1
class model_Index extends model_Model {
2
3
    public function getWord ($word) {
4
        return $this->doStatement("SELECT doc_id FROM words WHERE word=" . $this->quoteString($word));
5
    }   
6
}
7
8
class model_Documents extends model_Model {
9
10
    public function get ($id) {
11
        return $this->doStatement( "SELECT * FROM documents WHERE id=" . intval($id) );
12
    }
13
}

In diesen abgeleiteten Modellen fügen wir die öffentlichen Informationen hinzu. Ihre Verwendung ist äußerst einfach:

1
$index = new model_Index();
2
$words = $index->getWord("coche");
3
var_dump($words);

Das Ergebnis für dieses Beispiel könnte ähnlich aussehen (offensichtlich hängt es von Ihren tatsächlichen Daten ab):

1
array(119) {
2
  [0]=>
3
  array(1) {
4
    ["doc_id"]=>
5
    string(4) "4630"
6
  }
7
  [1]=>
8
  array(1) {
9
    ["doc_id"]=>
10
    string(4) "4635"
11
  }
12
  [2]=>
13
  array(1) {
14
    ["doc_id"]=>
15
    string(4) "4873"
16
  }
17
  [3]=>
18
  array(1) {
19
    ["doc_id"]=>
20
    string(4) "4922"
21
  }
22
  [4]=>
23
  array(1) {
24
    ["doc_id"]=>
25
    string(4) "5373"
26
  }
27
...

Was wir geschrieben haben, wird im nächsten UML-Klassendiagramm gezeigt:

2. Planung unseres Caching-Schemas

Wenn die Dinge auf Ihrem Datenbankserver zusammenbrechen, ist es Zeit, eine Pause einzulegen und über eine Optimierung der Datenschicht nachzudenken. Nachdem Sie Ihre Abfragen optimiert, die richtigen Indizes usw. hinzugefügt haben, besteht der zweite Schritt darin, unnötige Abfragen zu vermeiden: Warum bei jeder Benutzeranforderung dieselbe Anforderung an die Datenbank stellen, wenn sich diese Daten kaum ändern?

Mit einer gut geplanten und gut entkoppelten Klassenorganisation können wir unserer Anwendung fast ohne Programmierkosten eine zusätzliche Ebene hinzufügen. In diesem Fall werden wir die Klasse "model_Model" erweitern, um unserer Datenbankebene transparentes Caching hinzuzufügen.

Die Caching-Grundlagen

Da wir wissen, dass wir ein Caching-System benötigen, konzentrieren wir uns auf dieses spezielle Problem und werden es nach dem Aussortieren in unser Datenmodell integrieren. Im Moment werden wir nicht in SQL-Abfragen denken. Es ist einfach, ein wenig zu abstrahieren und ein ausreichend allgemeines Schema zu erstellen.

Das einfachste Caching-Schema besteht aus [Schlüssel, Daten] -Paaren, wobei der Schlüssel die tatsächlichen Daten identifiziert, die wir speichern möchten. Dieses Schema ist nicht neu, es ist in der Tat analog zu den assoziativen Arrays von PHP und wir verwenden es ständig.

Wir brauchen also eine Möglichkeit, ein Paar zu speichern, zu lesen und zu löschen. Das reicht aus, um unsere Benutzeroberfläche für Cache-Helfer zu erstellen:

1
interface cache_CacheHelper {
2
3
    function get ($key);
4
5
    function put ($key,$data);
6
7
    function delete ($key);
8
}

Die Schnittstelle ist recht einfach: Die get-Methode erhält einen Wert anhand ihres identifizierenden Schlüssels, die put-Methode setzt (oder aktualisiert) den Wert für einen bestimmten Schlüssel und die delete-Methode löscht ihn.

Vor diesem Hintergrund ist es an der Zeit, unser erstes echtes Caching-Modul zu implementieren. Bevor wir dies tun, wählen wir die Datenspeichermethode.

Das zugrunde liegende Speichersystem

Die Entscheidung, eine gemeinsame Schnittstelle (wie cache_CacheHelper) für das Zwischenspeichern von Helfern zu erstellen, ermöglicht es uns, diese nahezu über jedem Speicher zu implementieren. Aber obendrein auf welchem Speichersystem? Es gibt viele davon, die wir verwenden können: gemeinsam genutzten Speicher, Dateien, zwischengespeicherte Server oder sogar SQLite-Datenbanken.

DBM-Dateien werden oft unterschätzt und eignen sich perfekt für unser Caching-System. Wir werden sie in diesem Lernprogramm verwenden.

DBM-Dateien arbeiten naiv mit (Schlüssel-, Daten-) Paaren und sind aufgrund ihrer internen B-Tree-Organisation sehr schnell. Sie übernehmen auch die Zugriffskontrolle für uns: Wir müssen uns nicht darum kümmern, den Cache vor dem Schreiben zu blockieren (wie wir es auf anderen Speichersystemen tun müssen). DBM erledigt das für uns.

DBM-Dateien werden nicht von teuren Servern gesteuert, sondern erledigen ihre Arbeit in einer kompakten Bibliothek auf der Clientseite, die lokal auf die eigentliche Datei zugreift, in der die Daten gespeichert sind. Tatsächlich handelt es sich um eine Familie von Dateiformaten, die alle dieselbe Basis-API für den (Schlüssel-, Daten-) Zugriff haben. Einige von ihnen erlauben wiederholte Schlüssel, andere sind konstant und erlauben keine Schreibvorgänge nach dem ersten Schließen der Datei (cdb) usw. Weitere Informationen hierzu finden Sie unter http://www.php.net/manual/en /dba.requirements.php

Nahezu jedes UNIX-System installiert einen oder mehrere dieser Bibliotheken (wahrscheinlich Berkeley DB oder GNU dbm). In diesem Beispiel verwenden wir das Format "db4" (Sleepycat DB4-Format: http://www.sleepycat.com). Ich habe festgestellt, dass diese Bibliothek häufig vorinstalliert ist, aber Sie können jede gewünschte Bibliothek verwenden (außer natürlich cdb: Wir möchten in die Datei schreiben). Tatsächlich können Sie diese Entscheidung in die Klasse "app_AppConfig" verschieben und für jedes von Ihnen durchgeführte Projekt anpassen.

Mit PHP haben wir zwei Alternativen für den Umgang mit DBM-Dateien: die Erweiterung "dba" (http://php.net/manual/en/book.dba.php) oder das Modul "PEAR::DBA" (http://pear.php.net/package/DBA). Wir werden die Erweiterung "dba" verwenden, die Sie wahrscheinlich bereits in Ihrem System installiert haben.

Moment mal, wir beschäftigen uns mit SQL und Ergebnismengen!

DBM-Dateien arbeiten mit Zeichenfolgen für Schlüssel und Werte. Unser Problem besteht jedoch darin, SQL-Ergebnismengen zu speichern (deren Struktur sehr unterschiedlich sein kann). Wie könnten wir es schaffen, sie von einer Welt in die andere zu konvertieren?

Nun, für Schlüssel ist es sehr einfach, da die tatsächliche SQL-Abfragezeichenfolge einen Datensatz sehr gut identifiziert. Wir können den MD5-Digest der Abfragezeichenfolge verwenden, um den Schlüssel zu verkürzen. Für Werte ist es schwieriger, aber hier sind Ihre Verbündeten die PHP-Funktionen serialize() / unserialize(), mit denen Sie von Arrays in Zeichenfolgen konvertieren können und umgekehrt.

Wie das alles funktioniert, sehen wir im nächsten Abschnitt.

3. Statisches Caching

In unserem ersten Beispiel befassen wir uns mit dem einfachsten Weg, Caching durchzuführen: Caching für statische Werte. Wir werden eine Klasse namens "cache_DBM" schreiben, die die Schnittstelle "cache_CacheHelper" einfach so implementiert:

1
class cache_DBM implements cache_CacheHelper {
2
    protected $dbm = null;
3
4
    function __construct ( $cache_file = null ) {
5
        $this->dbm = dba_popen($cache_file, "c", "db4"); 
6
7
        if ( !$this->dbm ) {
8
            throw new Exception("$cache_file: Cannot open cache file");
9
        }
10
    }
11
12
    function get ($key) {
13
        $data = dba_fetch($key, $this->dbm);
14
        if ( $data !== false ) {
15
            return $data;        
16
        }
17
        return null;
18
    }
19
    
20
    function put ($key,$data) {
21
        if ( ! dba_replace($key, $data, $this->dbm) ) {
22
            throw new Exception("$key: Couldn't store");
23
        }
24
    }
25
    
26
    function delete ($key) {
27
        if ( ! dba_delete($key, $this->dbm) ) {
28
            throw new Exception("$key: Couldn't delete");
29
        }
30
    }
31
}

Diese Klasse ist sehr einfach: eine Zuordnung zwischen unserer Schnittstelle und den dba-Funktionen. Im Konstruktor wird die angegebene Datei geöffnet.
und der zurückgegebene Handler wird im Objekt gespeichert, um ihn in den anderen Methoden zu verwenden.

Ein einfaches Anwendungsbeispiel:

1
$cache = new cache_DBM( "/tmp/my_first_cache.dbm" );
2
$cache->put("key1", "my first value");
3
echo $cache->get("key1");
4
        
5
$cache->delete("key1");
6
$data = $cache->get("key1");
7
if ( is_null($data) ) {
8
    echo "\nCorrectly deleted!";
9
}

Nachfolgend finden Sie, was wir hier getan haben, ausgedrückt als UML-Klassendiagramm:

Fügen wir nun das Caching-System zu unserem Datenmodell hinzu. Wir hätten die Klasse "model_Model" ändern können, um jeder abgeleiteten Klasse Caching hinzuzufügen. Wenn wir dies getan hätten, hätten wir die Flexibilität verloren, die Caching-Eigenschaft nur bestimmten Modellen zuzuweisen, und ich denke, dies ist ein wichtiger Teil unserer Arbeit.

Also werden wir eine weitere Klasse namens "model_StaticCache" erstellen, die "model_Model" erweitert und die Caching-Funktionalität hinzufügt. Beginnen wir mit dem Skelett:

1
class model_StaticCache extends model_Model {
2
    
3
    protected static $cache = array();
4
    protected $model_name = null;
5
    
6
    function __construct () { }
7
8
    protected function doStatement ($query) { }
9
}

Im Konstruktor rufen wir zuerst den übergeordneten Konstruktor auf, um eine Verbindung zur Datenbank herzustellen. Anschließend erstellen und speichern wir statisch ein "cache_DBM" -Objekt (falls nicht zuvor an anderer Stelle erstellt). Wir speichern eine Instanz für jeden abgeleiteten Klassennamen, da wir für jeden eine DBM-Datei verwenden. Zu diesem Zweck verwenden wir das statische Array "$cache".

1
    function __construct () {
2
        parent::__construct();
3
4
        $this->model_name = get_class($this);
5
        if ( ! isset( self::$cache[$this->model_name] ) ) {
6
            $cache_dir = app_AppConfig::getCacheDir();
7
            self::$cache[$this->model_name] = new cache_DBM( $cache_dir . $this->model_name);
8
        }
9
   }

Um festzustellen, in welches Verzeichnis wir die Cache-Dateien schreiben müssen, haben wir erneut die Konfigurationsklasse der Anwendung verwendet: "app_AppConfig".

Und jetzt: die Methode doStatement(). Die Logik für diese Methode lautet: Konvertieren Sie die SQL-Anweisung in einen gültigen Schlüssel, suchen Sie den Schlüssel im Cache, und geben Sie den Wert zurück, falls er gefunden wurde. Wenn nicht gefunden, führen Sie es in der Datenbank aus, speichern Sie das Ergebnis und geben Sie es zurück:

1
    protected function doStatement ($query) {
2
        $key = md5($query);
3
4
        $data = self::$cache[$this->model_name]->get($key);
5
        if ( ! is_null($data) ) {
6
            return unserialize($data);
7
        }
8
        
9
        $data = parent::doStatement($query);
10
        
11
        self::$cache[$this->model_name]->put($key,serialize($data));
12
13
        return $data;
14
    }

Es gibt noch zwei weitere bemerkenswerte Dinge. Zunächst verwenden wir das MD5 der Abfrage als Schlüssel. Tatsächlich ist dies nicht erforderlich, da die zugrunde liegende DBM-Bibliothek Schlüssel beliebiger Größe akzeptiert, es jedoch besser erscheint, den Schlüssel trotzdem zu kürzen. Wenn Sie vorbereitete Anweisungen verwenden, denken Sie daran, die tatsächlichen Werte mit der Abfragezeichenfolge zu verknüpfen, um den Schlüssel zu erstellen!

Sobald der "model_StaticCache" erstellt wurde, ist es trivial, ein konkretes Modell für seine Verwendung zu ändern. Sie müssen nur noch die Klausel "extended" in der Klassendeklaration ändern:

1
class model_Documents extends model_StaticCache {
2
}

Und das ist alles, die Magie ist geschafft! Das "model_Document" führt nur eine Abfrage für jedes abzurufende Dokument aus. Aber wir können es besser machen.

4. Caching-Ablauf

Bei unserem ersten Ansatz bleibt eine Abfrage, sobald sie im Cache gespeichert ist, für immer gültig, bis zwei Dinge auftreten: Wir löschen ihren Schlüssel explizit oder lösen die Verknüpfung der DBM-Datei.

Dieser Ansatz gilt jedoch nur für einige Datenmodelle unserer Anwendung: die statischen Daten (wie Menüoptionen und dergleichen). Die normalen Daten in unserer Anwendung sind wahrscheinlich dynamischer.

Denken Sie an eine Tabelle mit den Produkten, die wir auf unserer Webseite verkaufen. Es ist unwahrscheinlich, dass sich diese Daten jede Minute ändern, aber es besteht die Möglichkeit, dass sich diese Daten ändern (durch Hinzufügen neuer Produkte, Ändern der Verkaufspreise usw.). Wir brauchen eine Möglichkeit, das Caching zu implementieren, aber wir können auf Änderungen in Daten reagieren.

Ein Ansatz für dieses Problem besteht darin, eine Ablaufzeit für die im Cache gespeicherten Daten festzulegen. Wenn wir neue Daten im Cache speichern, legen wir ein Zeitfenster fest, in dem diese Daten gültig sind. Nach dieser Zeit werden die Daten erneut aus der Datenbank gelesen und für einen weiteren Zeitraum im Cache gespeichert.

Nach wie vor können wir mit dieser Funktionalität eine weitere abgeleitete Klasse aus "model_Model" erstellen. Dieses Mal nennen wir es "model_ExpiringCache". Das Skelett ähnelt "model_StaticCache":

1
class model_ExpiringCache extends model_Model {
2
3
    protected static $cache = array();
4
    protected $model_name = null;
5
    protected $expiration = 0;
6
7
    function __construct () { }
8
9
    protected function doStatement ($query) { }
10
}

In dieser Klasse haben wir ein neues Attribut eingeführt: $ expiration. In diesem wird das konfigurierte Zeitfenster für gültige Daten gespeichert. Wir setzen diesen Wert im Konstruktor, der Rest des Konstruktors ist der gleiche wie in "model_StaticCache":

1
    function __construct () {
2
        parent::__construct();
3
4
        $this->model_name = get_class($this);
5
        if ( ! isset( self::$cache[$this->model_name] ) ) {
6
            $cache_dir = app_AppConfig::getCacheDir();
7
            self::$cache[$this->model_name] = new cache_DBM( $cache_dir . $this->model_name);
8
        }
9
10
        $this->expiration = 3600;   // 1 hour

11
   }

Der Großteil des Auftrags kommt im doStatement. Die DBM-Dateien haben keine interne Möglichkeit, den Ablauf von Daten zu steuern, daher müssen wir unsere eigenen implementieren. Wir werden es tun, indem wir Arrays wie dieses speichern:

1
array(
2
        "time" => 1250443188,
3
        "data" => (the actual data)
4
)

Diese Art von Array wird serialisiert und im Cache gespeichert. Der Schlüssel "Zeit" ist die Änderungszeit der Daten im Cache, und die "Daten" sind die tatsächlichen Daten, die wir speichern möchten. Wenn wir beim Lesen feststellen, dass der Schlüssel vorhanden ist, vergleichen wir die gespeicherte Erstellungszeit mit der aktuellen Zeit und geben die Daten zurück, wenn sie nicht abgelaufen sind.

1
    protected function doStatement ($query) {
2
        $key = md5($query);
3
        $now = time();
4
5
        $data = self::$cache[$this->model_name]->get($key);
6
        if ( !is_null($data) ) {
7
            $data = unserialize($data);
8
            if ( $data['time'] + $this->expiration > $now ) {
9
                return $data['data'];
10
            }
11
        }

Wenn der Schlüssel nicht vorhanden ist oder abgelaufen ist, führen wir die Abfrage weiter aus und speichern die neue Ergebnismenge im Cache, bevor wir sie zurückgeben.

1
        $data = parent::doStatement($query);
2
        
3
        self::$cache[$this->model_name]->put( $key, 
4
                serialize( array("data"=>$data,"time"=>$now) ) );
5
6
        return $data;
7
    }

Einfach!

Konvertieren wir nun den "model_Index" in ein Modell mit ablaufendem Cache. Bei "model_Documents" müssen wir nur die Klassendeklaration und die Klausel "extensiv" ändern:

1
class model_Documents extends model_ExpiringCache {
2
}

Über die Ablaufzeit... müssen einige Überlegungen angestellt werden. Der Einfachheit halber verwenden wir eine konstante Ablaufzeit (1 Stunde=3.600 Sekunden) und weil wir den Rest unseres Codes nicht ändern möchten. Wir können es jedoch auf viele Arten leicht ändern, um unterschiedliche Ablaufzeiten zu verwenden, eine für jedes Modell. Danach werden wir sehen wie.

Das Klassendiagramm für alle unsere Aufgaben lautet wie folgt:

5. Unterschiedlicher Ablauf

Ich bin sicher, dass Sie bei jedem Projekt für fast jedes Modell eine andere Ablaufzeit haben werden: von ein paar Minuten bis zu Stunden oder sogar Tagen.

Wenn wir nur für jedes Modell eine andere Verfallszeit haben könnten, wäre es perfekt... aber warten Sie! Wir können es leicht machen!

Der direkteste Ansatz besteht darin, dem Konstruktor ein Argument hinzuzufügen. Der neue Konstruktor für "model_ExpiringCache" lautet also folgender:

1
    function __construct ( $expiration=3600 ) {
2
        parent::__construct();
3
4
        $this->expiration = $expiration;
5
        ...
6
    }

Wenn wir dann ein Modell mit einer Ablaufzeit von 1 Tag (1 Tag=24 Stunden=1.440 Minuten=86.400 Sekunden) wünschen, können wir dies folgendermaßen erreichen:

1
class model_Index extends model_ExpiringCache {
2
    function __construct() {
3
        parent::__construct(86400);
4
    }
5
6
   ...
7
}

Und das ist alles. Der Nachteil ist jedoch, dass wir alle Datenmodelle ändern müssen.

Eine andere Möglichkeit besteht darin, die Aufgabe an die "app_AppConfig" zu delegieren:

1
class app_AppConfig {
2
    ...
3
    public static function getExpirationTime ($model_name) {
4
        switch ( $model_name ) {
5
            case "model_Index":
6
                return 86400;
7
            ...
8
            default:
9
                return 3600;
10
        }
11
    }
12
}

Fügen Sie dann den Aufruf dieser neuen Methode im Konstruktor "model_ExpiringCache" wie folgt hinzu:

1
    function __construct () {
2
        parent::__construct();
3
4
        $this->model_name = get_class($this);
5
6
        $this->expiration = app_AppConfig::getExpirationTime($this->model_name);
7
8
        ...
9
    }

Mit dieser neuesten Methode können wir ausgefallene Dinge tun, z.B. zentralere Verwendung unterschiedlicher Ablaufwerte für Produktions- oder Entwicklungsumgebungen. Wie auch immer, Sie können Ihre wählen.

In UML sieht das Gesamtprojekt folgendermaßen aus:

6. Einige Vorsichtsmaßnahmen

Es gibt einige Abfragen, die nicht zwischengespeichert werden können. Die offensichtlichsten sind das Ändern von Abfragen wie INSERT, DELETE oder UPDATE. Diese Abfragen müssen beim Datenbankserver eingehen.

Aber selbst bei SELECT-Abfragen gibt es einige Umstände, unter denen ein Caching-System Probleme verursachen kann. Schauen Sie sich eine Abfrage wie diese an:

1
SELECT * FROM banners WHERE zone='home' ORDER BY rand() LIMIT 10

Diese Abfrage wählt zufällig 10 Banner für die "Home" -Zone unserer Website aus. Dies soll eine Bewegung in den in unserem Haus angezeigten Bannern erzeugen. Wenn wir diese Abfrage jedoch zwischenspeichern, sieht der Benutzer überhaupt keine Bewegung, bis die zwischengespeicherten Daten ablaufen.

Die Funktion rand() ist nicht deterministisch (wie es jetzt nicht ist() oder andere); Daher wird bei jeder Ausführung ein anderer Wert zurückgegeben. Wenn wir es zwischenspeichern, frieren wir nur eines dieser Ergebnisse für den gesamten Caching-Zeitraum ein und brechen daher die Funktionalität.

Mit einem einfachen Re-Factoring können wir jedoch die Vorteile des Caching nutzen und Pseudozufälligkeit zeigen:

1
class model_Banners extends model_ExpiringCache {
2
3
    public function getRandom ($zone) {
4
        $random_number = rand(1,50);
5
        $banners = $this->doStatement( "SELECT * FROM banners WHERE zone=" . 
6
                $this->quoteString($zone) . 
7
                " AND $random_number = $random_number ORDER BY rand() LIMIT 10" );
8
        return $banners;
9
    }
10
...
11
}

Was wir hier tun, ist, fünfzig verschiedene zufällige Bannerkonfigurationen zwischenzuspeichern und sie zufällig auszuwählen. Die 50 SELECTs sehen folgendermaßen aus:

1
SELECT * FROM banners WHERE zone='home' AND 1=1 ORDER BY rand() LIMIT 10
2
SELECT * FROM banners WHERE zone='home' AND 2=2 ORDER BY rand() LIMIT 10
3
...
4
SELECT * FROM banners WHERE zone='home' AND 50=50 ORDER BY rand() LIMIT 10

Wir haben der Auswahl eine konstante Bedingung hinzugefügt, die für den Datenbankserver keine Kosten verursacht, aber 50 verschiedene Schlüssel für das Caching-System rendert. Ein Benutzer muss die Seite fünfzig Mal laden, um alle verschiedenen Konfigurationen des Banners anzuzeigen. So wird der dynamische Effekt erreicht. Die Kosten betragen fünfzig Abfragen an die Datenbank, um den Cache abzurufen.

7. Benchmark

Welche Vorteile können wir von unserem neuen Caching-System erwarten?

Zunächst muss gesagt werden, dass unsere neue Implementierung bei der Rohleistung manchmal langsamer ausgeführt wird als Datenbankabfragen, insbesondere bei sehr einfachen, gut optimierten Abfragen. Bei Abfragen mit Joins wird unser DBM-Cache jedoch schneller ausgeführt.

Das Problem, das wir gelöst haben, ist jedoch nicht die Rohleistung. Sie werden niemals einen Ersatzdatenbankserver für Ihre Tests in der Produktion haben. Sie werden wahrscheinlich einen Server mit hoher Arbeitslast haben. In dieser Situation kann selbst die schnellste Abfrage langsam ausgeführt werden, aber mit unserem Caching-Schema verwenden wir nicht einmal den Server, und tatsächlich reduzieren wir dessen Arbeitslast. Die tatsächliche Leistungssteigerung wird also in Form von mehr Petitionen pro Sekunde erfolgen.

Auf einer Website, die ich gerade entwickle, habe ich einen einfachen Benchmark durchgeführt, um die Vorteile des Caching zu verstehen. Der Server ist bescheiden: Auf Ubuntu 8.10 läuft ein AMD Athlon 64 X2 5600+ mit 2 GB RAM und einer alten PATA-Festplatte. Das System führt Apahce und MySQL 5.0 aus, die mit der Ubuntu-Distribution ohne Optimierung geliefert werden.

Der Test ist so: das Benchmark-Programm (ab) von Apache mit 1, 5 und 10 gleichzeitigen Clients auszuführen, die eine Seite 1.000 Mal von meiner Entwicklungswebsite laden. Die eigentliche Seite war ein Produktdetail mit nicht weniger als 20 Abfragen: Menüinhalt, Produktdetails, empfohlene Produkte, Banner usw.

Die Ergebnisse ohne Cache betrugen 4,35 p/s für 1 Client, 8,25 für 5 Clients und 8,29 für 10 Clients. Mit Caching (unterschiedlicher Ablauf) betrugen die Ergebnisse 25,55 p/s bei 1 Client, 49,01 bei 5 Clients und 48,74 bei 10 Clients.

Schlussfolgerung

Ich habe Ihnen eine einfache Möglichkeit gezeigt, wie mandas Caching in Ihr Datenmodell einzufügen kann. Natürlich gibt es eine Vielzahl von Alternativen, aber diese ist nur eine Wahl, die Sie haben.

Wir haben lokale DBM-Dateien zum Speichern der Daten verwendet, aber es gibt noch schnellere Alternativen, die Sie möglicherweise untersuchen sollten. Einige Ideen für die Zukunft: Verwendung von apc_store() von APC als zugrunde liegendes Speichersystem, gemeinsamer Speicher für die wirklich kritischen Daten, Verwendung von Memcached usw.

Ich hoffe, dieses Tutorial hat Ihnen genauso gut gefallen, wie mir. Viel Spaß beim Caching!