Caching

Η κρυφή μνήμη επιταχύνει την εφαρμογή σας αποθηκεύοντας δεδομένα – αφού ανακτηθούν με δυσκολία – για μελλοντική χρήση. Θα σας δείξουμε:

  • Πώς να χρησιμοποιήσετε την κρυφή μνήμη
  • Πώς να αλλάξετε την αποθήκευση της κρυφής μνήμης
  • Πώς να ακυρώνετε σωστά την κρυφή μνήμη

Η χρήση της κρυφής μνήμης είναι πολύ εύκολη στη Nette, ενώ καλύπτει και πολύ προχωρημένες ανάγκες κρυφής μνήμης. Είναι σχεδιασμένη για απόδοση και 100% ανθεκτικότητα. Βασικά, θα βρείτε προσαρμογείς για τους πιο συνηθισμένους backend αποθηκευτικούς χώρους. Επιτρέπει την ακύρωση με βάση τις ετικέτες, την προστασία από το stampede της κρυφής μνήμης, τη χρονική λήξη κ.λπ.

Εγκατάσταση

Κατεβάστε και εγκαταστήστε το πακέτο χρησιμοποιώντας το Composer:

composer require nette/caching

Βασική χρήση

Το κέντρο της εργασίας με την κρυφή μνήμη είναι το αντικείμενο Nette\Caching\Cache. Δημιουργούμε την εμφάνισή του και παραδίδουμε στον κατασκευαστή ως παράμετρο τη λεγόμενη αποθήκευση. Το οποίο είναι ένα αντικείμενο που αντιπροσωπεύει το μέρος όπου θα αποθηκευτούν φυσικά τα δεδομένα (βάση δεδομένων, Memcached, αρχεία στο δίσκο, …). Παίρνετε το αντικείμενο storage περνώντας το χρησιμοποιώντας dependency injection με τύπο Nette\Caching\Storage. Θα μάθετε όλα τα βασικά στοιχεία στην ενότητα Storage (Αποθήκευση).

Στην έκδοση 3.0, η διεπαφή είχε ακόμα το I prefix, so the name was Nette\Caching\IStorage. Επίσης, οι σταθερές της κλάσης Cache γράφονταν με κεφαλαίο, έτσι για παράδειγμα Cache::EXPIRE αντί για Cache::Expire.

Για τα παρακάτω παραδείγματα, ας υποθέσουμε ότι έχουμε ένα ψευδώνυμο Cache και μια αποθήκευση στη μεταβλητή $storage.

use Nette\Caching\Cache;

$storage = /* ... */; // instance of Nette\Caching\Storage

Η κρυφή μνήμη είναι στην πραγματικότητα ένας αποθηκευτής κλειδιών-τιμών, οπότε διαβάζουμε και γράφουμε δεδομένα κάτω από κλειδιά ακριβώς όπως οι συσχετιστικοί πίνακες. Οι εφαρμογές αποτελούνται από έναν αριθμό ανεξάρτητων τμημάτων, και αν όλα χρησιμοποιούσαν έναν αποθηκευτικό χώρο (για ιδέα: έναν κατάλογο σε ένα δίσκο), αργά ή γρήγορα θα υπήρχε σύγκρουση κλειδιών. Το Nette Framework λύνει το πρόβλημα χωρίζοντας ολόκληρο το χώρο σε χώρους ονομάτων (υποκαταλόγους). Κάθε μέρος του προγράμματος χρησιμοποιεί τότε τον δικό του χώρο με ένα μοναδικό όνομα και δεν μπορούν να προκύψουν συγκρούσεις.

Το όνομα του χώρου καθορίζεται ως δεύτερη παράμετρος του κατασκευαστή της κλάσης Cache:

$cache = new Cache($storage, 'Full Html Pages');

Μπορούμε τώρα να χρησιμοποιήσουμε το αντικείμενο $cache για να διαβάσουμε και να γράψουμε από την κρυφή μνήμη. Η μέθοδος load() χρησιμοποιείται και για τα δύο. Το πρώτο όρισμα είναι το κλειδί και το δεύτερο είναι το callback της PHP, το οποίο καλείται όταν το κλειδί δεν βρεθεί στην κρυφή μνήμη. Το callback παράγει μια τιμή, την επιστρέφει και την αποθηκεύει στην προσωρινή μνήμη:

$value = $cache->load($key, function () use ($key) {
	$computedValue = /* ... */; // βαριούς υπολογισμούς
	return $computedValue;
});

Εάν το δεύτερο όρισμα δεν έχει καθοριστεί $value = $cache->load($key), επιστρέφεται το null εάν το στοιχείο δεν υπάρχει στην κρυφή μνήμη.

Το σπουδαίο είναι ότι μπορούν να αποθηκευτούν στην προσωρινή μνήμη οποιεσδήποτε σειριοποιήσιμες δομές, όχι μόνο συμβολοσειρές. Και το ίδιο ισχύει και για τα κλειδιά.

Το στοιχείο διαγράφεται από την κρυφή μνήμη χρησιμοποιώντας τη μέθοδο remove():

$cache->remove($key);

Μπορείτε επίσης να αποθηκεύσετε ένα στοιχείο στην προσωρινή μνήμη χρησιμοποιώντας τη μέθοδο $cache->save($key, $value, array $dependencies = []). Ωστόσο, προτιμάται η παραπάνω μέθοδος που χρησιμοποιεί το load().

Απομνημόνευση

Η απομνημόνευση σημαίνει την προσωρινή αποθήκευση του αποτελέσματος μιας συνάρτησης ή μεθόδου, ώστε να μπορείτε να το χρησιμοποιήσετε την επόμενη φορά αντί να υπολογίζετε το ίδιο πράγμα ξανά και ξανά.

Οι μέθοδοι και οι συναρτήσεις μπορούν να κληθούν memoized χρησιμοποιώντας το call(callable $callback, ...$args):

$result = $cache->call('gethostbyaddr', $ip);

Η συνάρτηση gethostbyaddr() καλείται μόνο μία φορά για κάθε παράμετρο $ip και την επόμενη φορά θα επιστραφεί η τιμή από την κρυφή μνήμη.

Είναι επίσης δυνατό να δημιουργηθεί ένα μνημονικό περιτύλιγμα για μια μέθοδο ή συνάρτηση που μπορεί να κληθεί αργότερα:

function factorial($num)
{
	return /* ... */;
}

$memoizedFactorial = $cache->wrap('factorial');

$result = $memoizedFactorial(5); // το μετράει
$result = $memoizedFactorial(5); // το επιστρέφει από την κρυφή μνήμη

Λήξη & Ακύρωση

Με την προσωρινή αποθήκευση, είναι απαραίτητο να αντιμετωπιστεί το ζήτημα ότι ορισμένα από τα δεδομένα που έχουν αποθηκευτεί προηγουμένως θα καταστούν άκυρα με την πάροδο του χρόνου. Το Nette Framework παρέχει έναν μηχανισμό, με τον οποίο μπορείτε να περιορίσετε την εγκυρότητα των δεδομένων και να τα διαγράψετε με ελεγχόμενο τρόπο (“να τα ακυρώσετε”, χρησιμοποιώντας την ορολογία του πλαισίου).

Η εγκυρότητα των δεδομένων ορίζεται κατά τη στιγμή της αποθήκευσης με τη χρήση της τρίτης παραμέτρου της μεθόδου save(), π.χ:

$cache->save($key, $value, [
	$cache::Expire => '20 minutes',
]);

Ή με τη χρήση της παραμέτρου $dependencies που περνά μέσω αναφοράς στην επανάκληση της μεθόδου load(), π.χ:

$value = $cache->load($key, function (&$dependencies) {
	$dependencies[Cache::Expire] = '20 minutes';
	return /* ... */;
});

Ή χρησιμοποιώντας την 3η παράμετρο στη μέθοδο load(), π.χ:

$value = $cache->load($key, function () {
	return ...;
}, [Cache::Expire => '20 minutes']);

Στα παραδείγματα που ακολουθούν, θα υποθέσουμε τη δεύτερη παραλλαγή και συνεπώς την ύπαρξη μιας μεταβλητής $dependencies.

Λήξη

Η απλούστερη λήξη είναι το χρονικό όριο. Εδώ είναι ο τρόπος για να αποθηκεύσετε δεδομένα που ισχύουν για 20 λεπτά:

// δέχεται επίσης τον αριθμό των δευτερολέπτων ή τη χρονοσφραγίδα UNIX
$dependencies[Cache::Expire] = '20 minutes';

