Nie pytaj co chwila o to samo – przechowywanie zmiennych na trochę dłużej

W pustym mieszkaniu, w którym jest klatka z papugą słychać pukanie do drzwi.
„Kto tam?” – skrzeczy ptak ludzkim głosem.
„Listonosz” – odpowiada osoba za drzwiami.
„Kto tam?” – powtarza papuga.
„Listonosz!” – głośniej odkrzykuje ktoś za drzwiami, może go nie usłyszała.
„Ktoo taaam” – ponowne skrzeczenie.
„Listonosz!!!”
„Ktooo taaaam?”
„Liiiistooonooosz!”
„Ktoooo taaam?”

Wymiana skrzeków i krzyków trwa kilka minut, aż serce listonosza nie wytrzymuje ze zdenerwowania i ten pada trupem u progu.
Po jakimś czasie zjawia się właściciel mieszkania i widząc zwłoki, przerażony pyta sam siebie pod nosem:
„A to kto?”
„Listonosz” – odpowiada zza drzwi spokojnie papuga.

Dowcip ten na pewno nie jest śmieszny według serwerów. A przynajmniej tak by serwery stwierdziły, gdyby stwierdzać cokolwiek potrafiły. Codziennie bowiem są odpytywane o to samo, a co więcej wielu takich pytań można by uniknąć.

O ile serwery w większości wypadków znoszą to tak naprawdę ze stoickim spokojem, bo – co tu dużo mówić – w większości wypadków na nasze blogi wchodzi najwyżej jedna osoba co kilka minut, to zdarzają się sytuacje, w których skracanie czasu przetwarzania zapytań na pewno ma kluczowe znaczenie. Co jeśli naszą stronę będzie odwiedzać kilkaset osób na sekundę? Co jeśli wykupiliśmy współdzielony hosting w firmie, w której jedna maszyna obsługuje setki stron? Takie coś na pewno tylko „czeka” aż napiszemy niewydajny kod i zacznie się przytykać.

Metod na uniknięcie wielokrotnych zapytań jest wiele, a każda ma swoje plusy i minusy. Dziś omówię jedną z nich, rzadko wspominaną, a zarazem niesamowicie prostą w użyciu.

Czy słyszeliście już o transient API w WordPressie?

Jeśli nie, to posłuchajcie.

Zdarzyło mi się tworzyć stronę dla firmy obsługującej szkolenia. Szkolenia mają swoje tematy i mogą odbywać się w różnych terminach. Stworzyłem więc własny typ treści (custom post type) o nazwie „szkolenia” i przy każdym wpisie tego typu dodałem meta pole na wprowadzenie daty, kiedy szkolenie się odbędzie.

Potem się dowiedziałem, że jedno szkolenie może odbywać się w wielu terminach. „Wielu terminach” to mało powiedziane – „szkolenie A” odbywało się około 50 razy w roku, „szkolenie B” podobnie, a szkoleń było kilkadziesiąt. Początkowo zastanawialiśmy się czy nie można by dla każdego terminu szkolenia dodawać nowy wpis i tak w kółko. Nie, to odpada. Nawet gdyby założyć, że wypełnienie opisu szkolenia przez kopiuj&wklej zajmie 10 sekund, całość – przez fakt mnogości terminów i rodzajów szkoleń – zajęłaby tygodnie. Trzeba by było stworzyć tysiące wpisów dla każdej pary „dane szkolenie w danym terminie”.

Zdecydowaliśmy więc zmienić meta pole do podawania daty na pole przedstawiające kalendarz, w którym operator serwisu będzie zaznaczał kolejne terminy. Tak oto zmieniliśmy powyższą relację „dane szkolenie w danym terminie” na wygodniejszą „dane szkolenie w wielu terminach”:

W bazie WordPress nadal zapisywał to jako własne pole „_data”, jednak zmienna taka zawierała już nie jeden termin, a tablicę terminów. Każdy termin zapisany był jako znacznik czasu (timestamp) danego dnia:

// przykładowa zawartość tablicy
$daty = get_post_meta($post->ID, "_data", true);
print_r($data);
/*
zwróci:
Array
(
 [0] => 1317656795
 [1] => 1327656795
 [2] => 1325656795
)
*/

I wszystko działało dobrze. Odwiedzający odwiedzali stronę firmy prowadzącej szkolenia, zaglądali do opisu szkolenia, a tam widzieli ładny terminarz szkoleń. Działało jak należy.

