0251 / 590 837 15
info@a-coding-project.de
;

Caching (CMS)

Unser CMS ist mittlerweile etwas langsamer geworden, was an der hohen Anzahl an Datenabfragen liegt. Im Dashboard bin ich auf 182 Datenbankabfragen gekommen – Eindeutig zu viel.

Durch das Caching können wir die Anzahl der Abfragen verringern und die Ergebnisse z.B. in Dateien zwischenspeichern. So ist der Datenbankzugrif an einigen Stellen nicht mehr nötig.

Ich plane ein Caching-System mit drei Komponenten:

  • Cache – Basisklasse
  • FileCache – Dateizwischenspeicher
  • MemoryCache – Aufruf-Zwischenspeicher (pro Seitenaufruf bleiben bestimmte Daten erhalten, beim nächsten Aufruf sind sie weg)

Heute fangen wir erst einmal mit der Basisklasse und dem FileCache an, den MemoryCache, werden wir vermutlich zur Beta-Phase einbauen. Die Cache-Klasse ruft derzeit nur die FileCache-Klasse auf, wird jedoch später dann auch den MemoryCache aufrufen. Klasse /system/classes/cache.php:

<?PHP
  Class Cache{
    static function setData($area, $key, $value){
      FileCache::setData($area,$key,$value);
    }

    static function getData($area,$key){
      return FileCache::getData($area,$key);
    }

    static function clear($area = ""){
      FileCache::clear($area);
    }

    static function contains($area,$key){
      return FileCache::contains($area,$key);
    }
  }
?>

Was die Funktionen machen, wird dir denke ich schon klar sein. Die area werde ich mal erklären. Und zwar cachen wir ja verschiedene Dinge, wie zum Beispiel die Settings- oder Menüobjekte. Diese haben selbst bestimmte Parameter, an welchem die Datensätze wiedererkannt werden. Beim Menü ist dies zum Beispiel die id. Die area ist also quasi der Typ, den wir Speichern.

In der FileCache-Klasse speichern wir alle Cache-Dateien im neuen Ordner /cache. Dieser wird automatisiert vom FileCache angelegt. Die einzelnen areas, haben dann jeweils einen Unterordner. So können wir einfach den Ordner leeren und haben alle Cache-Objekte vom Typ gelöscht.

In Code sieht das dann so aus (system/classes/filecache.php):

<?PHP

  class FileCache extends Cache{
    public static function setData($area, $key, $value){
      $destination=fopen(self::getFileName($area, $key),"w");
      fwrite($destination,serialize($value));
      fclose($destination);
    }

    public static function getData($area, $key){
      $source=fopen(self::getFileName($area, $key),"r");
      $code = "";
      while ($a=fread($source,1024)){
        $code.= $a;
      }
      return unserialize($code);
    }

    public static function contains($area, $key){
      return file_exists(self::getFileName($area, $key));
    }

    public static function clear($area = ""){
      $path = self::getCacheDir($area);
      $dir = @opendir ($path);
      while (($entry = @readdir($dir)) !== false) {
        if ($entry == '.' || $entry == '..') continue;
        if (is_file ($path.'/'.$entry) || is_link ($path.'/'.$entry)) {
          $res = @unlink ($path.'/'.$entry);
        }
      }
      closedir ($dir);
      rmdir ($path);
    }

    public static function getCacheDir($area){
      $dir = Settings::getInstance->get("root")."cache/".$area."/";
      if(!file_exists(Settings::getInstance->get("root")."cache")
        mkdir(Settings::getInstance->get("root")."cache");
      }
      if(!file_exists($dir)){
        mkdir($dir);
      }
      return $dir;
    }

    protected static function getFileName($area, $key){
      return self::getCacheDir($area).md5($key).".txt";
    }
  }

?>

Ich habe bereits an einigen Stellen den Cache eingebau. Zum Beispiel zeige ich einmal das Cachen der Menüs. In der Klasse /system/classes/menu.php gibt es die Funktion getCode. Diese muss immer aufgerufen werden, sobald wir das Menü anzeigen möchten. Und genau hier Cache ich das Ergebnis, so müssen wir zum Beispiel für Untermenüs keine neuen Abfragen mehr absenden:

  static function getCode($id, $globalstart,$globalend, $elementstart,$elementend,$class){
    if(Cache::contains("menu",$id."|".$globalstart."|".$globalstart."|".$elementstart."|".$elementend."|".$class)){
      $res = Cache::getData("menu",$id."|".$globalstart."|".$globalstart."|".$elementstart."|".$elementend."|".$class);
    }
    else{
      $res     = $globalstart;
      $entries = self::getEntries($id);
      $i = 1;
      if($entries){
        foreach($entries as $entry){
          $res .= $entry->getCode($globalstart,$globalend, $elementstart,$elementend,$class,$i);
          $i++;
        }
      }
      $res .= $globalend;
      Cache::setData("menu",$id."|".$globalstart."|".$globalstart."|".$elementstart."|".$elementend."|".$class,$res);
    }
    return $res;
  }

Ich muss hier alle Parameter dem Key übergeben, da auch die Rückgabe je nach Parameter anders ausfallen kann. An allen Stellen, wo wir das Menü bearbeiten müssen wir den Menu-Cache leeren. So zum Beispiel in der Funktion addEntry:

  function addEntry($menu,$title,$href){
    $maxID = $GLOBALS['db']->ReadField("SELECT MAX(id) FROM {'dbprefix'}menu 
                                        WHERE menuID = '".$menu."'");
    $id = $maxID + 1;
    $res = $GLOBALS['db']->Execute("INSERT INTO {'dbprefix'}menu (id,menuID,title,href)
                                    VALUES('".$id."','".$menu."',
                                    '".$title."','".$href."')");
    if($res){
      $args['menu']  = $menu;
      $args['title'] = $title;
      $args['href']  = $href;
      EventManager::raiseEvent("menu_entry_added","../",$args);
    }
    Cache::clear("menu");
    return $res;
  }

Ich hoffe die Funktionalität ist klar geworden. Wir könnten auch noch extra Ordner für Plugins anlegen, aber das ist noch nicht so wichtig. Durch die jetztige Optimierung hab ich die Datenbankabfragen schonmal von 182 auf 56 reduziert (was immer noch hoch ist).

Und zum Abschluss noch eine gute Nachricht: Du musst nicht neu installieren! Ich hab die Änderungen als Update zur Verfügung gestellt. Bei Google kannst du in Revision 70 alles nachverfolgen.