Αν θέλουμε να επεκτείνουμε την περίοδο ισχύος με κάθε ανάγνωση, αυτό μπορεί να επιτευχθεί με αυτόν τον τρόπο, αλλά προσέξτε, αυτό θα αυξήσει την επιβάρυνση της κρυφής μνήμης:

$dependencies[Cache::Sliding] = true;

Η εύχρηστη επιλογή είναι η δυνατότητα να αφήνουμε τα δεδομένα να λήγουν όταν αλλάζει ένα συγκεκριμένο αρχείο ή ένα από πολλά αρχεία. Αυτό μπορεί να χρησιμοποιηθεί, για παράδειγμα, για την προσωρινή αποθήκευση δεδομένων που προκύπτουν από την επεξεργασία αυτών των αρχείων. Χρήση απόλυτων διαδρομών.

$dependencies[Cache::Files] = '/path/to/data.yaml';
// ή
$dependencies[Cache::Files] = ['/path/to/data1.yaml', '/path/to/data2.yaml'];

Μπορούμε να αφήσουμε ένα στοιχείο της κρυφής μνήμης να λήξει όταν λήξει ένα άλλο στοιχείο (ή ένα από πολλά άλλα). Αυτό μπορεί να χρησιμοποιηθεί όταν αποθηκεύουμε στην κρυφή μνήμη ολόκληρη τη σελίδα HTML και τμήματα αυτής κάτω από άλλα κλειδιά. Μόλις αλλάξει το απόσπασμα, ολόκληρη η σελίδα καθίσταται άκυρη. Αν έχουμε αποθηκευμένα αποσπάσματα κάτω από κλειδιά όπως frag1 και frag2, θα χρησιμοποιήσουμε:

$dependencies[Cache::Items] = ['frag1', 'frag2'];

Η λήξη μπορεί επίσης να ελεγχθεί με τη χρήση προσαρμοσμένων συναρτήσεων ή στατικών μεθόδων, οι οποίες αποφασίζουν πάντα κατά την ανάγνωση αν το στοιχείο είναι ακόμα έγκυρο. Για παράδειγμα, μπορούμε να αφήσουμε το στοιχείο να λήξει κάθε φορά που αλλάζει η έκδοση της PHP. Θα δημιουργήσουμε μια συνάρτηση που θα συγκρίνει την τρέχουσα έκδοση με την παράμετρο και κατά την αποθήκευση θα προσθέσουμε έναν πίνακα της μορφής [function name, ...arguments] στις εξαρτήσεις:

function checkPhpVersion($ver): bool
{
	return $ver === PHP_VERSION_ID;
}

$dependencies[Cache::Callbacks] = [
	['checkPhpVersion', PHP_VERSION_ID] // λήγει όταν checkPhpVersion(...) === false
];

Φυσικά, όλα τα κριτήρια μπορούν να συνδυαστούν. Η κρυφή μνήμη τότε λήγει όταν δεν ικανοποιείται τουλάχιστον ένα κριτήριο.

$dependencies[Cache::Expire] = '20 minutes';
$dependencies[Cache::Files] = '/path/to/data.yaml';

Ακύρωση με χρήση ετικετών

Οι ετικέτες είναι ένα πολύ χρήσιμο εργαλείο ακύρωσης. Μπορούμε να αντιστοιχίσουμε μια λίστα ετικετών, οι οποίες είναι αυθαίρετες συμβολοσειρές, σε κάθε στοιχείο που είναι αποθηκευμένο στην κρυφή μνήμη. Για παράδειγμα, ας υποθέσουμε ότι έχουμε μια σελίδα HTML με ένα άρθρο και σχόλια, την οποία θέλουμε να αποθηκεύσουμε στην κρυφή μνήμη. Οπότε καθορίζουμε ετικέτες κατά την αποθήκευση στην κρυφή μνήμη:

$dependencies[Cache::Tags] = ["article/$articleId", "comments/$articleId"];

Τώρα, ας προχωρήσουμε στη διαχείριση. Εδώ έχουμε μια φόρμα για την επεξεργασία άρθρων. Μαζί με την αποθήκευση του άρθρου σε μια βάση δεδομένων, καλούμε την εντολή clean(), η οποία θα διαγράψει τα αποθηκευμένα στοιχεία ανά ετικέτα:

