Cache-uri

Cache-ul este o zonă temporară de stocare de informatii, care duplică niste date considerate originale, creat pentru acces direct si rapid asupra acestor date. Aceasta mapare a datelor faţă de locul lor de stocare iniţial, se face pentru  acele date care sunt greu de accesat în mod direct, care se afla în zone partajate, care ar avea un timp de prelucrare ridicat si care ar fi solicitate în mod frecvent, iar rezultatul ar fi de fiecare dată acelaşi.

Sisteme de cache se întalnesc implementate în microprocesoare(exemplul fiind cache-ul de nivel 2), în hard disk-uri(pentru a micşora timpul de citire al datelor), în sisteme de management al bazei de date(MySQL va ţine în cache rezultatul unei interogari, iar la primirea aceleiaşi interogări va returna rezultatul din cache, făra a interoga tabelele în sine), in browser-ele pe car ele folosim zi de zi(Firefox de exemplu îşi va face cache la imagini, ca la un refresh sa nu fie nevoit sa le preia de pe server din nou), chiar şi Google deţine propriul cache din care poate furniza conţinutul paginilor web.

Aşadar, scopul declarat al cache-ului este de a economisi timp.

Teoria Cache-ului

Există cateva concepte cheie în teoria cache-ului care trebuiesc respectate în implementarea unui astfel de sistem

  1. identificatorul unic – care va fi folosit la identificarea elementului în cache
  2. durata de viaţă – defineşte cât timp un element din cache va fi considerat valid
  3. preluarea condiţionată – astfel încat părţile din cod care ar accesa informaţiile originale să fie evitate, dar să si permită reîmprospătarea
  4. resetarea la cerere – pentru păstrarea consistenţei informaţiilor din cache cu cele din locaţia originală, este necesară posibilitatea ca la o modificare a datelor din această locaţie, cache-ul sa fie marcat ca invalid şi reconstruit

Astfel, un algoritm general de folosire a cache-urilor ar fi:

  • dacă elementul din cache cerut de aplicaţie există atunci el va fi returnat intocmai
  • dacă elementul din cache cerut nu există atunci datele acelui cache se vor aduce din locaţia originală, se va crea elementul corespunzător în cache, iar datele vor fi returnate aplicaţiei

Aplicare

Aşadar, vom presupune o apicaţie web, pentru care avem un număr mare de accesări atât din partea vizitatorilor dar şi din partea celor care administrează respectivul website. Pentru o şi mai buna exemplificare, vom considera cazul standard al unui magazin online, în care avem listări de categorii, listări de produse din fiecare categorie şi afisări detaliate de produse(preţ, descriere, detalii tehnice etc). În spatele site-ului, respectiv în aplicaţia de administrare a acestuia, avem un număr de operatori care lucrează necontenit la imbunătăţirea informaţiilor prezentate pe acel site. Mai mult decât atât, să mai luam în calcul existenţa unor aplicaţii care periodic sincronizează preţurile şi stocurile produselor cu cele existente la furnizorii direcţi. Pentru a îmbunătăţi imaginea de ansamblu să considerăm ca magazinul are câteva zeci de mii de produse. În cuvinte mult mai simple şi mai tehnice: o mulţime de interogări sql de tip insert, update dar mai ales select.

Dezavantajul unui astfel de scenariu este evident cel al supraîncărcării bazei de date cu interogări care de cele mai multe ori se vor repeta şi vor furniza acelaşi set de date. Cu toate că, de exemplu, MySQL deţine un cache propriu din care returnează un set de date al unei interogari la o repetare a acesteia, aplicaţia PHP care interogheză baza de date va trebui sa realizeze tot protocolul de comunicare, să furnizeze interogarea şi să primească datele, deci nişte timpi deşi mici, deloc de neglijat în contextul unui volum de trafic ridicat.

