Tranzakciókezelés¶
Tranzakciónak nevezünk minden olyan utasítássorozatot, melyben minden egyes utasításnak sikeresen végbe kell mennie. Amennyiben a tranzakció egy vagy több utasítása sikertelen, úgy az egész tranzakció érvényét veszíti és vissza kell állítanunk a tranzakció által módosított adatokat a tranzakció előtti állapotra. Ez utóbbi az alkalmazás integritásának megőrzését segíti, azaz az adatok aktuális értéke (az alkalmazás állapota) nem sérül, azaz nem kerülünk invalid állapotba. A tranzakciók egy vagy több erőforrást használhatnak (legyen ez adatbázis vagy egy üzenet sor)
A tranzakcióknak alapvetően két fajtáját különböztethetjük meg:
- Lokális: A tranzakciók kizárólag egy adott erőforrásra vonatkoznak (pl.: egy adatbázis).
- Globális: A konténer kezeli, mely több tranzakciós erőforrást is magában foglalhat (egy tranzakció során több erőforrást használunk).
Lokális tranzakciók¶
A lokális tranzakciók megvalósítása egyszerű feladat, viszont későbbi globális bővítés során (több tranzakciós erőforrás kezelése) a megírt kód felhasználása nem valószínű.
A tranzakciók általános érvényűek, így a lokális tranzakcióban résztvevő erőforrás sokféle lehet. Itt azonban csak az adatbázisokkal foglalkozunk.
JDBC¶
Magát a JDBC-t már korábban bemutattuk. Most nézzük meg, hogy miként vehet részt erőforrásként a tranzakciókezelésben. Maga a JDBC a következőképpen ékelődik be az alkalmazásunk és az adatbázis közé:
A JDBC támogatást ad az SQL utasítások tranzakciókban való futtatásához.
A Connection
alapértelmezett viselkedése auto-commit
, azaz minden egyes utasítást külön tranzakcióként kezel, melyeket automatikusan commit-ol a lefuttatás után.
Ugyanakkor arra is lehetőség van, hogy több utasítást összefogjunk egy tranzakcióba.
1 2 3 4 5 6 7 8 9 10 11 |
|
A fenti kódban az auto-commit
-ot kikapcsoljuk, így ilyen esetben manuálisan válogathatjuk össze az egy tranzakcióban lefuttatni kívánt utasításokat, melyeket aztán nekünk kell kommitálnunk vagy hiba esetén rollback-elnünk.
Ezen felül a JDBC arra is lehetőséget ad, hogy mentési pontokat adjunk meg.
JPA¶
A JPA-val külön foglalkoztunk az előző fejezetben, így itt azt külön nem részletezzük.
Ami a tranzakciók szempontjából lényege az az, hogy egy perzisztencia kontextushoz (Persistence Context) több EntityManager is tartozhat. A perzisztencia kontext kétféle lehet:
- Transaction-scoped: Egy darab tranzakcióhoz kötött, ez az alapértelmezett
- Extended-scoped: Több tranzakcióhoz is kötődhet
Példa:
1 2 3 4 5 6 7 8 9 10 |
|
Globális tranzakciók¶
Globális tranzakciók megvalósítása során a JTA (Java Transactional API) áll rendelkezésre.
Ebben az esetben minden tranzakciós erőforrást egy tranzakciókezelő (Resource Manager) vezérel, melyek páronként az XA(nyílt standard az elosztott tranzakciókezeléshez) protokollon keresztül képesek kommunikálni.
A tranzakciókezelőket általában az adott tranzakciós erőforrás gyártója adja, például MySQL esetében MysqlXADataSource
.
Magukat a tranzakciókezelőket fogja össze a JTA Transaction Manager, mely koordinálja és szinkronizálja az összes tranzakciókezelő munkáját.
A fentieket foglalja össze a következő ábra:
Az ilyen felépítésű globális tranzakciókezelést elosztott tranzakciókezelésnek is hívják.
Springben a PlatformTransactionManager
interface a TransactionDefinition
és a TransactionStatus
interface-eket használja a tranzakciókezelés megvalósításában.
A tranzakciókezelők tekintetében számos választásunk van:
DataSourceTransactionManager
: JDBC-hezJpaTransactionManager
: JPA-val kompatibilisHibernateTransactionManager
: Hibernate tranzakciókezelőjeJmsTransactionManager
: JMS kompatibilis
Az eddig bemutatottak általános érvényűek. Jelen fejezetben a relációs adatbázisokra vonatkozó tranzakciókezelésre fókuszálunk.
Tranzakciók tulajdonságai¶
A tranzakciókezelés során 4 tulajdonságot kell szem előtt tartanunk, melyeket összefoglalva ACID néven is szoktak emlegetni
- Atomicity: Az atomicitás megköveteli, hogy több műveletet atomi (oszthatatlan) műveletként lehessen végrehajtani, azaz vagy az összes művelet sikeresen végrehajtódik, vagy egyik sem.
- Consistency: A konzisztencia biztosítja, hogy az adatok a tranzakció előtti érvényes állapotból ismét egy érvényes állapotba kerüljenek. Minden erre vonatkozó szabálynak (hivatkozási integritás, adatbázis triggerek stb.) érvényesülnie kell.
- Isolation: A tranzakciók izolációja azt biztosítja, hogy az egy időben zajló tranzakciók olyan állapothoz vezetnek, mint amilyet sorban végrehajtott tranzakciók érnének el. Egy végrehajtás alatt álló tranzakció hatásai nem láthatóak a többi tranzakcióból.
- Durability: A végrehajtott tranzakciók változtatásait egy tartós adattárolón kell tárolni, hogy a szoftver vagy a hardver meghibásodása, áramszünet, vagy egyéb hiba esetén is megmaradjon.
Ezeket a tulajdonságokat maguknak a tranzakciós erőforrásoknak kell biztosítaniuk, noha a CAP-tétel gátat szab ezek egyszerre történő biztosításakor. Néhány dolgot azonban befolyásolhatunk, mint például: tranzakció csak olvasást végezhet, izolációs szint meghatározása.
Spring-ben a PlatformTransactionManager.getTransaction()
metódusa egy TransactionDefinition
-t vár paraméterül és egy TransactionStatus
-t ad vissza.
A statust a tranzakciók futásának szabályozásához lehet felhasználni (megadja, hogy egy új tranzakcióról van szó vagy az adott tranzakció véget ért, stb.).
A TransactionDefinition
-ben megadhatjuk egy tranzakció tulajdonságait:
1 2 3 4 5 6 7 8 |
|
A tranzakció izolációs szintje megadja, hogy a konkurens tranzakciók mit láthatnak egymás adataiból. Idevágó fogalmak, melyek az adatok konkurens hozzáférésekor léphetnek fel:
- Dirty read: a még nem kommitált adat kiolvasása egy konkurens tranzakcióból (mely később még változhat)
- Nonrepeatable read: Más értékek eredményül kapása egy sor újra olvasása esetén, mert egy konkurens tranzakció frissítette ugyanazt a rekordot (és kommitálta is a változásokat)
- Phantom read: több rekord újbóli lekérdezése esetén más sorokat kapok vissza, mert egy konkurens tranzakció hozzáadott vagy törölt rekordokat (ezeket kommitálta is közben)
Az izolációs szintek a következőek lehetnek:
ISOLATION_DEFAULT
: Alapértelmezett (a mögötte lévő datasource-tól kéri le)ISOLATION_READ_UNCOMMITTED
: A legalacsonyabb szintű izoláció, mivel ez a tranzakció láthatja a többi tranzakció még nem commitált adatait (olvasásra). Dirty-rad, Nonrepeatable read és Phantom read is előfordulhat.ISOLATION_READ_COMMITTED
: A legtöbb adatbázisban ez az alapértelmezett. Biztosítja, hogy a még nem kommitált adatokat ne lehessen olvasni (más tranzakcióknak) a tranzakció közben. Ugyanakkor, a kommitált adatokat kiolvashatják más tranzakciók és módosíthatják is azt. A dirty read-től megvéd, de a többi még előfordulhat.ISOLATION_REPEATABLE_READ
: Az előzőnél szigorúbb. Újra kiválasztható az adathalmaz azután is, hogy egy másik tranzakció beszúrt új adatot (még nem kommitált adatot), viszont ugyanazt az eredményt kapjuk. A phantom read továbbra is felléphet, viszont ezzel az izolációs szinttel megakadályozzuk, hogy két konkurens tranzakció ugyanazt a rekordot szerkessze egyszerre.ISOLATION_SERIALIZABLE
: Legmegbízhatóbb, de a legdrágább. Teljes atomicitás biztosítása: olyan mintha a tranzakciók egymás után futnának le.
Az izolációs szint kiválasztása kulcsfontosságú az adatok konzisztenciájának megőrzése szempontjából, ugyanakkor a konzisztencia szempontjából leghatékonyabb megoldás nyilván a legtöbbet veszi el a teljesítményből.
A propagálás szintjei a következőek lehetnek:
REQUIRED
: Az alapértelmezett propagációs szint. Amennyiben nincs futó tranzakció a megadott néven, akkor a Spring csinál egy újat. Ellenkező esetben a lefuttatandó utasítások belekerülnek a tranzakció végére.SUPPORTS
: Amennyiben van futó tranzakció, akkor annak utasításai után belekerülnek az új utasítások is, de ha nincs futó tranzakció, akkor az utasítások végrehajtása tranzakció nélküli lesz.MANDATORY
: Ha van aktív tranzakció, akkor azt használja, ha nincs akkor kivételt dob.NEVER
: Ha van futó tranzakció, akkor kivételt dobNOT_SUPPORTED
: A Spring felfüggeszti a futó tranzakciót, ha van ilyen, a kiadott utasításokat nem tranzakcionálisan lefuttatja, majd folytatja a tranzakció futását.REQUIRES_NEW
: A futó tranzakciót felfüggeszti és egy új tranzakcióban végrehajtja az utasításokat.NESTED
: Ha van futó tranzakció, akkor azt felfüggeszti, de egy SavePoint-ot is rak erre az állapotra, majd az új utasításokat futtatja egy tranzakcióban. Ha az új tranzakció elhasal akkor visszakerülünk a savepoint-ra. Ha nincs aktív tranzakció, akkor egyenértékű aREQUIRED
-el.
A TransactionStatus
a következőket adja meg:
1 2 3 4 5 6 7 8 |
|
Tranzakciók használata Spring-ben¶
Példa tranzakciók használatára:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
A fenti példában a @Transactional
annotációval látjuk el magát az osztály, mely által a Spring biztosítja, hogy maga a tranzakció rendelkezésre álljon még azelőtt, hogy bármelyik metódust is lefuttatnánk).
Ugyanezt az annotációt a metódusokra is alkalmazhatjuk, melyek által egy-egy tranzakciós utasítássorozat (a metódusban lévő utasítások halmaza) tulajdonságait adhatjuk meg (isolation, propgation, readOnly, timeout, stb).
Üres @Transactional
annotáció a metóduson így a következőt jelenti:
- Izoláció: DEFAULT
- Propagáció: REQUIRED
- Timeout: DEFAULT
- Mód: Olvasás-írás
TransactionTemplate¶
A fent bemutatott annotáció alapú tranzakciószabályozás csak egy lehetőség arra, hogy a finomhangoljuk a tranzakcióinkat (daklaratív módon).
Nézzünk egy példát, hogy mikor sülhet el rosszul egy tranzakció.
1 2 3 4 5 6 7 |
|
A fenti példában többféle műveletet végzünk.
Először adatbázishoz fordulunk aztán REST API hívást végzünk, majd ismét két adatbázis műveletet hajtunk végre.
A baj akkor történik, amikor a REST API hívás sok idő múlva ad csak választ.
Mivel Transactional
-el annotált a metódust, így a REST API hívás idején is nyitva marad a kapcsolat, melyet a Connection Pool-tól kapunk.
Ha a rendszerünket sokan használják, akkor gyorsan kifuthatunk a megengedett kapcsolatokból, ha egyszerre sok kérést kell kiszolgálnunk.
Fontos
Az adatbázis műveleteket más típusú input/output műveletekkel vegyíteni igencsak bad smell. Kerüljük annak használatát!!!
A TransactionTemplate
call-back alapú API-t biztosít a tranzakciók manuális menedzseléséhez.
1 2 3 4 5 6 7 8 |
|
Látható, hogy a TransactionTemplate
-nek megadhatjuk a tranzakció beállításait is (ha többféle konfigurációra van szükségünk, akkor csináljunk több TransactionTemplate
-t).
A TransactionTemplate
a TransactionManager
segítségével tud létrehozni, kommitolni és rollback-elni tranzakciókat.
A TransactionTemplate
kínál egy execute
nevű metódust, melynek segítségével bármilyen kódot futtathatunk egy tranzakcióban, mely utána valamilyen eredménnyel visszatér.
Példa:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
Az execute
egy TransactionCallback<T>
(interfész) típusú objektumot vár, melynek aztán meghívja a doInTransaction()
metódusát (a fenti példában lambdát használunk, mely alapján ezt nem láthatjuk).
Amennyiben hiba áll be akkor meghívhatjuk a transactionStatus.setRollbackOnly();
metódust, amely rollback-et idéz elő.
Amennyiben a tranzakció nem állít elő eredményt, akkor használhatjuk a TransactionCallbackWithoutResult
callback osztályt.