Do momentu, kiedy klient zażyczył sobie w ramach aktualizacji strony, aby pojawiła się na niej nowa tabelka z wszystkimi szkoleniami, ale poukładana według dat. Jakie szkolenia odbywają się dziś, jakie jutro, pojutrze… I tak na cały najbliższy rok.

Zadanie nie wyglądało na błahe: o ile łatwo jest wyciągnąć z WordPressa wszystkie wpisy danego typu, ciężko jest z takiego zapytania wyekstrahować w jakich datach – ukrytych w tablicy – szkolenie się odbywa, a już naprawdę ciężko jest przetasować to tak by poukładane to było po owych datach od najbliższych do najdalszych. Algorytmicznie wyglądało by to w ten sposób:

Zakładamy już, że w zmiennej $szkolenia posiadamy tablicę identyfikatorów wszystkich wpisów ze szkoleniami. W pierwszej kolejności musimy stworzyć większą tablicę, poindeksowaną identyfikatorami szkoleń, a przy każdej pozycji znajdowała by się tablica zagnieżdżona ze znacznikami czasu poszczególnych szkoleń:

$id_daty = array();

foreach ($szkolenia as $id) {
 $daty = get_post_meta($id, "_data", true);
 $id_daty[$id] = $daty;
}

To dopiero pierwszy etap. Teraz musimy tablicę „wywrócić”. Indeksem jest tu ID szkolenia, chcemy jednak by tablica była poindeksowana datami, a do nich dopiane były identyfikatory wszystkich szkoleń danego dnia. Wywrócenie tablicy robimy następująco:

$daty_id = array();

foreach ($id_daty as $id => $daty) {
 foreach ($daty as $data) {
  $daty_id[$data][] = $id;
 }
}

Następnie należy tablicę uporządkować w kolejności znaczników czasu – teraz jest ona bowiem dość losowo posortowana. Wystarczy zwykłe ksort():

ksort($daty_id);

Już mamy porządek. Nasza tablica ma mniej więcej postać:

Array
(
 '1317656996' => Array ('1', '3', '7')
 '1317666762' => Array ('1', '2', '5', '6', '12')
 ...
)

I to by było na tyle. Mamy śliczną tablicę posortowaną datami. Koniec zadania.

…aż do momentu kiedy zauważymy, że nasza strona zaczęła wolniej działać. Stworzenie tablicy z identyfikatorami szkoleń, stworzenie potem tablicy wielowymiarowej z przypisanymi do każdego identyfikatora znacznikami czasu, wywrócenie tego wszystkiego i posortowanie zajmuje trochę czasu. Im więcej mamy szkoleń i w im większej ilość terminów będą się one obdywać, tym dłużej wykonywać się będzie produkcja takiej tabelki.

Tabelki, która umieszczona jest na stronie głównej bardzo popularnego serwisu. Czy WordPress naprawdę musi te operacje wykonywać ponownie dla każdego odwiedzającego stronę, skoro wytwarzana tablica $daty_id zawsze będzie wyglądać tak samo (a przynajmniej do momentu, aż ktoś w Kokpicie zaktualizuje któreś ze szkoleń)?

Nie, nie musi i do tego możemy wykorzystać właśnie transient API.

Transient to przymiotnik w języku angielskim tłumaczony jako przelotny. Coś co chwilę jest, a później znika lub się zmienia. Przez jakiś czas z nami tutaj będzie, ale później już niekoniecznie.

Tak jak nasza tablica z datami i identyfikatorami: pobędzie w naszym WordPressie przynajmniej do czasu kolejnej aktualizacji strony. Pozwólmy jej więc zostać na – powiedzmy – godzinę. Tak by każdy kolejny odwiedzający stronę w ciągu godziny pobierał ją zawsze taką samą, zapisaną, zamiast na nowo wykonywać cały proces jej generowania.

Zmienne takie w WordPressie nazywamy zmiennymi typu przejściowego (ang. transient variable). Zostają na dłużej niż jedno odświeżenie strony, a do ich obsługi służą dwie proste funkcje: tworząca zmienną i odczytująca jej wartość. Zaczniemy od tej drugiej.

Aby odczytać wartość jakiejś zmiennej przejściowej używamy funkcji:

get_transient('nazwa')