Continuând scenariul nostru de “groază” mai trebuie luat în considerare faptul ca la o interogare de tip insert sau update, cele cauzate de aplicaţia de administrare, pot apărea lock-uri pe câmpurile, înregistrările sau chiar tabelele din baza de date, deci până la terminarea execuţiei şi scrierea ori modificarea cu succes a datelor în bază, o instrucţiune select, nu va putea citi baza de date pentru preluarea informaţiilor şi va fi pusă în aşteptare până la terminarea tranzacţiei. Rezultă un timp mort şi mai mare. Cache-ul ar trebui sa intervină în astfel de momente, când putem spune ca majoritatea interogarilor vor furniza acelaşi set de date pentru perioade definite de timp,  iar rularea lor nu ne-ar aduce decât dezavantaje.

Interogând baza de date pentru a obţine informaţiile despre produsele din categoria “Monitoare LCD” vom obţine unset de date reprezentat printr-un vector cu produsele din acea categorie. Datele din acest vector pot fi introduse în cache. Conform algoritmului general descris mai sus putem scrie urmatorul cod PHP

if (!($data = loadFromCache('cache_for_category_id_' . $categoryId)))
{
    $data = loadDataFromDatabase($categoryId);
    saveToCache('cache_for_category_id_' . $categoryId, $data, 3600);
}

Plecăm astfel de la premisa ca informaţia căutată se afla în cache şi chiar încercăm să o preluăm. Dacă functia loadFromCache() va întoarce o valoare nulă atunci înseamnă ca datele nu se află în cache şi ele vor trebui aduse din baza de date, lucru ce se va face prin functia loadDataFromDatabase() iar apoi salvate în cache cu ajutorul funcţiei saveToCache(), cache valabil o ora. Chiar dacă presupunerea noastră iniţială referitoare la existenţa datelor în cache este adevărată sau nu, după executarea acestei porţiuni de cod vom avea în variabila $data informaţiile necesare.

Trebuie avut în vedere faptul că în tot acest timp datele considerate valide sunt cele din baza de date, cache-ul fiind doar o copie locala a acesteia. Deşi sistemul ne va reseta automat cache-ul după expirarea perioadei de viaţă, vor exista situaţii când cache-ul va deveni inconsistent, adică nu va mai reflecta realitatea din baza de date. Deci, la adăugarea unui produs nou în baza de date în categoria “Monitoare LCD”, cache-ul construit mai devreme nu mai este consistent(nu conţine si acest nou produs). Cum varianta în care aşteptăm trecerea celor 3600 de secunde pentru a se recrea cache-ul nu ne multumeşte(perioada putând fi mult mai mare), aplicaţia de administrare va trebui sa intervină asupra cache-ului şî să invalideze înregistrarea ce conţine datele din această categorie. În acest mod vom forţa recreerea cache-ului cu noile informaţii la următoarea accesare a categoriei respective.

resetCache('cache_for_category_id_' . $categoryId);

În tot scenariul de mai sus am considerat crearea de cache-uri pe categorii şi nu unul global care să conţină toate categoriile existente pe site, din considerente de acces si de resetare. Este mai simplu sa alegem direct cache-ul categoriei pe care dorim să o afişăm decât sa încărcăm toate categoriile prin care să o căutăm pe cea dorită, precum este mai normal ca la introducerea produsului nou în categorie să resetăm doar cache-ul categoriei respective şi nu cel al tuturor produselor.

Unelte

PHP nu deţine nativ funcţii de lucru cu cache-ul, însă există extensii PECL care pot fi instalate şi cu care se pot lucra, printre care enumerăm Memcache şi APC.

Folosirea extensiei Memcache:

$cache = new Memcache();
$cache->addServer('localhost');