$cache->clean([
	$cache::Tags => ["article/$articleId"],
]);

Ομοίως, στη θέση της προσθήκης ενός νέου σχολίου (ή της επεξεργασίας ενός σχολίου), δεν θα ξεχάσουμε να ακυρώσουμε τη σχετική ετικέτα:

$cache->clean([
	$cache::Tags => ["comments/$articleId"],
]);

Τι πετύχαμε; Ότι η κρυφή μνήμη HTML μας θα ακυρώνεται (διαγράφεται) κάθε φορά που το άρθρο ή τα σχόλια αλλάζουν. Κατά την επεξεργασία ενός άρθρου με ID = 10, η ετικέτα article/10 αναγκάζεται να ακυρωθεί και η σελίδα HTML που φέρει την ετικέτα διαγράφεται από την κρυφή μνήμη. Το ίδιο συμβαίνει όταν εισάγετε ένα νέο σχόλιο κάτω από το σχετικό άρθρο.

Οι ετικέτες απαιτούν Journal.

Ακύρωση κατά προτεραιότητα

Μπορούμε να ορίσουμε την προτεραιότητα για μεμονωμένα στοιχεία στην κρυφή μνήμη, και θα είναι δυνατή η διαγραφή τους με ελεγχόμενο τρόπο όταν, για παράδειγμα, η κρυφή μνήμη υπερβαίνει ένα συγκεκριμένο μέγεθος:

$dependencies[Cache::Priority] = 50;

Διαγραφή όλων των στοιχείων με προτεραιότητα ίση ή μικρότερη από 100:

$cache->clean([
	$cache::Priority => 100,
]);

Οι προτεραιότητες απαιτούν το λεγόμενο Journal.

Εκκαθάριση προσωρινής μνήμης

Η παράμετρος Cache::All καθαρίζει τα πάντα:

$cache->clean([
	$cache::All => true,
]);

Bulk Reading

Για μαζική ανάγνωση και εγγραφή στην κρυφή μνήμη χρησιμοποιείται η μέθοδος bulkLoad(), όπου περνάμε έναν πίνακα κλειδιών και λαμβάνουμε έναν πίνακα τιμών:

$values = $cache->bulkLoad($keys);

Η μέθοδος bulkLoad() λειτουργεί παρόμοια με τη μέθοδο load() με τη δεύτερη παράμετρο επανάκλησης, στην οποία περνάει το κλειδί του παραγόμενου στοιχείου:

$values = $cache->bulkLoad($keys, function ($key, &$dependencies) {
	$computedValue = /* ... */; // βαριοί υπολογισμοί
	return $computedValue;
});

Προσωρινή αποθήκευση εξόδου

Η έξοδος μπορεί να συλληφθεί και να αποθηκευτεί στην προσωρινή μνήμη πολύ κομψά:

if ($capture = $cache->capture($key)) {

	echo ... // εκτύπωση ορισμένων δεδομένων

	$capture->end(); // αποθήκευση της εξόδου στην κρυφή μνήμη
}

Σε περίπτωση που η έξοδος υπάρχει ήδη στην κρυφή μνήμη, η μέθοδος capture() την εκτυπώνει και επιστρέφει null, οπότε η συνθήκη δεν θα εκτελεστεί. Διαφορετικά, αρχίζει να αποθηκεύει την έξοδο και επιστρέφει το αντικείμενο $capture με το οποίο τελικά αποθηκεύουμε τα δεδομένα στην κρυφή μνήμη.

Στην έκδοση 3.0 η μέθοδος ονομαζόταν $cache->start().

Προσωρινή αποθήκευση στο Latte

Η προσωρινή αποθήκευση σε πρότυπα Latte είναι πολύ εύκολη, απλά τυλίξτε μέρος του προτύπου με ετικέτες {cache}...{/cache}. Η προσωρινή αποθήκευση ακυρώνεται αυτόματα όταν αλλάζει το πηγαίο πρότυπο (συμπεριλαμβανομένων τυχόν περιεχόμενων προτύπων μέσα στις ετικέτες {cache} ). Οι ετικέτες {cache} μπορούν να είναι φωλιασμένες, και όταν ένα φωλιασμένο μπλοκ ακυρώνεται (για παράδειγμα, από μια ετικέτα), το γονικό μπλοκ ακυρώνεται επίσης.