Zwraca ona wartość zmiennej w takiej postaci, w jakiej została ona zapamiętana (jeśli była to tablica, dostaniemy tablicę z powrotem). Przyjmuje jako parametr nazwę zmiennej, pod jaką ją zapisaliśmy. Jeśli zmienna nie istnieje otrzymamy wartość logiczną false.

A zapisujemy ją kolejną funkcją:

set_transient('nazwa', 'zawartość', 'czas')

Przy osadzaniu zmiennej podajemy kilka parametrów. Jako 'nazwa’ przekazujemy oczywiście nazwę zmiennej. Następnie podajemy w parametrze 'zawartość’ co ma być w niej zapamiętane, a jako 'czas’ określamy na jak długo (w sekundach).

By zastosować zmienne przejściowe, musimy nasz kod przepisać w następujący sposób: najpierw pobieramy zmienną i sprawdzamy czy jej zawartość nie wynosi false. Jeśli wynosi, oznacza to, że zmienna jeszcze nie istnieje lub wygasła. W takim wypadku tworzymy naszą tablicę od nowa i zapisujemy do zmiennej przejściowej, w celu wykorzystania przy ponownych odwiedzinach:

$daty_id = get_transient('daty_id');

if ('false' === $daty_id) {
 // tutaj caly nasz powyższy kod generujący
 // odpowiednią tablicę w zmiennej $daty_id
 $id_daty = array();

 foreach ($szkolenia as $id) {
  $daty = get_post_meta($id, "_data", true);
  $id_daty[$id] = $daty;
 }

$daty_id = array();

foreach ($id_daty as $id => $daty) {
 foreach ($daty as $data) {
  $daty_id[$data][] = $id;
 }
}

ksort($daty_id);

 // i gdy już mamy zmienną, zapamiętujemy ją
 set_transient('daty_id', $daty_id, 3600);

}

I to wszystko. Teraz, zanim WordPress przejdzie do wykonywania całej operacji tworzenia i sortowania tablic, najpierw sprawdzi czy już nie robił tego w ciągu ostatniej godziny. I jeśli robił, wczyta zapamiętaną tablicę.

Wady Transient API?

Oczywiście istnieją. Przede wszystkim, co zostanie raz zapamiętane, nie zostanie usunięte przez podaną ilość sekund. Utrudnia więc to nieco debugowanie strony i tworzy na niej opóźnienia. Nie stosujemy tego do zapamiętywania na stronie, na której mamy na przykład relacje z meczu piłki nożnej wprowadzane na żywo ;) A przynajmniej nie zapamiętujmy w ten sposób owej relacji.

Co jednak jeśli chcemy koniecznie usunąć zmienną przejściową przed czasem jej wygaśnięcia? Oczywiście jest taka możliwość i służy do tego funkcja:

delete_transient('nazwa')

Można ją sobie ręcznie dopisywać do pliku wtyczki na moment, kiedy chcemy zmienną usunąć. Można ją także podczepić pod akcję. Jeśli chcemy aby nasza zmienna 'daty_id’ była kasowana przy każdej aktualizacji wpisu, wystarczy dopisać następujący kod:

add_action('save_post', 'wyczysc');
function wyczysc($id) {
 delete_transient('daty_id');
}

Mniej istotnym ograniczeniem jest rozmiar tak przechowywanej zmiennej. Nie może ona (po serializacji) zajmować więcej niż… 4 miliardy znaków. OK, mało kto zapewne natknie się na takie ograniczenie, więc nie skupiajmy się na metodach jego obchodzenia ;)

Powyżej opisałem tylko jeden z przykładów użycia transient API. A czy Wam zdarzyło się sięgać po te trzy funkcje? Jeśli nie, może już widzicie potencjalne miejsca w swoim już stworzonym kodzie, w których zda to egzamin? Zapraszam do komentowania.


Opublikowano

w

przez

Komentarze

