Autor: Tomasz Jędrzejewski
Data publikacji: 02.03.2004, 17:47 | Ostatnia modyfikacja: 05.11.2006, 18:17
ArtykuÅ‚ ten pokazuje, jak przyspieszyć skrypty wykorzystujÄ…ce bazy danych poprzez cachowanie wyników zapytaÅ„ do późniejszego użycia. Demonstracja na bazie MySQL.
Bardzo dużą część czasu wykonywania skryptu PHP + baza danych zabiera właśnie wysyłanie zapytań SQL, stąd też każdy programista powinien zadbać o to, by wykonywać ich jak najmniej. W tym artykule chcę właśnie to zaprezentować.
Omawiana tu technika zwie siÄ™ cachowaniem wyników zapytaÅ„. Polega ona na tym, że po pierwszym pobraniu jakiÅ› danych z bazy, pakujemy je Å‚adnie do pliku. W nastÄ™pnych uruchomieniach skryptu nie wysyÅ‚amy już zapytania, a dane zasysamy wÅ‚aÅ›nie z utworzonego wczeÅ›niej pliku. Wbrew pozorom nie jest to trudne do wykonania; wymaga tylko odrobiny czasu na zaplanowanie tego tak, by skrypt odpowiednio dziaÅ‚aÅ‚. CaÅ‚kiem dobry mechanizm cache można napisać po jednej godzince pracy nad kodem, a zyski sÄ… znaczne.
JeÅ›li nadal nie jesteÅ› przekonany o skutecznoÅ›ci cachowania, może zaprezentujÄ™ pomiary prÄ™dkoÅ›ci, jakie sporzÄ…dziÅ‚em na tÄ™ okazjÄ™. Normalny czas wykonania zapytania typu "SELECT * FROM tabela" wynosiÅ‚ ok. 0.00075 sekundy, natomiast pobranie ok. 20 rekordów zajmowaÅ‚o 0.0005 sekundy. Wyniki przy zastosowaniu cachowania danych przeszÅ‚y moje najÅ›mielsze oczekiwania. Czas wykonania funkcji wysyÅ‚ajÄ…cej zapytanie byÅ‚ tak niski, że musiaÅ‚em sztucznie go zawyżyć, by uzyskać informacjÄ™ czytelnÄ… dla przeciÄ™tnego zjadacza chleba. Po rÄ™cznym odjÄ™ciu zawyżenia okazaÅ‚o siÄ™, że czas ten wyniósÅ‚... 0.00008 sekundy, a wiÄ™c byÅ‚ ok. 10 razy niższy! Pobieranie wyników również byÅ‚o szybsze, lecz już nie tak spektakularnie - Å›rednia wyniosÅ‚a okoÅ‚o 0.0003 sekundy. Wiem, że różnice rzÄ™du tysiÄ™cznych części sekundy mogÄ… wydawać Ci siÄ™ Å›mieszne, ale weź pod uwagÄ™, że normalny serwer jest znacznie bardziej obciążony. Przecież na sekundÄ™ może obsÅ‚ugiwać nawet 10 wywoÅ‚aÅ„ różnych skryptów, z czego każdy żąda dostÄ™pu do bazy. Wtedy nawet czasy rzÄ™du jednej dziesiÄ™ciotysiÄ™cznej sekundy sÄ… niezwykle cenne!
Zacznijmy od zaplanowania pracy. RzeczÄ… najważniejszÄ… jest wykorzystanie rÄ™cznie napisanego sterownika do bazy danych (tÅ‚umaczenie dla programistów zanglicyzowanych: database layer), bo bez niego to nawet nie masz co marzyć o stworzeniu cachowania, chyba że lubisz do każdego wywoÅ‚ania mysql_query() doklepywać rÄ™cznie dodatkowych 20 linijek kodu :). Przy okazji jest to jedna z sytuacji, kiedy użycie takiego sterownika przydaje siÄ™ nawet wtedy, gdy nie masz zamiaru wykorzystywać przenoÅ›noÅ›ci miÄ™dzy różnymi bazami danych.
Jeśli chcesz się dowiedzieć więcej o takim sterowniku, możesz zajrzeć do artykułu sickboy'a zatytułowanego "Prosty sterownik MySQL w PHP".
Jako, że bÄ™dziemy cachować tylko niektóre zapytania, musimy mieć możliwość włączania tej możliwoÅ›ci na życzenie. Dlatego też domyÅ›lnie zapytania bÄ™dÄ… wykonywane bez tego. Dopiero po wywoÅ‚aniu metody sql_cache(), do której podamy nazwÄ™, pod jakÄ… chcemy zapisać nasze dane, mechanizm zostanie uaktywniony. Po pobraniu wyników bÄ™dziemy go z powrotem wyłączać (by nastÄ™pne zapytanie omyÅ‚kowo nie wykorzystaÅ‚o tego samego uchwytu i nie zwaliÅ‚o wszystkiego), wywoÅ‚ujÄ…c metodÄ™ sql_cache() bez parametrów. Dodatkowo musimy mieć możliwość czyszczenia cache, co bardzo przyda siÄ™ nam przy zmianie danych w bazie. Zaraz po dodaniu/zmianie/usuniÄ™ciu czegoÅ›, skasujemy plik ze starymi informacjami, co spowoduje, że pierwszy gość, który zażąda do nich dostÄ™pu, pobierze wyniki bezpoÅ›rednio z bazy. ZostanÄ… one od nowa scachowane. Takim oto sposobem zmiana informacji bÄ™dzie od razu widoczna w serwisie.
Aby wszystko to byÅ‚o możliwe, sterownik musi przechowywać gdzieÅ› informacjÄ™ o tym, w jakim trybie aktualnie pracuje. Trybów bÄ™dzie trzy. Pierwszy z nich (numer 0) - cachowanie wyłączone, normalny tryb pracy. Drugi (1) oznacza czytanie z pliku cache bez wykonywania danego zapytania. Trzeci i ostatni (z numerkiem 2) bÄ™dzie informowaÅ‚ sterownik, iż zapytanie ma wykonać, ale jego wyniki ma zachować także w odpowiednim pliku. Dla nas oznacza to po prostu utworzenie pliku cache.
Na poczÄ…tek chciaÅ‚bym dodać pewnÄ… uwagÄ™ odnoÅ›nie kodu do niektórych "czytelników" moich artykuÅ‚ów: nawet nie próbuj bezmyÅ›lnie kopiować poniższego kodu, nie patrzÄ…c na opisy do poszczególnych jego fragmentów. Na pytania "dlaczego to nie dziaÅ‚a" spowodowane wÅ‚aÅ›nie przez gÅ‚upotÄ™ (bo inaczej tego nazwać nie można) po prostu nie bÄ™dÄ™ odpowiadaÅ‚. Najpierw zajmiemy siÄ™ poczÄ…tkiem pliku sterownika, czyli deklaracjÄ… klasy + połączeniem siÄ™ z bazÄ…:
<?php define('CACHE_DIR', './sql_cache/'); class sql{ var $connection; var $result; var $rows; var $queries = 0; var $cache_state =0; var $cache_file; var $cache_buffer; var $cache_ptr; function sql_connect($host, $user, $pass, $db){ $this -> connection = mysql_connect($host, $user, $pass); mysql_select_db($db); } function sql_close(){ mysql_close($this -> connection); }
StaÅ‚a CACHE_DIR przechowuje Å›cieżkÄ™ do katalogu, w którym bÄ™dÄ… skÅ‚adowane pliki cache. Ponadto, w deklaracji klasy znajdujÄ… siÄ™ cztery pola wykorzystywane przez nasz mechanizm. $cache_state przechowuje numer aktualnego stanu (0, 1, lub 2). $cache_file trzyma nazwÄ™ pliku, w którym cache ma być zachowane. $cache_buffer to bufor, w którym bÄ™dziemy gromadzić dane przy generowaniu cache, bÄ…dź z którego bÄ™dziemy je czytać. Natomiast $cache_ptr przyda siÄ™ wÅ‚aÅ›nie przy pobieraniu danych, przechowujÄ…c numer ostatnio pobranego pola w buforze.
Teraz podstawa mechanizmu pozwalająca nam go włączyć, bądź wyłączyć:
function sql_cache($handle = 0){ if(is_string($handle)){ if(file_exists(CACHE_DIR.'xxx_'.$handle.'.666')){ $this -> cache_state = 1; $this -> cache_ptr = 0; $this -> cache_buffer = unserialize(file_get_contents(CACHE_DIR.'xxx_'.$handle.'.666')); }else{ $this -> cache_state = 2; $this -> cache_buffer = array(); $this -> cache_file = CACHE_DIR.'xxx_'.$handle.'.666'; } }else{ if($this -> cache_state == 2){ file_put_contents($this -> cache_file, serialize($this -> cache_buffer)); } $this -> cache_state = 0; } }
Jeśli podaliśmy uchwyt (is_string($handle)), PHP musi zdecydować, czy należy pobrać dane z cache, czy też takowe wygenerować. Określa to na podstawie istnienia pliku danego uchwytu. Jeżeli istnieje - to OK (tryb 1), w przeciwnym wypadku będziemy go generować (tryb 2). Niepodanie uchwytu wyzwoli mechanizm wyłączania cache. Gdy ten był włączony w trybie zapisu, musimy dodatkowo zachować nasze dane w pliku.
Scachowane dane zachowane sÄ… w pliku w postaci zserializowanej tablicy. Dlaczego tak? Przecież mogÅ‚em generować od razu odpowiedni kod PHP, który wystarczyÅ‚oby tylko dołączyć poprzez require()! Otóż w tym przypadku nie jest to najwydajniejsze wyjÅ›cie. ZmierzyÅ‚em czas odczytu dla obu metod - serializacji i plików PHP. Wynika z nich, iż "odserializowywanie" danych jest dwa razy szybsze, niż dołączanie kodu PHP z nimi używajÄ…c require()!
Tu chciaÅ‚bym zamienić sÅ‚ówko z posiadaczami przedpotopowego PHP 4 :). Otóż z jakiÅ› powodów nie ma w nim jeszcze funkcji file_put_contents() pozwalajÄ…cej w prosty sposób zapisać dane do pliku za jednym zamachem. Jako, że takowa jest wykorzystywana w naszym skrypcie (pisaÅ‚em go na PHP 5), musisz jÄ… samemu "zrobić". PodajÄ™ tu gotowy kod, który należy umieÅ›cić PRZED deklaracjÄ… aktualnie pisanej klasy:
function file_put_contents($plik, $dane){ $f = fopen($plik, 'w'); fwrite($f, $dane); fclose($f); }
Wracamy do naszej właściwej klasy. Teraz prościutka metoda czyszczenia cache:
function sql_cache_remove($handle){ if(file_exists(CACHE_DIR.'xxx_'.$handle.'.666')){ unlink(CACHE_DIR.'xxx_'.$handle.'.666'); } }
Wiadomo - jeżeli plik dla danego uchwytu istnieje, to go wywal.
Teraz przechodzimy do najważniejszych metod sterownika - sql_query(), sql_fetch_array(), oraz sql_fetch_row(). To dzięki nim będziemy wykonywali operacje na bazie danych:
function sql_query($query){ if($this -> cache_state != 1){ $this -> result = mysql_query($query); $this -> queries++; if(mysql_errno() != 0){ die('Error: '.mysql_error().'<br/>'); } return 1; } }
W tej metodzie zapytanie jest wysyłane jedynie wtedy, gdy cachowanie jest wyłączone (tryb 0), lub jesteśmy w trakcie generowania pliku cache (tryb 2). Jeżeli czytamy dane z pliku, nic się nie dzieje i to jest recepta na tak niski czas wykonywania tej metody.
Metody pobierania danych sÄ… już troszkÄ™ bardziej skomplikowane, gdyż muszÄ… inaczej obsÅ‚ugiwać każdy z trybów pracy. WrzuciÅ‚em obie naraz, gdyż różniÄ… siÄ™ one tylko tym, że w jednej wywoÅ‚ywana jest funkcja mysql_fetch_assoc(), a w drugiej mysql_fetch_row().
function sql_fetch_array(){ if($this -> cache_state == 1){ if(!isset($this -> cache_buffer[$this -> cache_ptr])){ return 0; } $this -> rows = $this -> cache_buffer[$this -> cache_ptr]; $this -> cache_ptr++; return 1; }else{ if($this -> rows = mysql_fetch_assoc($this -> result)){ if($this -> cache_state == 2){ // Dodaj do cache $this -> cache_buffer[] = $this -> rows; } return 1; } } return 0; } function sql_fetch_row(){ if($this -> cache_state == 1){ // czy koniec bufora? if(!isset($this -> cache_buffer[$this -> cache_ptr])){ return 0; } // odczytaj z bufora $this -> rows = $this -> cache_buffer[$this -> cache_ptr]; $this -> cache_ptr++; return 1; }else{ if($this -> rows = mysql_fetch_row($this -> result)){ if($this -> cache_state == 2){ // Jeśli tworzymy cache, musimy rekord dodatkowo zapisac w buforze $this -> cache_buffer[] = $this -> rows; } return 1; } } return 0; } } // koniec klasy ?>
Stan 1 obsługuje pierwsza część metody. Wykorzystujemy tam czytanie z naszego bufora cache - $this -> cache_buffer, a do identyfikacji, rekordu do pobrania używamy $this -> cache_ptr. Oczywiście na początku musimy sprawdzić, czy przypadkiem nie osiągnęliśmy już końca bufora. Gdyby tego nie było, albo znajdowało się to w innym miejscu, otrzymalibyśmy albo pętlę nieskończoną, albo błędy przy pobieraniu.
Czytanie z bazy danych jest już bardziej znajome - po prostu przypisanie tablicy wygenerowanej przez funkcję mysql_fetch_xxx(). Dodatkowo doszło tu sprawdzenie pozwalające nam zapisać tę tablicę w buforze, w przypadku generowania cache. I to właściwie tyle, jeśli chodzi o kod.
Teraz omówiÄ™, jak używać podanego powyżej sterownika. Wbrew pozorom jest to bardzo proste. Oto przykÅ‚ad kodu bez cachowania:
<?php require('./sterownix.php'); $sql = new sql; $sql -> sql_connect('localhost', 'root', '', 'moja_kurde_baza'); $sql -> sql_query('SELECT * FROM config'); while($sql -> sql_fetch_row()){ echo $sql -> rows[0].' - '.$sql -> rows[1].'<br/>'; } $sql -> sql_close(); ?>
Jak widać, użycie sterownika bez wykorzystania cachowania jest banalnie proste. Podobnie jest także z użyciem cachowania:
<?php require('./sterownix.php'); $sql = new sql; $sql -> sql_connect('localhost', 'zyx', 'doopah', 'fws'); $sql -> sql_cache('uchwyt'); $sql -> sql_query('SELECT * FROM config'); while($sql -> sql_fetch_row()){ echo $sql -> rows[0].' - '.$sql -> rows[1].'<br/>'; } $sql -> sql_cache(); $sql -> sql_close(); ?>
Aby scachować jakieÅ› zapytanie, musimy przed nim wywoÅ‚ać metodÄ™ sql_cache() z podanÄ… nazwÄ… uchwytu, pod jakim chcemy te dane zapisać. Po pobraniu wyników cache jest wyłączany, by nie powodować problemów z resztÄ… zapytaÅ„. JeÅ›li chcesz cachować dwa zapytania nastÄ™pujÄ…ce po sobie, także nie możesz zapomnieć o uprzednim wyłączeniu mechanizmu:
$sql -> sql_cache('zapytanie1'); ... $sql -> sql_cache(); $sql -> sql_cache('zapytanie2'); ... $sql -> sql_cache();
Posiadanie cache to jedna strona medalu - wÅ‚aÅ›ciwe użycie to druga. Przede wszystkim nigdy nie powinieneÅ› używać go przy zapytaniach, w których musisz pobrać konkretnÄ… informacjÄ™, np. dane użytkownika, który chce siÄ™ zalogować. Takie coÅ› mija siÄ™ z celem i może spowodować mnóstwo błędów. WedÅ‚ug mnie przydaje siÄ™ to bardzo przy wyÅ›wietlaniu listy dostÄ™pnych zasobów strony, np. newsów, listy artykuÅ‚ów, czy też porad. Listy takie oglÄ…da mnóstwo ludzi i dlatego należaÅ‚oby przyspieszyć ich generowanie m.in wÅ‚aÅ›nie cachujÄ…c je! Z kolei pojedynczy zasób, np. artykuÅ‚ także do scachowania raczej siÄ™ nadaje, gdyż zajmować bÄ™dzie niepotrzebnie miejsce na twoim koncie. MusiaÅ‚byÅ› także rozwiÄ…zać problem z uchwytami.
Możesz także użyć mechanizmu do przyspieszenia inicjacji silnika strony. W moim skrypcie w ten sposób oszczÄ™dziÅ‚em sobie każdorazowe zmuszanie bazy do pobrania konfiguracji strony, a także menu. Obie te rzeczy mogÄ™ przecież bez żadnych moralnych itp. szkód trzymać na dysku i w ten sposób przyspieszyć pracÄ™ silnika.
ChciaÅ‚bym poruszyć jeszcze sprawÄ™ edycji/dodawania/usuwania danych z cachowanej tabeli. Otóż w takim przypadku zaraz po wysÅ‚aniu do bazy odpowiedniego zapytania musisz usunąć plik cache, przez co pierwszy użytkownik, który zechce sobie coÅ› scachowanego obejrzeć, automatycznie wygeneruje nowy plik z już zmienionymi danymi. Możesz to zrobić, używajÄ…c metody sql_cache_remove($uchwyt); dostÄ™pnej w sterowniku:
$sql -> sql_query('INSERT INTO news VALUES ....'); $sql -> sql_cache_remove('news');
W ten oto sposób rozwiążesz problem z aktualizacjÄ… zasobów serwisu.
Autor: Tomasz "Zyx" Jędrzejewski, www.zyxist.com
Waszym zdaniem:
Nikt jeszcze nie dodał swojego komentarza. Możesz być pierwszy!