Στην ετικέτα είναι δυνατόν να καθοριστούν τα κλειδιά στα οποία θα δεσμεύεται η κρυφή μνήμη (εδώ η μεταβλητή $id) και να οριστούν οι ετικέτες λήξης και ακύρωσης

{cache $id, expire: '20 minutes', tags: [tag1, tag2]}
	...
{/cache}

Όλες οι παράμετροι είναι προαιρετικές, οπότε δεν χρειάζεται να καθορίσετε τη λήξη, τις ετικέτες ή τα κλειδιά.

Η χρήση της κρυφής μνήμης μπορεί επίσης να εξαρτάται από τη διεύθυνση if – το περιεχόμενο θα αποθηκευτεί στην κρυφή μνήμη μόνο εάν πληρούται η συνθήκη:

{cache $id, if: !$form->isSubmitted()}
	{$form}
{/cache}

Αποθήκες

Ένας αποθηκευτικός χώρος είναι ένα αντικείμενο που αντιπροσωπεύει τον τόπο όπου αποθηκεύονται φυσικά τα δεδομένα. Μπορούμε να χρησιμοποιήσουμε μια βάση δεδομένων, έναν διακομιστή Memcached ή τον πιο διαθέσιμο αποθηκευτικό χώρο, που είναι τα αρχεία στο δίσκο.

Αποθήκευση Περιγραφή
FileStorage προεπιλεγμένη αποθήκευση με αποθήκευση σε αρχεία στο δίσκο
MemcachedStorage χρησιμοποιεί τον διακομιστή Memcached
MemoryStorage τα δεδομένα βρίσκονται προσωρινά στη μνήμη
SQLiteStorage τα δεδομένα αποθηκεύονται σε βάση δεδομένων SQLite
DevNullStorage τα δεδομένα δεν αποθηκεύονται – για δοκιμαστικούς σκοπούς

Λαμβάνετε το αντικείμενο αποθήκευσης περνώντας το χρησιμοποιώντας dependency injection με τον τύπο Nette\Caching\Storage. Από προεπιλογή, η Nette παρέχει ένα αντικείμενο FileStorage που αποθηκεύει δεδομένα σε έναν υποφάκελο cache στον κατάλογο για προσωρινά αρχεία.

Μπορείτε να αλλάξετε την αποθήκευση στη διαμόρφωση:

services:
	cache.storage: Nette\Caching\Storages\DevNullStorage

FileStorage

Γράφει την κρυφή μνήμη σε αρχεία στο δίσκο. Η αποθήκευση Nette\Caching\Storages\FileStorage είναι πολύ καλά βελτιστοποιημένη για την απόδοση και πάνω απ' όλα εξασφαλίζει πλήρη ατομικότητα των λειτουργιών. Τι σημαίνει αυτό; Ότι κατά τη χρήση της κρυφής μνήμης δεν μπορεί να συμβεί να διαβάσουμε ένα αρχείο που δεν έχει ακόμα γραφτεί πλήρως από κάποιο άλλο νήμα ή να το διαγράψει κάποιος “κάτω από τα χέρια σας”. Η χρήση της κρυφής μνήμης είναι επομένως απολύτως ασφαλής.

Αυτή η αποθήκευση έχει επίσης ένα σημαντικό ενσωματωμένο χαρακτηριστικό που αποτρέπει την ακραία αύξηση της χρήσης της CPU όταν η κρυφή μνήμη διαγράφεται ή ψύχεται (δηλαδή δεν δημιουργείται). Αυτή είναι η πρόληψη stampede cache. Συμβαίνει ότι σε μια στιγμή υπάρχουν πολλές ταυτόχρονες αιτήσεις που θέλουν το ίδιο πράγμα από την κρυφή μνήμη (π.χ. το αποτέλεσμα ενός ακριβού ερωτήματος SQL) και επειδή αυτό δεν έχει αποθηκευτεί στην κρυφή μνήμη, όλες οι διεργασίες αρχίζουν να εκτελούν το ίδιο ερώτημα SQL. Το φορτίο του επεξεργαστή πολλαπλασιάζεται και μπορεί να συμβεί ακόμη και να μην μπορεί κανένα νήμα να ανταποκριθεί εντός του χρονικού ορίου, να μη δημιουργηθεί η κρυφή μνήμη και να καταρρεύσει η εφαρμογή. Ευτυχώς, η κρυφή μνήμη στο Nette λειτουργεί με τέτοιο τρόπο ώστε όταν υπάρχουν πολλαπλές ταυτόχρονες αιτήσεις για ένα στοιχείο, αυτό δημιουργείται μόνο από το πρώτο νήμα, τα υπόλοιπα περιμένουν και στη συνέχεια χρησιμοποιούν το παραγόμενο αποτέλεσμα.

