Cache
Über die Klasse JTL\Cache\JTLCache
sowie die zugehörigen Backend-Klassen
in <Shop-Root>/includes/src/Cache/Methods/
wird ein Objektcache bereitgestellt,
welcher auch in Plugins genutzt werden kann.
Die Konfiguration erfolgt im Backend über den Menüpunkt "System -> Cache".
Standardmäßig unterstützt JTL-Shop die folgenden Caching-Methoden:
- Redis
- Memcache(d)
- APC
- SQLite
- Dateien
- Dateien (erweitert)
Darüber hinaus erfolgt eine Gruppierung von Cache-Einträgen über Gruppen und Tags.
Cache-Group-Tags
Die zur Verfügung stehenden Standard-Gruppen lauten:
Gruppe | Beschreibung |
---|---|
CACHING_GROUP_CATEGORY |
Kategorien |
CACHING_GROUP_LANGUAGE |
Sprachwerte |
CACHING_GROUP_TEMPLATE |
Templates und Templateoptionen |
CACHING_GROUP_OPTION |
Allgemeine Optionen |
CACHING_GROUP_PLUGIN |
Plugins und Optionen |
CACHING_GROUP_CORE |
Wichtige Core-Daten |
CACHING_GROUP_OBJECT |
Allgemeine Objekte |
CACHING_GROUP_BOX |
Boxen |
CACHING_GROUP_NEWS |
Newseinträge/Archiv |
CACHING_GROUP_ATTRIBUTE |
Attribute |
CACHING_GROUP_MANUFACTURER |
Herstellerdaten |
Warum Tags?
Wenn ein beliebiges Datum unter einer eindeutigen ID gespeichert wird, ist es schwierig, diesen Eintrag wieder zu invalidieren.
Entweder müsste dazu die genaue ID bekannt sein oder es müssten sämtliche Einträge auf einmal gelöscht werden. Letzteres würde zu einem sehr häufigen Neuaufbau des Caches führen. Andererseits müssen Cache-IDs aber so genau wie möglich sein. Falls beispielsweise eine Produktobjekt im Cache gespeichert werden soll, hängen dessen Daten von verschiedenen Faktoren wie aktueller Sprache, Kundengruppe etc. ab.
Haben sich z. B. durch die Synchronisation mit JTL-Wawi Produktdaten geändert, muss dieser Eintrag nun aber invalidiert werden. Entweder indem alle Cache-IDs gelöscht werden, oder indem alle zulässigen Werte einzeln gelöscht werden. So müssten also für alle Sprachen und alle Kundengruppen Cache-IDs generiert und anschließend alle gelöscht werden.
Einfacher ist dies mit Tags:
Jedes Produkt wird im Cache zusätzlich zur eindeutigen ID mit (mindestens) zwei Tags versehen:
CACHING_GROUP_ARTICLE
und CACHING_GROUP_ARTICLE_$kArtikel
.
Falls sich nun Artikeldaten für das Produkt mit $kArtikel
"12345" geändert haben, wird der
Cache-Tag CACHING_GROUP_ARTICLE_12345
geleert - alle anderen Daten bleiben im Cache erhalten.
Genau dies geschieht automatisch, beispielsweise in dbeS, wenn dort Produktdaten ankommen. Das Verfahren mit Kategorien ist analog.
Ähnlich ist es beim Speichern von Optionen im Backend:
Sobald der Nutzer dort auf "Speichern" klickt, werden alle mit dem Cache-Tag CACHING_GROUP_OPTION
versehenen
Einträge gelöscht. Das Speichern von Plugin-Optionen invalidiert automatisch die
Gruppe CACHING_GROUP_PLUGIN_$kPlugin
.
Ein weiterer Vorteil der Tags ist die Möglichkeit, dass der Nutzer einzelne Bereiche von JTL-Shop gezielt vom Caching ausnehmen kann. Über das Backend sind daher alle Standard-Tags jeweils einzeln deaktivierbar, sodass Schreibversuche in diesen Gruppen nicht mehr möglich sind und Leseoperationen stets FALSE zurückgeben.
Generelles Vorgehen beim Lesen/Speichern
1. Klasseninstanz holen, z.B. via `Shop::Container()->getCache()`
2. CacheID generieren
3. mit `mixed|bool JTLCache::get(string $cacheID [,callable $callback = null, mixed $customData = null])`
im Cache nach entsprechendem Eintrag suchen
4. bei *Hit* direkt zurückgeben
5. bei *Miss* Daten berechnen
6. Daten im Cache über
`bool JTLCache::set(string $cacheID, mixed $content [, array $tags = null, int $expiration = null])` speichern
und dabei mit Tags versehen
Beispiel:
<?php
class testClass
{
private $cache = null;
private $myCacheTag = 'myOwnTag';
public function __construct () {
$this->cache = Shop::Container()->getCache();
}
public function test () {
$cacheID = 'tct_' . Shop::getLanguageID();
if (($myObject = $this->cache->get($cacheID)) === false) {
//not found in cache
$myObject = $this->doSomethingThatTakesSomeTime();
$this->cache->set($cacheID, $myObject, [CACHING_GROUP_OPTION, $this->myCacheTag]);
}
return $myObject;
}
}
Über den vierten Parameter der set()
-Funktion kann außerdem eine eigene Cache-Gültigkeit in Sekunden
gesetzt werden. Standardmäßig wird der im Backend konfigurierte Wert genommen.
Generelles Invalidieren
Important
Falls sich betroffene Daten ändern, z. B. beim Abgleich mit JTL-Wawi oder durch Nutzerinteraktion, müssen die Caches (repräsentiert durch die CacheID) gelöscht werden.
Hierzu kann via $cache->flush($cacheID)
die ID gelöscht werden oder via $cache->flushTags(array $tags)
ganze Tags gelöscht werden.
Beispiel:
<?php
class testClass
{
// [...]
/**
* return int - the number of deleted IDs
*/
public function invalidate () {
return $this->cache->flushTags([$this->myCacheTag]);
}
}
Generierung von IDs
Cache-IDs sollten möglichst einzigartig sein, gleichzeitig aber auch in ihrer Berechnung nicht zu komplex, um den Geschwindigkeitsvorteil des Caches nicht wieder zu verspielen.
Generell sollten alle Faktoren, die die Berechnung eines Wertes beeinflussen, in die ID mit einbezogen werden.
Dies betrifft bei JTL-Shop häufig die aktuelle Sprache ($_SESSION['kSprache']
bzw. Shop::getLanguageID()
), die
Kundengruppe (JTL\Session\Frontend::getCustomer()->getID()
)
oder die Währung (JTL\Session\Frontend::getCurrency()->getID()
).
Die Funktion JTLCache::getBaseID()
versucht, die gängigsten Einflussfaktoren zu bedenken und so eine Basis-ID
zu generieren, die als Teil der CacheID verwendet werden kann.
Ihre Signatur sieht wie folgt aus:
string JTLCache::getBaseID([bool $hash = false, bool $customerID = false, bool $customerGroup = true, bool $currencyID = true, bool $sslStatus = true])
Der erste Parameter gibt dabei an, ob ein md5-Hash generiert werden soll. Die weiteren Parameter geben an, welche Faktoren bedachte werden sollen.
Zweckmäßig wäre es beispielsweise, diese Basis-ID mit einer Abkürzung des Funktionsnamens zu kombinieren,
wie beispielsweise $cacheID = 'mft_' . Shop::Container()->getCache()->getBaseID()
, wenn die entsprechende Zeile
in einer Funktion namens "myFunctionTest" steht.
CacheIDs und Tags in Plugins
Die in Hook-Dateien verwendbaren $oPlugin
-Objekte und die im Bootstrapper injectete Plugininstanz haben die
automatisch generierten Attribute pluginCacheID
sowie pluginCacheGroup
.
Diese können verwendet werden, um nicht selbständig IDs berechnen zu müssen.
Außerdem werden diese beim Speichern von Optionen im Plugin-Backend automatisch invalidiert.
Boolsche Werte im Cache
Falls auch boolsche Werte im Cache gespeichert werden sollen, ist eine Prüfung des get-Ergebnisses
gegen JTLCache::RES_SUCCESS
mithilfe der Funktion JTLCache::getResultCode()
notwendig, da JTLCache::get()
im Fehlerfall FALSE zurückgibt. So ist es nicht möglich, einen explizit gespeicherten boolschen Wert von einem
fehlgeschlagenen Lesevorgang zu unterscheiden.
Beispiel:
$result = Shop::Container()->getCache()->get($cacheID);
if (Shop::Container()->getCache()->getResultCode() === JTLCache::RES_SUCCESS) {
//ok
} else {
//Cache miss - JTLCache::RES_FAIL
}
Mehrere Werte setzen/lesen
Über JTLCache::getMulti(array $cacheIDs)
können mehrere Werte gleichzeitig ausgelesen
und über JTLCache::setMulti(array $keyValue, array|null $tags[, int|null $expiration])
gesetzt werden.
Beispiel:
$foo = [
'key1' => 'value1',
'key2' => 222
];
$write = $cache->setMulti($foo, ['tag1', 'tag2'], 60);
Shop::dbg($write);
// output: TRUE
// request 3 keys while just 2 are set
$keys = ['key1', 'key2', 'key3'];
$read = $cache->getMulti($keys);
Shop::dbg($read);
// output:
//
// array(3) {
// [" key1 "] => string(6) "value1"
// [" key2 "] => int (222)
// [" key3 "] => bool(false)
// }
Hooking
Caching hat auch den Vorteil, dass gewisse Hooks nicht häufiger ausgeführt werden müssen als nötig - wie z. B.
Hook HOOK_ARTIKEL_CLASS_FUELLEARTIKEL
(110).
Um Plugins die Möglichkeit zu geben, auch eigene Cache-Tags
hinzufügen zu lassen, ist es angebracht, die vorgesehenen Tags ebenfalls an den Hook zu übergeben.
Beispiel:
$cacheTags = [CACHING_GROUP_ARTICLE . '_' . $this->kArtikel, CACHING_GROUP_ARTICLE];
executeHook(HOOK_ARTIKEL_CLASS_FUELLEARTIKEL, [
'oArtikel' => &$this,
'cacheTags' => &$cacheTags,
'cached' => false
]
);
$cache->set($key, $this, $cacheTags);
Aufgrund vielfacher Wünsche von Entwicklern wird der Hook 110 nun bei einem Cache-Hit ausgeführt.
Der übergebene Parameter cached
ist in diesem Fall auf TRUE gesetzt. Falls Sie ein Plugin programmieren, welches
einmalig Eigenschaften eines Artikels modifiziert, achten Sie bitte darauf, komplexe Logik nur auszuführen,
wenn der Parameter FALSE ist.
Anschließend werden Ihre Änderungen automatisch im Cache gespeichert und müssen nicht erneut
durchgeführt werden.
Auf diese Weise kann ein Plugin einen eigenen Tag hinzufügen und beispielsweise bei Änderungen an den Plugin-Optionen reagieren und die betroffenen Caches leeren (vgl. jtl_example_plugin).
Beachten Sie dabei die Reigenfolge:
1. Standard-Cache-Tags definieren
2. Hook mit Daten und Tags ausführen
3. Daten speichern.
Nur so können die durch ein Plugin evtl. modifizierten Daten auch im Cache gespeichert und von diesem invalidiert werden.
Welche Caching-Methode?
Generell sind alle implementierten Caching-Methoden funktional, aufgrund ihrer Eigenheiten aber nur bedingt für alle Szenarien zu empfehlen.
Dateien-Cache
Der Dateien-Cache ist im Falle von vielen Dateien die langsamste und unflexibelste Cache-Methode, hat außerdem Probleme bei gleichzeitigen Zugriffen und sollte daher nur im Notfall genutzt werden. Allerdings ist er immer verfügbar und kann durch Auslagerung des Cache-Ordners auf ein RAM-basiertes Dateisystem deutlich beschleunigt werden.
Dateien(erweitert)-Cache
Die Methode Dateien (erweitert) versucht, diese Nachteile durch
Symlinks zu umgehen.
Hierbei werden im Ordner templates_c/filecache/
für jeden Tag Unterordner angelegt, die Symlinks zu den
einzelnen Cache-Einträgen enthalten. Hierdurch kann eine bessere Parallelität beim Schreiben von neuen Einträgen
erreicht werden.
APC-Cache
APC ist die schnellste Variante, hat im Praxistest bei hoher Belastung und vielen Einträgen aber Skalierungsprobleme. Zumindest im Bereich von ca. 3-4 GB Daten wird er außerdem stark fragmentiert und die Leistung kann einbrechen.
Redis-Cache
Die für große Datenmengen am besten geeignete Variante ist Redis. Auch im Bereich von mehreren Gigabyte arbeitet sie schnell und kann außerdem auch als Session-Handler genutzt werden.
Memcache(d)-Cache
Für memcache(d) gilt prinzipiell dasselbe wie für Redis, allerdings ist es weniger getestet.