miercuri, martie 13, 2013

Concurenta (in baza de date)



Notiuni de baza
  • baza de date relationale (SGBD) - OSS (Mysql, PostgreSQL) Commercial (Oracle, MSQL, DB2, Informix)
  • tranzactii (ACID)
  • SQL
  • JDBC
  • ORM (Hibernate)
  • JPA (OpenJPa)
  • GORM
Concurenta in baze de date

In aplicatiile enterprise, concurenta (accesul concurent al utilizatorilor: vizitatori = nu au cont, utilizatori = au cont, clienti = platesc pentru servicii) este asigurata in nivelul de prezentare de pool-uri de thread-uri si enqueuing, in nivelul de servicii de pool-uri de obiecte sau pur si simplu singletons (traversati concurent de thread-uri). Spre exemplu in aplicatiile Web pentru orice request venit din browser se ia un thread thin pool care trece prin workflow-ul din controller (action, backed bean, etc), apoi in busines-ul implementat in serviciu (sesion bean, singleton) si mai apoi in partea de persistenta (DAO, persistence session). Practic un acelasi thread parcurge toate layer-ele aplicatiei de la cerere si pana la returnarea raspunsului (un contraexemplu sunt apelurile neblocante: NIO, Netty, Scala, Play si alte tehnologii moderne despre care nu discutam aici).

Problemele de concurenta in business-ul aplicatiei se refera la accesarea unor informatii partajate in comun de diverse entitati. Spre exemplu ce se intampla cand doi utilizatori ai aplicatiei partajeaza un acelasi cont (de banca) avand drept de citire si scriere simultan (o singura entitate de domeniu numita Cont). Spre exemplu pun si scot bani din contul respectiv in mod concurent. Nu constitue probleme de concurenta cazul in care fiecare utilizator are un cont al lui in care scrie si citeste si daca e singurul utilizator care-si acceseaza contul.

Problemele de concurenta de business ale aplicatiilor se sumarizeaza astfel in probleme in accesarea unui acelasi row dintr-o tabela de catre mai multe tranzactii. Tranzactiile respecta principiile ACID dintre care atomicitatea (totul sau nimic) si izolarea (marirea concurentei) se reflecata si in API-ul JDBC. Aceste principii ale tranzactiei complica si mai mult probblemele care pot aparea din cauza concurentei - tranzactiile au o viata mai lunga decat operatiile atomice simple, se poate faca rollback unei tranzatii, etc.

Probleme de concurenta posibile

Pot aparea urmatoarele tipuri de probleme pe care le vom numi inconsistente logice prima categorie legata de scriere iar a doua de citire:

Lost updates

Second lost update problem
Two concurrent transactions both read a row, one writes to it and commits, and then the second writes to it and commits. The changes made by the first writer are lost.

Inconsistent reads


Dirty Read
One transaction reads changes made by another transaction that hasn’t yet been committed. This is dangerous, because those changes may later be rolled back.
Unrepeatable Read
A transaction reads a row twice and reads different state each time. For example, another transaction may have written to the row, and committed between the two reads.
Phantom Read
A transaction executes a query twice, and the second result set includes rows that weren’t visible in the first result set. This situation is caused by another transaction inserting new rows between the execution of the two queries.

Problemele de scriere (lost updates) trebuie rezolvate de programator prin locking explicit - optimistic/pessimistic pe cand cele de citire (inconsistent reads) se rezolva prin setarea nivelului de izolare al tranzactiilor - deci implicit de catre baza de date - uneori tot prin lock-uri automate dar si prin alte mecanisme. Nivelurile de izolare posibile: READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE au fost introduse pentru a relaxa izolarea maxima intre tranzactii data de  SERIALIZABLE introducand pe rand cate o problema de concurenta de tip inconsitents read dar marind in acest fel concurenta (liveness) - cel care seteaza nivelul de izolare (global la nivelul aplicatiei sau la nevelul fiecarei tranzactii) trebuie sa fie constient de problemele care pot aparea.


Isolation level
Dirty reads
Non-repeatable reads
Phantoms
Read Uncommitted
may occur
may occur
may occur
Read Committed
-
may occur
may occur
Repeatable Read
-
-
may occur
Anomaly Serializable
-
-
-

Optimistic/pessimistic Locking

Mecanismul standard prin care se pot evita problemele de concurenta sunt lock-uri. Acestea se impart in doua categorii optimistic si pessimistic. Sunt usor de inteles pentru cei care folosesc un SCM (Source Control Management) cum e SVN sau GIT care ofera amblele mecanisme de locking. Lock-urile optimiste corespund mai mult cu modul natural in care omul rezolva problemele doar cand apar. Putem sa ne gandim ca ne ducem la doctor doar cand avem nevoie si nu incercam niciodata sa prevenim boala prin anumite restrictii pe care ni le impunem mereu. Sau conducem agresiv in trafic in ideea ca daca apare o situatie periculoasa pot sa o evit.

Implemetarea la nivel de baza de date (de fapt se face printr-un workaround la nivel de ORM sau SQL-uri pentru ca baza de date nu face astfel de loc-uri - face doar pessimistic) se face prin introducerea unui camp de tip version sau timestamp in fiecare tabela, care este citit inainte de modificare si reverificat daca a fost schimbat inainte de a scrie. Daca a fost schimbat intre timp inseamna ca un lost update s-a produs si in acest caz se arunca o exceptie care trebuie tratata de dezvoltator. Tratarea se poate face in functie de situatie: se poate face merge si in acest fel se tine cont de valoarea salvata intre timp. Optimistic locking garanteaza ca prima tranzactie care opereaza row-ul iese castigatoare. Eroarea in cazul Hibernate este: org.hibernate.StaleObjectStateException.