14 odpowiedzi na „Nie pytaj co chwila o to samo – przechowywanie zmiennych na trochę dłużej”

  1. Awatar Łukasz Więcek

    O proszę – i że ja nie wiedziałem o istnieniu transient API? Dzięki za ten post! Przyda się i to już na dniach :)

    1. Awatar Konrad Karpieszuk

      ależ proszę bardzo :)

  2. Awatar Stanowski

    Nawet nie wiedziałem, że WordPress ma coś takiego ;) A to dlatego że wykorzystałbym raczej Memcache. I tu pytanie, bo może autor wie: czy ten mechanizm działa na plikach i czy jest szybka opcja przerobienia go na memcache?

    1. Awatar Konrad Karpieszuk

      Michał, musisz przetestować bo nie wiem. set_transient pobiera przekazane dane, serializuje je za pomoca serialize() i umieszcza w option_value w wp_options. wg dokumentacji serializowac mozna wszystko oprocz danych typu resource. jak domyslam sie wlasnie przez ersource pewnie bys chcial dodac ów plik? :)

    2. Awatar Marcin

      Do memcache należy użyć presistent cache wraz z odpowiednią wtyczką.

  3. Awatar Marcin Biegun

    Czy jest sens „bawić się” w transient gdy mamy tyle opcji cacheowania całej strony/zapytań do bazy? Rozumiem, że chodzi o sytuacje, gdy chcemy cacheować konkretną część strony a nie jej całość?

    1. Awatar Konrad Karpieszuk

      jak wspomnialem ma to swoje plusy i minusy. cacheowanie wszystkiego jak leci nie zawsze sie sprawdza. co wiecej zakladam ze tekst czytaja tez tworcy wtyczek, a ci wplywu nie beda mieli czy wtyczka zostanie zainstalowana na wordpressie z cachowaniem czy nie. dlatego warto znac wszystkie opcje

      1. Awatar Marcin Biegun

        a! fakt, tworząc samą wtyczkę a nie pełną stronę, gdzie mamy kontrolę nad cacheowaniem, warto o tym pomyśleć :)

  4. Awatar Marcin

    hry… no to ujawniłeś 1/3 mojej prezentacji na najbliższym wordcampie :( transient, presistent, proxy cache)… cóż.

    1. Awatar Konrad Karpieszuk

      spokojnie, do wordcampu jest jeszcze kilka dni, wiec jest szansa ze ujawnie i pozostale 2/3 ;) a tak powaznie: nie wiedzialem. na WC nie beda wszyscy ktorzy czytaja dev.wpzlecenia i odwrotnie

      1. Awatar Marcin

        Spoko, możesz pisać, przecież to nie problem, może tylko będzie że „znowu to samo”.

  5. […] serwisu Follow us on Twitter 88 śledzących RSS Feed  /  Mail 450 czytelników Zapamiętywanie zmiennych dzięki transient API w WordPressie 1 głosuj! Czy słyszeliście już o transient API w WordPressie? Jeśli nie, to […]

  6. […] też rozważyć zapamiętywanie odpowiedzi zdalnej bazy przynajmniej na kilka minut by nie generować niepotrzebnego ruchu. Tagi: baza danych, […]

  7. […] Inne ciekawe rozwiązanie to wspomniane już wyżej terminy imprez. Każda wycieczka może być zorganizowana wiele razy w roku w różnych datach. Pierwotnie chciałem by klient musiał dla każdego terminu dodać kolejną wycieczkę, którą od poprzedniej różniła by się tylko datą, ale okazało się, że w takim wypadku pracy byłoby więcej niż przy starej wersji strony. Przykładowo „Fotosafari” organizowane jest kilkadziesiąt razy w roku. Dodanie kilkudziesięciu wpisów różniących się jednym polem nie wygląda zbyt wydajnie. Kolejnym utrudnieniem tutaj był fakt, że firma chciała móc przy każdym terminie danej imprezy zaznaczyć, że wszystkie miejsca już zostały wykupione, a nawet nie określać terminu w ogóle – w takim wypadku przy zamawianiu klient sam określał kiedy chce przyjechać na wycieczkę. Cały ten miks miał być trzymany w jakiś sposób w WordPressie i dawać się łatwo zarządzać i porządkować. Udało się, co na przykład możecie zobaczyć na stronie wszystkich wycieczek – imprezy uporządkowane są datami (a gdyby ktoś chciał zmienić sposób porządkowania po długości trwania, nie ma problemu). Jeśli jakiś termin jest już niedostępny informuje o tym odpowiednia ikona. Tabela uwzględnia też imprezy bez określonego terminu i właściciel strony sam decyduje w której części tabeli umieścić taką wycieczkę. Sortować można także po grupie docelowej wycieczki i sposobie jej organizacji. Wypas (btw. po części jak to zrobiłem opisałem w artykule na dev.wpzlecenia). […]