if(!($data = $cache->get('cache_id'))
{
    $data = getData();
    $cache->add('cache_id', $data);
}
$cache->delete('cache_id');

Folosirea extensiei APC:

if(!($data = apc_fetch('cache_id'))
{
    $data = getData();
    apc_add('cache_id', $data);
}
apc_delete('cache_id');

Diferenţa dintre cele 2 extensii este aceea că Memcache va stoca informaţiile în memoria RAM a serverului, pe când APC le va stoca in fişiere pe hard disc.

UPDATE:

A Practical Guide to Data Caching with Zend Server scrisă de Shahar Evron, Product Manager la Zend Technologies, Inc., este o lucrare apărută recent şi pe care o recomandăm celor interesati de acest subiect.

Trigger-i în MySQL

Un trigger reprezintă o “subrutină” stocată în baza de date care conţine cod executabil, al cărei execuţii se va declanşa automat la întâmplarea unui anume eveniment. Spre deosebire de procedurile stocate sau funcţiile definite de utilizator care sunt definite global într-o bază de date, trigger-ii sunt strict legaţi de tabele, ei se definesc în relaţie cu acestea iar acţiunile lor sunt menite să aibă sens asupra lor sau pornind de la fiecare în parte.

Există două tipuri de trigger-i, cei care afectează toate înregistrările dintr-o tabelă şi cei care afecteză doar o anume înregistrare. Ambele însă au efect(se declanşează) atunci când una din următoarele operaţii este lansată asupra tabelei: INSERT, UPDATE, DELETE, iar pentru aceste operaţii se vor defini trigger-i care au efect ÎNAINTE sau DUPĂ ce instrucţiunea a fost executată pe baza de date.


CREATE
[DEFINER = { user | CURRENT_USER }]
TRIGGER trigger_name trigger_time trigger_event
ON tbl_name FOR EACH ROW trigger_stmt

În MySQL trigger-ii au fost introduşi în versiunea 5.0.2 iar pentru crearea unuia este necesară deţinerea de drepturi corespunzătoare. Clauza DEFINER va stabili nivelul de drepturi necesare pentru a se putea executa trigger-ul;trigger_name reprezintă numele trigger-ului, iar trigger_time şi trigger_event definesc situaţia în care trigger-ului se va executa.

trigger_time reprezintă momentul în care se va executa trigger-ul relativ la operaţia ce s-a lansat pe acea tabela, şi poate avea următoarele valori: BEFORE şi AFTER. trigger_event reprezintă însuşi acţiunea la care se face referire, iar acest parametru va avea una din următoarele valori:

  • INSERT caz în care trigger-ul va fi lansat la execuţia unei instrucţiuni INSERT, LOAD DATA sau REPLACE pe baza de date;
  • UPDATE caz în care trigger-ul se va lansa la apelul unei instrucţiuni UPDATE
  • DELETE caz în care trigger-ul va fi lansat la ştergerea de înregistrări folosind instrucţiunea DELETE; trigger-ul nu va fi lansat la execuţia instrucţiunii TRUCATE sau DROP TABLE

tbl_name reprezintă numele tabelei la care trigger-ul va reacţiona iar trigger_stmt reprezintă instrucţiunea SQL ce va fi lansată; în caz că se doresc execuţia mai multor instrucţiuni acestea vor fi introduse intre cuvintele BEGIN şi END. Toate restricţiile discutate în cadrul articolului Subrutine în MySQL sunt valabile şi pentru trigger-i.


/*
trigger ce va fi lansat inaintea unei instructiuni INSERT
*/
CREATE TRIGGER trTestInsert BEFORE INSERT ON products
FOR EACH ROW BEGIN
SET NEW.products_guid = UUID();
INSERT INTO audit (user_id, products_guid, audit_action) VALUES (NEW.users_id, NEW.products_guid, 'insert');
UPDATE categories SET categories_count = categories_count + 1 WHERE categories_id = NEW.categories_id;
END;
/*
trigger ce va fi lansat dupa o instructiune DELETE
*/
CREATE TRIGGER trTestDelete AFTER DELETE ON products
FOR EACH ROW BEGIN
SET NEW.products_guid = UUID();
INSERT INTO audit (products_guid, audit_action) VALUES (OLD.products_guid, 'delete');
UPDATE categories SET categories_count = categories_count - 1 WHERE categories_id = OLD.categories_id;
END;

În definirea celor 2 trigger-e am folosit două cuvinte rezervate MySQL, şi anume: OLD şi NEW. Prin aceste două referinţe vă puteţi adresa la câmpurile din înregistrările care au fost sau urmează a fi procesate. Astfel la o inserare în baza de date NEW va conţine toate câmpurile din înregistrarea pentru care a fost declanşat acel trigger, pe când la un trigger pe o instrucţiune UPDATE, în OLD veţi găsi toate informaţiile legate de înregistrarea ce a fost modificată.

Trigger-ii ajută la creşterea gradului de automatizare a unor procese, pot fi folosiţi foarte bine la logarea de date sau informaţii, ori folosit la crearea datelor de audit. Nu în ultimul rând pot fi folosiţi şi la aplicarea unor restricţii suplimentare asupra operaţiunilor făcute asupra bazelor de date.

Subrutine în MySQL

Subrutinele sunt mici bucăţi de cod executabil care sunt stocate într-o bază de date relaţională, definite de către utilizator ce are posibilitatea de a le chema atunci când situaţia o cere. La fel ca o funcţie definită de utilizator în cadrul unui limbaj de programare, o subrutină extinde funcţionalitatea nativă oferită de SGBD-ul în care se scrie această subrutină însă personalizată pe baza de date în care este scrisă şi structura acesteia.

Utilizatorii MySQL s-au putut bucura de aceste facilităţi începând cu versiunea 5 a bazei de date, moment în care acestea s-au introdus alături de view-uri sau trigger-i. Până în acest moment (2005), dezvoltatorii MySQL erau nevoiţi să stocheze aceste coduri în aplicaţie sau în diverse fişiere pe server. De-a lungul timpului s-au detaşat 2 şcoli de gândire dintre care unii susţin că funcţionalitatea legată de baza de date ar trebui sa stea în aplicaţie şi alţii care susţin că această funcţionalitate ar trebui sa stea în interiorul bazei de date. Evident, adevărul este undeva la mijloc. Majoritatea programatorilor nu se bazează exclusiv pe una din cele două modalităţi de gândire, de multe ori folosirea unei modalităţi este preferată pentru că aceasta face sens în diverse situaţii. Este interesant totuşi de observat că mulţi gânditori ai şcolii numite în aplicaţie au luat această poziţie pentru că nu exista altă varianta, neexistând proceduri stocate in trecut, şi că aceştia sunt din ce în ce mai puţini. Şi totuşi, de ce ar stoca cineva subrutinele într-o bază de date şi nu în aplicţie?

Avantajele principale ale subrutinelor stocate sunt acelea referitoare la portabilitate şi îmbunătăţire a funcţionalităţii. O subrutină definită în baza de date, va rula în orice condiţii şi pe orice platformă suportată de catre baza de date gazdă. Mai mult, fiind inglobate în baza de date nu are importanţă ce aplicaţie(limbaj de programare) se foloseşte din exteriorul acesteia pentru a le utiliza şi pentru a returna date. Acestea au şi mai multă logică când se folosesc mai multe tehnologii pentru a crea aplicaţii complexe şi interconectabile cu o bază de date centrală. Deasemeni acestea pot reduce considerabil traficul dintre aplicaţie şi baza de date, deci într-un final rezultând un timp de execuţie mai bun al acţiunilor dorite. Imaginaţi-vă o situaţie în care pentru a obtine o raportare este nevoie de interogarea bazei de date, de obţinere a unui set mare de date, prelucrarea acestora în cadrul aplicaţiei, iar apoi pentru fiecare element din setul de date se impune interogarea bazei de date pentru a obţine alt set de date. Este uşor de observat ca transmiterea datelor între baza de date şi aplicaţie este mult mai uşoară si mai puţin costisitoare daca s-ar face când toate datele ar fi pregătite: datele iniţiale ar rămâne în interiorul bazei de date şi nu s-ar trimite la aplicaţie, s-ar prelucra în baza de date, s-ar interoga din nou baza de date direct din interiorul subrutinei, iar la final după ce s-ar fi obţinut şi al doilea set de date, acestea (împreună cu primul) s-ar returna aplicaţiei.

Subrutinele sunt de două feluri: proceduri stocate şi funcţii definite de utilizator. Diferenţa dintre cele 2 este că cea din urmă poate returna o valoare scalară în timp ce procedura stocată poate “returna” şi unul sau mai multe seturi de date. Un exemplu simplu de procedură stocată este următorul:


CREATE PROCEDURE `inphpwetrust`.`spTest`()
BEGIN
SELECT * FROM tbl;
END;
/*
am definit o procedura stocata pe baza inphpwetrust numita spTest
care va returna datele din tabela numita tbl
*/
CREATE PROCEDURE `inphpwetrust`.`spTest`(in varID int)
BEGIN
SELECT * FROM tbl where tbl_id = varID;
END$$
/*
aceasta a doua procedura are rolul de a filtra datele din tabelul tbl dupa un parametru
*/

În definirea unei proceduri stocate se pot folosi mai mulţi parametri de intrare, care la rândul lor pot fi de mai multe feluri dintre care cel implicit este primul:

  • IN va transmite procedurii stocate un parametru care va putea fi folosit, modificat, însă modificările asupra lui nu vor avea efect în exteriorul procedurii
  • OUT nu va transmite procedurii stocate nici o valoare, sau o va considera pe cea trimisă ca fiind NULL; aceasta va fi modificată în interiorul procedurii iar modificările vor avea efect în exteriorul funcţiei
  • INOUT înglobează caracteristicile precedentelor; astfel se va putea trimite un parametru la procedura stocată care va putea fi folosit, modificat şi moridificările vor fi vizibile în exteriorul procedurii


/*
exemplu de procedura stocata ce se foloseste de parametri IN
acestia pot fi specificati sau nu
*/
CREATE PROCEDURE `spParamIN`(IN param1 varchar(10), param2 varchar(10))
BEGIN
select concat(param1, param2) as `concat`;
END;
/*
exemplu de procedura stocata ce se foloseste de parametri OUT
*/
CREATE PROCEDURE `spParamOUT`(IN param1 varchar(10), OUT param2 int)
BEGIN
set param2 = length(param1);
END;
/*
exemplu de procedura stocata ce se foloseste de parametri INOUT
*/
CREATE PROCEDURE `spParamINOUT`(INOUT param1 varchar(10), IN param2 int)
BEGIN
set param1 = param1 + param2;
END;

Folosire

În crerea unei proceduri stocate vă puteţi folosi de orice instrucţiune SQL existentă sau combinaţii ale acestor instrucţiuni. Se pot selecta data din bază, se pot adauga, edita sau şterge înregistrări ale diverselor tabele. Pe lângă aceste operaţii se pot folosi şi varibilele definite de utilizator. Acestea se declară local, folosind sintaxa:


DECLARE var1[,var2, ... , varn] TYPE [DEFAULT value]

Aceste variabile pot fi folosite fie pentru “bucătăria internă” a subrutinei, fie pentru a transmite anumite valori returnate din cadrul unor interogări SQL. Astfel, în exemplul urmator, prima instrucţiune SELECT va genera returnarea unui set de date, pe când cel de-al doilea va atribui valoarea din campul găsit către variabila indicată folosind operatorul INTO


CREATE PROCEDURE `spInto`(IN valID INT)
BEGIN
DECLARE tmpName VARCHAR(100);
SELECT * FROM customers;
SELECT name INTO tmpName FROM customers WHERE id = valID;
END;

Orice modificări aduse conţinutului varibilelor se poate face cu ajutorul operatorului SET, pe una sau mai multe variabile simultan. Se pot utiliza oricare din structurile de control existente in MySQL precum şi cursori sau tranzacţii.

Alte opţiuni

În definirea unei subrutine pot interveni şi alţi parametri ce pot influenţa modul de funcţionare a acestia. Modul general de definire a acestor subrutine este urmatorul:


CREATE
[DEFINER = { utilizator | CURRENT_USER }]
PROCEDURE nume_procedura ([parametrii[,...]])
[caracteristici ...] corp_rutina

CREATE
[DEFINER = { utilizator | CURRENT_USER }]
FUNCTION nume_functie ([parametrii[,...]])
RETURNS tip
[caracteristici ...] corp_rutina

parametrii:
[ IN | OUT | INOUT ] param tip

tip:
Orice tip valid de date MySQL

caracteristici:
LANGUAGE SQL
| [NOT] DETERMINISTIC
| { CONTAINS SQL | NO SQL | READS SQL DATA | MODIFIES SQL DATA }
| SQL SECURITY { DEFINER | INVOKER }
| COMMENT 'string'

corp_rutina:
Instructiuni valide SQL

LANGUAGE SQL este o opţiune care indica limbajul folosit pentru a crea subrutina. Acesta este limbajul implicit, deci poate fi ignorat. O procedură este considerata determinista (specificată prin opţiunea DETERMINISTIC) dacă aceasta va întoarce acelaşi set de date pentru aceeaşi parametri de intrare sau nedeterministă în caz contrar(implicit). Următoarele variabile au in MySQL doar rol informativ şi se referă la natura datelor folosite in cadrul rutinei, MySQL nerestricţionând intrucţiunile folosite în interiorul rutinei bazându-se pe aceste informaţii:

  • CONTAINS SQL specifică faptul că subrutina conţine cod SQL
  • NO SQL indică faptul că subrutina nu conţine cod SQL
  • READS SQL DATA indică faptul că subrutina citeşte date din baza de date (folosind instrucţiunea SELECT)
  • MODIFIES SQL DATA indică faptul ca subrutina va scrie(modifica) date din baza de date (folosind INSERT, UPDATE sau DELETE)

SQL SECURITY se referă la faptul că o subrutină va putea fi executată folosind drepturile utilizatorului care a creat (DEFINER) sau care invocă (INVOKER) acea subrutină. COMMENT permite adăugarea de informaţii suplimentare asupra acelei subrutine

Restricţii

Există câteva restricţii care MySQL le aplică în momentul creării de funcţii sau proceduri stocate şi de care este bine să se ţină cont.

  • instrucţiunile CHECK TABLE, OPTIMIZE TABLE, LOCK TABLES, UNLOCK TABLES, ALTER VIEW, LOAD DATA, LOAD TABLE nu pot fi folosite
  • folosirea de expresii predefinite cu ajutorul PREPARE, EXECUTE, DEALLOCATE PREPARE este interzisă; începând cu versiunea 5.0.13 această restricţie a fost ridicată în cazul procedurilor stocate, însă se menţine în cazul funcţiilor
  • folosirea instrucţiunii INSERT DELAY este identică cu INSERT în interiorul subrutinelor

Următoarele restricţii sunt aplicate doar funcţiilor definite de utilizator:

  • operaţiile care execută COMMIT sau ROLLBACK
  • operaţiile care returnează seturi de date
  • operaţia FLUSH
  • folosirea lor recursiv
  • modificări asupra tabelei are se operează citiri sau scrieri de către instrucţiunea care a chemat funcţia respectivă

Securitate

Pentru a avea posibilitatea de a crea proceduri stocate resepctiv funcţii, utilizatorul de MySQL trebuie să deţină drepturi de CREATE ROUTINE. MySQL va atribui automat drepturi pentru operaţiile ALTER ROUTINE şi EXECUTE pentru utilizatorul care a creat subrutina respectivă.