Expire mit Fibonacci

Gewöhnlicherweise legt man für Daten welche sich nicht so oft ändern einen Expire-Wert in Sekunden fest, wann diese erneut geladen oder ausgeführt werden. Ist dieser Expire-Wert noch nicht abgelaufen, werden die Daten auf einer Cache geladen um höhere Performance zu erreichen oder auch Traffic zu sparen.

Ich habe mir dazu Gedanken gemacht die Expiration von Daten zu optimieren, da es auch oft vorkommt, dass nach Ablauf des Expire-Wertes immer und immer wieder die selben Daten geladen werden.

Der Gedankengang sieht folgendermaßen aus. Wenn unbekannt ist wie oft Daten sich ändern und diese eine gewisse Zeit gültig sein durften, dann dürfen diese auch eine gewisse Zeit ohne Prüfung gültig bleiben. Und umso länger diese gültig waren desto länger werden sie wohl auch gleich bleiben. Um dieses Problem zu lösen kam mir der Goldene Schnitt in den Sinn. Damit kann die Expiration dynamisch auf die Dauer der nicht Veränderung angepasst werden. Natürlich ist mir klar, dass dies auch in manchen fällen nicht optimal ist und man womöglich dennoch ein Cap an einem großzügigen Expire-Wert nach Zeit festgelegt werden soll oder durch einen gewissen Flag diese Werte zurückgesetzt werden müssen.


Im folgenden Abschnitt möchte ich dazu ein Code-Beispiel anhand eines Callback's zeigen, welcher nach diesen Schema berechnet oder aus der Cache geladen wird.

Wir haben also eine Funktion bzw. einen Callback welcher ein gewisses Resultat liefert und möchten, dass anhand meines gedankenganges eine angemessene Expiration mithilfe von Fibonacci bestimmt und gecached wird.

Dabei wird die eigentliche Fibonacci-Expire-Funktion um die Klasse Storage erweitert, welche in einer Produktiven verwendung durch eine geeignete umsetzung ersetzt werden müsste.

Ebenso hinzu kommt ein kleiner Beispiel-Callback welcher sein Resultat alle 7 Ausführungen ändert, sowie eine for-Schleife um das Beispiel zu testen.


Der Callback wird ausgeführt und in die Cache geschrieben. Bei jedem Aufruf iteriert ein Zähler. Trifft dieser eine Zahl der Fibonacci Reihe, so wird der callback erneut ausgeführt und mit der gecached'ten variante verglichen. Sollte eine Änderung stattfinden, so wird der Zähler zurückgesetzt.

Ganz offentsichtlich fällt dem ein oder anderen auf, dass diese Funktion viel effizienter geschrieben werden kann. Die Info und die Daten der Cache sollten getrennt geladen werden. Außerdem könnte, wenn das ergebnis sich lange nicht ändert, schnell ein Buffer-overflow erreicht werden. Auch dafür gibt es eine Lösung, was hier jedoch nicht Thema ist.

/** * Simple Storage for this example. In your solution this should be a cache from file and cacheinfo from database or Redis (depends on your data) */ class Storage { private $data = []; public function get($key) { return (isset($this->data[$key]) ? (object) $this->data[$key] : false); } public function set($key, $value) { $this->data[$key] = $value; return true; } } $storage = new Storage(); /** * Phixpire - Returns a cached or uncached result of a callback, dependent on fibonacci * @param callable $callback_name - Callback which is executed * @return Result of the callback */ function Phixpire($callback_name) { //initial fibonacci $last = 0; $current = 1; $iterator = 0; //You should make sth. to unique indentify your callback $key = $callback_name; //You should have a storage to load something from a cache global $storage; //In your solution the cacheinfo and cache result data should be splitted, depending on performance reasons //Load data from Cache if available, if not make the initial calculation if($cachedata = $storage->get($key)) { $last = $cachedata->last; $current = $cachedata->current; $iterator = $cachedata->iterator; $result = unserialize($cachedata->result); //For our output echo "$iterator: Cache"; } else { $result = call_user_func($callback_name); //For our output echo "$iterator: Initial"; } //For our output echo " - last=$last, current=$current ### "; //Here the phibonacci begins $next = $last + $current; //If our iterator is equal the next number from fib. we recalculate the data. if($next == $iterator) { $newresult = call_user_func($callback_name); //For our output echo "Recalculate ### "; var_dump($result);var_dump($newresult); //And if no changes are happen. The fib. line grows. If not. We restart the fib. if($result === $newresult) { $last = $current; $current = $next; echo "+++ EQUAL"; } else { $last = 0; $current = 1; $iterator = 0; $result = $newresult; echo "+++ DIFFERENT"; echo "
"; } } //For our output echo "
"; //Save to Storage/Cache $storage->set($key, ['last' => $last, 'current' => $current, 'iterator' => $iterator+1, 'result' => serialize($result)]); return $result; } //Simple callback function. Iterate all 7 calls function cb() { if(!isset($GLOBALS['counter'], $GLOBALS['modulo'])) { $GLOBALS['counter'] = 0;$GLOBALS['modulo'] = 0; } $GLOBALS['counter']++; ($GLOBALS['counter'] % 7 ? : $GLOBALS['modulo']++); return $GLOBALS['modulo']; } //Call for Testing for ($i=0; $i < 35; $i++) { Phixpire('cb'); }

Der Expire-Wert steigt also ganz nach dem goldenen Schnitt der häufigkeit der Änderung an, und beginnt dann von neuem, sollte eine Änderung festgestellt werden. Das Diagramm, welche die Ausgabe unseres Code-Beispiels darstellt, verdeutlicht dies.

Ich finde den Gedankengang gut. Intelligenter Code sollte keine Konstanten beinhalten. Jedoch zeigt dieses Beispiel nur eine Hinführung zu einer Optimalen Lösung. Denn mir gefällt das Verhalten bei sehr häufigen oder sehr seltenen Änderungen nicht. Weshalb meine Meinung ist, es muss eine Hybride Methode geben. Welche anhand der Ausführungsänderungen entscheidet was getan wird. Wenn bei mehr als der häfte der Callback-Ausführung eine Änderung auftrat, wird ein durch Phixpire Analysierter Wert ExpireByTime oder ExpireByCalls verwendet, andernfalls darf die dynamische Phixpire Variante verwendet werden.