Παράδειγμα δημιουργίας ενός FileStorage:

// η αποθήκευση θα είναι ο κατάλογος '/path/to/temp' στο δίσκο
$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp');

MemcachedStorage

Ο διακομιστής Memcached είναι ένα κατανεμημένο σύστημα αποθήκευσης υψηλής απόδοσης, του οποίου ο προσαρμογέας είναι Nette\Caching\Storages\MemcachedStorage. Στη διαμόρφωση, καθορίστε τη διεύθυνση IP και τη θύρα, εάν διαφέρουν από το πρότυπο 11211.

Απαιτεί την επέκταση PHP memcached.

services:
	cache.storage: Nette\Caching\Storages\MemcachedStorage('10.0.0.5')

MemoryStorage

Nette\Caching\Storages\MemoryStorage είναι ένας αποθηκευτικός χώρος που αποθηκεύει δεδομένα σε έναν πίνακα PHP και έτσι χάνονται όταν τερματιστεί η αίτηση.

SQLiteStorage

Η βάση δεδομένων SQLite και ο προσαρμογέας Nette\Caching\Storages\SQLiteStorage προσφέρουν έναν τρόπο προσωρινής αποθήκευσης σε ένα μόνο αρχείο στο δίσκο. Η ρύθμιση παραμέτρων θα καθορίσει τη διαδρομή προς αυτό το αρχείο.

Απαιτεί τις επεκτάσεις PHP pdo και pdo_sqlite.

services:
	cache.storage: Nette\Caching\Storages\SQLiteStorage('%tempDir%/cache.db')

DevNullStorage

Μια ειδική υλοποίηση της αποθήκευσης είναι η Nette\Caching\Storages\DevNullStorage, η οποία στην πραγματικότητα δεν αποθηκεύει καθόλου δεδομένα. Επομένως, είναι κατάλληλη για δοκιμές αν θέλουμε να εξαλείψουμε την επίδραση της κρυφής μνήμης.

Χρήση της κρυφής μνήμης στον κώδικα

Όταν χρησιμοποιείτε προσωρινή αποθήκευση στον κώδικα, έχετε δύο τρόπους για να το κάνετε. Ο πρώτος είναι ότι παίρνετε το αντικείμενο αποθήκευσης περνώντας το με τη χρήση dependency injection και στη συνέχεια δημιουργείτε ένα αντικείμενο Cache:

use Nette;

class ClassOne
{
	private Nette\Caching\Cache $cache;

	public function __construct(Nette\Caching\Storage $storage)
	{
		$this->cache = new Nette\Caching\Cache($storage, 'my-namespace');
	}
}

Ο δεύτερος τρόπος είναι ότι παίρνετε το αντικείμενο αποθήκευσης Cache:

class ClassTwo
{
	private Nette\Caching\Cache $cache;

	public function __construct(Nette\Caching\Cache $cache)
	{
		$this->cache = $cache;
	}
}

Το αντικείμενο Cache δημιουργείται στη συνέχεια απευθείας στη διαμόρφωση ως εξής:

services:
	- ClassTwo( Nette\Caching\Cache(namespace: 'my-namespace') )

Περιοδικό

Η Nette αποθηκεύει ετικέτες και προτεραιότητες σε ένα λεγόμενο ημερολόγιο. Από προεπιλογή, χρησιμοποιείται το SQLite και το αρχείο journal.s3db γι' αυτό, ενώ απαιτούνται οι επεκτάσεις **PHP pdo και pdo_sqlite. **

Μπορείτε να αλλάξετε το ημερολόγιο στη διαμόρφωση:

services:
	cache.journal: MyJournal
έκδοση: 3.x