Pessimistic locking inseamna blocarea accesului la row de catre tranzactia curenta pana ce se termina (e o idee bun ca lock-ul sa se faca cat mai tarziu in tranzactie pentru ca el va fi eliberat doar cand tranzactia se termina). Pessimistic locking este chiar blocare efectiva a accesului de citire, scriere spre deosebire de optimistic care nu insemna efectiv blocare de acces. In cazul pessimistic locking pot aparea alte probleme noi de care trebuie tinut cont si anume: deadlocks (exceptia de JDBC este: com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction). Cauza lor este incrucisarea de lock-uri intre tranzactii si de obicei baza de date detecteaza cand apare un deadlock si iese cu exceptie.

Optimistic locking este perfect pentru situatii in care se poate decide oarecum ce trebuie facut cand apare (mai mult pentru operatii de GUI deoarece poti spre exemplu sa ceri utilizatorului sa mai incerce o data). Daca vrei precizie, cum poate fi cazul back-end-ului aplicatiei si nu poti distinge cine si cand va modifica acel row atunci pessimistic locking pare a fi mai potrivit. GORM (Grails) este setat by default cu optimistic locking. Hibernate nu e setat pe nici una din metode dar ofera suport pentru amandoua.

Locks in tranzactii

O tranzactie cand vrea sa citeasca sau sa scrie un row trebuie mai intai sa obtina un lock pe acel row. Lock-uri sunt de doua feluri:
  • shared lock - sunt lock-uri neblocante in citire dar care blocheaza obtinerea de catre o alta tranzactie a unui lock de scriere (o alta tranzactie poate obtine un lock de citire dar nu unul de scriere)
  • exclusive locks - sunt lock-uri de scriere blocante = celelate tranzactii trebuie sa astepte pentru a obtine un lock de citire sau scriere; doar tranzactia curenta poate scrie cand are lock-ul iar celelate sunt in asteptare
Aici vorbim de lock-urile care se pun la nivel de row. Exista si la nivel de tabele - acestea micsoreaza drastic concurenta.

Exista si un LOCK TIMEOUT care forteaza tranzactiile blocate sa iasa cu exceptie daca a expirat timpul maxim de bolcare (eroarea JDBC este java.sql.SQLException: Lock wait timeout exceeded; try restarting transaction). Pentru mysql aceasta valoare este de 50 de secunde.

Exista lock-uri implicite puse de nivelul de izolare si explicite puse de catre dezvoltatori. Lock-urile explicite pot fi puse pentru a evita spre exemplu lost update dar si pentru a oferi aplicatiei corectitudine logica - spre exemplu se blocheaza o inregistrare la citire pentru a folosi datele acelui row in operatii care murdaresc alte row-uri (exemple). Lock-urile explicite se pot pune la nivel de SQL prin constructii de genul SELECT ... FOR UPDATE sau SELECT … LOCK IN SHARE MODE. In framework-uri ORM (Hibernate, GORM, OpenJPA) se folosesc call-uri de tip lock() care trenslateaza spre query-uri SQL de tip SELECT ... FOR UPDATE.

Mecanismul de lock-uri implicite poate fi evitat de implementatorii RDBMS-ului in implementarea nivelurilor de izolare pentru o mai buna concurenta. O alternativa este spre exemplu este MVCC care propune un sistem bazat pe versionarea recordurilor pentru evitarea problemelor de concurenta (algoritmul folosit pentru MySQL se gaseste in carti de profil si nu in documentatie). Cam toate SGBD-urile nu se mai bazeaza pe lock-uri pentru nivelurile de izolare.

Bazele de date pot sa nu foloseasca lock-uri implicite pentru a aplica nivelurile de izolare pentru a incuraja concurenta tranzactiilor (liveness). Spre exemplu in cazul in care lock-urile ar fi folosite in REPEATABLE READ nu ar mai exista posibilitatea de a avea probleme de lost update. Dar daca mecanismul este  multi versioning (MVCC ) atunci nu ne putem baza pe nivelul de izolare pentru lost update si deci trebuie sa facem explicit lock - pessimistic sau optimistic.
 
MVCC reuseste sa rezolve problemele de concurenta. In Mysql cand se foloseste MVCC pentru toate nivelurile de izolare inafara de SERIALIZABLE nu se fac lock-uri pe query-uri simple de genul SELECT FROM - acestea sunt numite consistent non blocking read. Deci concurenta intre tranzactii este maxima + se respecta nivelurile de izolare - deci nu apar probleme specifice fiecarui nivel de izolare.

Referinte
http://www.cs.umb.edu/~poneil/iso.pdf
http://dev.mysql.com/doc/refman/5.0/en/innodb-transaction-model.html
http://docs.jboss.org/hibernate/orm/3.3/reference/en/html/transactions.html
http://openjpa.apache.org/builds/2.2.1/apache-openjpa/docs/main.html
http://www.ibm.com/developerworks/data/zones/informix/library/techarticle/db_isolevels.html
http://www.peternilsson.me/#post12
http://www.anyware.co.uk/2005/2012/11/12/the-false-optimism-of-gorm-and-hibernate