Inleiding tot transacties in Java en Spring

1. Inleiding

In deze tutorial zullen we begrijpen wat wordt bedoeld met transacties in Java. Daardoor zullen we begrijpen hoe we lokale resourcetransacties en wereldwijde transacties kunnen uitvoeren. Dit stelt ons ook in staat om verschillende manieren te onderzoeken om transacties in Java en Spring te beheren.

2. Wat is een transactie?

Transacties in Java, zoals in het algemeen, verwijzen naar een reeks acties die allemaal met succes moeten worden voltooid. Vandaar, als een of meer acties mislukken, moeten alle andere acties worden beëindigd, waarbij de status van de toepassing ongewijzigd blijft. Dit is nodig om ervoor te zorgen dat de integriteit van de applicatiestatus nooit wordt aangetast.

Bij deze transacties kunnen ook een of meer bronnen betrokken zijn, zoals een database, berichtenwachtrij, wat aanleiding geeft tot verschillende manieren om acties onder een transactie uit te voeren. Deze omvatten het uitvoeren van lokale resourcetransacties met individuele resources. Als alternatief kunnen meerdere bronnen deelnemen aan een wereldwijde transactie.

3. Resource lokale transacties

We zullen eerst onderzoeken hoe we transacties in Java kunnen gebruiken terwijl we met individuele bronnen werken. Hier hebben we misschien meerdere individuele acties die we uitvoeren met een bron zoals een database. Maar we willen misschien dat ze gebeuren als een verenigd geheel, als in een ondeelbare werkeenheid. Met andere woorden, we willen dat deze acties plaatsvinden onder één enkele transactie.

In Java hebben we verschillende manieren om toegang te krijgen tot en te werken met een bron zoals een database. Daarom is de manier waarop we met transacties omgaan ook niet hetzelfde. In deze sectie zullen we zien hoe we transacties kunnen gebruiken met sommige van deze bibliotheken in Java, die vrij vaak worden gebruikt.

3.1. JDBC

Java Database Connectivity (JDBC) is de API in Java die bepaalt hoe databases in Java kunnen worden benaderd. Verschillende databaseleveranciers bieden JDBC-stuurprogramma's om op een leverancieronafhankelijke manier verbinding te maken met de database. Dus halen we een Verbinding van een stuurprogramma om verschillende bewerkingen op de database uit te voeren:

JDBC biedt ons de mogelijkheid om verklaringen onder een transactie uit te voeren. De standaardgedrag van een Verbinding is automatisch vastleggen. Ter verduidelijking: dit betekent dat elk afschrift als een transactie wordt behandeld en direct na uitvoering automatisch wordt vastgelegd.

Willen we echter meerdere afschriften in één transactie bundelen, dan is dat ook mogelijk om:

Verbindingsverbinding = DriverManager.getConnection (CONNECTION_URL, GEBRUIKER, WACHTWOORD); probeer {connection.setAutoCommit (false); PreparedStatement firstStatement = verbinding .prepareStatement ("firstQuery"); firstStatement.executeUpdate (); PreparedStatement secondStatement = verbinding .prepareStatement ("secondQuery"); secondStatement.executeUpdate (); connection.commit (); } catch (uitzondering e) {connection.rollback (); }

Hier hebben we de automatische vastleggingsmodus van Verbinding. Daarom kunnen we definieer handmatig de transactiegrens en voer een plegen of terugrollen. JDBC stelt ons ook in staat om een Bewaarpunt dat geeft ons meer controle over hoeveel we terugdraaien.

3.2. JPA

Java Persistence API (JPA) is een specificatie in Java die kan worden gebruikt voor overbruggen van de kloof tussen objectgeoriënteerde domeinmodellen en relationele databasesystemen. Er zijn dus verschillende implementaties van JPA beschikbaar van derden, zoals Hibernate, EclipseLink en iBatis.

In JPA kunnen we reguliere klassen definiëren als een Entiteit dat geeft hen een blijvende identiteit. De EntityManager class biedt de nodige interface om met meerdere entiteiten te werken binnen een persistentiecontext. De persistentiecontext kan worden gezien als een cache op het eerste niveau waar entiteiten worden beheerd:

De persistentiecontext kan hier van twee typen zijn: transactiebereik of uitgebreid bereik. Een persistentiecontext op basis van een transactie is gebonden aan één transactie. Hoewel de persistentiecontext met een uitgebreid bereik zich over meerdere transacties kan uitstrekken. De standaardbereik van een persistentiecontext is transactiebereik.

Laten we eens kijken hoe we een EntityManager en handmatig een transactiegrens definiëren:

EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory ("jpa-voorbeeld"); EntityManager entityManager = entityManagerFactory.createEntityManager (); probeer {entityManager.getTransaction (). begin (); entiteitManager.persist (firstEntity); entiteitManager.persist (secondEntity); entiteitManager.getTransaction (). commit (); } catch (Exceotion e) {entityManager.getTransaction (). rollback (); }

Hier maken we een EntityManager van EntityManagerFactory binnen de context van een persistentiecontext op basis van een transactie. Vervolgens definiëren we de transactiegrens met beginnen, begaan, en terugrollen methoden.

3.3. JMS

Java Messaging Service (JMS) is een specificatie in Java die stelt toepassingen in staat om asynchroon te communiceren met behulp van berichten. Met de API kunnen we berichten maken, verzenden, ontvangen en lezen vanuit een wachtrij of onderwerp. Er zijn verschillende berichtenservices die voldoen aan de JMS-specificaties, waaronder OpenMQ en ActiveMQ.

De JMS API ondersteunt het bundelen van meerdere verzend- of ontvangstbewerkingen in één transactie. Door de aard van op berichten gebaseerde integratiearchitectuur, productie en consumptie van een bericht kunnen geen deel uitmaken van dezelfde transactie. Het bereik van de transactie blijft tussen de klant en de JMS-provider:

JMS stelt ons in staat om een Sessie van een Verbinding die we verkrijgen van een leverancierspecifieke ConnectionFactory. We hebben een optie om een Sessie dat is afgehandeld of niet. Voor niet-transactie Sessies, we kunnen ook verder een geschikte bevestigingsmodus definiëren.

Laten we eens kijken hoe we een transactie kunnen maken Sessie om meerdere berichten te verzenden onder een transactie:

ActiveMQConnectionFactory connectionFactory = nieuwe ActiveMQConnectionFactory (CONNECTION_URL); Verbindingsverbinding = = connectionFactory.createConnection (); connection.start (); probeer {Session session = connection.createSession (true, 0); Bestemming = bestemming = session.createTopic ("TEST.FOO"); MessageProducer producer = session.createProducer (bestemming); producer.send (firstMessage); producer.send (secondMessage); session.commit (); } catch (uitzondering e) {session.rollback (); }

Hier maken we een Bericht Producent voor de Bestemming van het type onderwerp. We krijgen de Bestemming van de Sessie we hebben eerder gemaakt. We gebruiken verder Sessie om transactiegrenzen te definiëren met behulp van de methoden plegen en terugrollen.

4. Wereldwijde transacties

Zoals we zagen, stellen lokale resource-transacties ons in staat om meerdere bewerkingen uit te voeren binnen een enkele resource als een verenigd geheel. Maar heel vaak hebben we te maken met bewerkingen die zich uitstrekken over meerdere bronnen. Bijvoorbeeld werking in twee verschillende databases of een database en een berichtenwachtrij. Hier zal lokale transactieondersteuning binnen middelen voor ons niet voldoende zijn.

Wat we nodig hebben in deze scenario's is een wereldwijd mechanisme om transacties af te bakenen over meerdere deelnemende bronnen. Dit staat vaak bekend als gedistribueerde transacties en er zijn specificaties voorgesteld om hiermee effectief om te gaan.

De XA-specificatie is zo'n specificatie die een transactiebeheerder definieert om transacties over meerdere bronnen te beheren. Java heeft een vrij volwassen ondersteuning voor gedistribueerde transacties die voldoen aan de XA-specificatie via de componenten JTA en JTS.

4.1. JTA

Java Transaction API (JTA) is een Java Enterprise Edition API die is ontwikkeld onder het Java Community Process. Het stelt Java-applicaties en applicatieservers in staat om gedistribueerde transacties over XA-bronnen uit te voeren. JTA is gemodelleerd rond de XA-architectuur, waarbij gebruik wordt gemaakt van committering in twee fasen.

JTA specificeert standaard Java-interfaces tussen een transactiebeheerder en de andere partijen in een gedistribueerde transactie:

Laten we enkele van de belangrijkste interfaces begrijpen die hierboven zijn gemarkeerd:

  • TransactionManager: Een interface waarmee een applicatieserver transacties kan afbakenen en controleren
  • UserTransaction: Met deze interface kan een applicatieprogramma transacties expliciet afbakenen en controleren
  • XAResource: Het doel van deze interface is om een ​​transactiebeheerder in staat te stellen samen te werken met resourcemanagers voor XA-compatibele resources

4.2. JTS

Java Transaction Service (JTS) is een specificatie voor het bouwen van de transactiebeheerder die overeenkomt met de OMG OTS-specificatie. JTS gebruikt de standaard CORBA ORB / TS-interfaces en Internet Inter-ORB Protocol (IIOP) voor de overdracht van transactiecontext tussen JTS-transactiebeheerders.

Op hoog niveau ondersteunt het de Java Transaction API (JTA). Een JTS-transactiemanager levert transactiediensten aan de partijen die betrokken zijn bij een gedistribueerde transactie:

Services die JTS aan een applicatie levert, zijn grotendeels transparant en daarom merken we ze misschien niet eens in de applicatiearchitectuur. JTS is opgebouwd rond een applicatieserver die alle transactiesemantiek van de applicatieprogramma's abstraheert.

5. JTA-transactiebeheer

Nu is het tijd om te begrijpen hoe we een gedistribueerde transactie kunnen beheren met JTA. Gedistribueerde transacties zijn geen triviale oplossingen en hebben daarom ook gevolgen voor de kosten. Bovendien, er zijn meerdere opties waaruit we kunnen kiezen om JTA in onze applicatie op te nemen. Daarom moet onze keuze liggen in het licht van de algehele applicatiearchitectuur en ambities.

5.1. JTA in toepassingsserver

Zoals we eerder hebben gezien, vertrouwt JTA-architectuur op de applicatieserver om een ​​aantal transactiegerelateerde bewerkingen te vergemakkelijken. Een van de belangrijkste services waarvan de server afhankelijk is, is een naamgevingsservice via JNDI. Dit is waar XA-bronnen, zoals gegevensbronnen, aan zijn gebonden en waaruit ze worden opgehaald.

Afgezien hiervan hebben we een keuze in termen van hoe we de transactiegrens in onze applicatie willen beheren. Dit geeft aanleiding tot twee soorten transacties binnen de Java-applicatieserver:

  • Door containers beheerde transactie: Zoals de naam al doet vermoeden, hier wordt de transactiegrens bepaald door de applicatieserver. Dit vereenvoudigt de ontwikkeling van Enterprise Java Beans (EJB), aangezien het geen verklaringen bevat met betrekking tot transactieafspraken en hiervoor uitsluitend afhankelijk is van de container. Dit biedt echter onvoldoende flexibiliteit voor de toepassing.
  • Door bonen beheerde transactie: In tegenstelling tot de door een container beheerde transactie, in een door bonen beheerde transactie EJB's bevatten de expliciete instructies om de transactieafbakening te definiëren. Dit geeft de toepassing nauwkeurige controle bij het markeren van de grenzen van de transactie, zij het ten koste van meer complexiteit.

Een van de belangrijkste nadelen van het uitvoeren van transacties in de context van een applicatieserver is dat de applicatie wordt nauw verbonden met de server. Dit heeft gevolgen voor de testbaarheid, beheersbaarheid en portabiliteit van de applicatie. Dit is dieper in de microservicearchitectuur waar de nadruk meer ligt op het ontwikkelen van serverneutrale applicaties.

5.2. JTA zelfstandig

De problemen die we in de vorige sectie bespraken, hebben een enorme impuls gegeven aan het creëren oplossingen voor gedistribueerde transacties die niet afhankelijk zijn van een applicatieserver. We hebben in dit verband verschillende opties, zoals het gebruik van transactieondersteuning met Spring of het gebruik van een transactiebeheerder zoals Atomikos.

Laten we eens kijken hoe we een transactiebeheerder zoals Atomikos kunnen gebruiken om een ​​gedistribueerde transactie met een database en een berichtenwachtrij te vergemakkelijken. Een van de belangrijkste aspecten van een gedistribueerde transactie is het in- en uitschakelen van de deelnemende middelen bij de transactiemonitor. Atomikos regelt dit voor ons. Het enige wat we hoeven te doen is door Atomikos geleverde abstracties te gebruiken:

AtomikosDataSourceBean atomikosDataSourceBean = nieuwe AtomikosDataSourceBean (); atomikosDataSourceBean.setXaDataSourceClassName ("com.mysql.cj.jdbc.MysqlXADataSource"); DataSource dataSource = atomikosDataSourceBean;

Hier maken we een instantie van AtomikosDataSourceBean en het registreren van de leverancierspecifieke XADataSource. Vanaf hier kunnen we dit blijven gebruiken zoals elk ander Databron en profiteer van de voordelen van gedistribueerde transacties.

Evenzo wij hebben een abstractie voor berichtenwachtrij die ervoor zorgt dat de leverancierspecifieke XA-bron automatisch bij de transactiemonitor wordt geregistreerd:

AtomikosConnectionFactoryBean atomikosConnectionFactoryBean = nieuwe AtomikosConnectionFactoryBean (); atomikosConnectionFactoryBean.setXaConnectionFactory (nieuwe ActiveMQXAConnectionFactory ()); ConnectionFactory connectionFactory = atomikosConnectionFactoryBean;

Hier maken we een instantie van AtomikosConnectionFactoryBean en het registreren van het XAConnectionFactory van een XA-enabled JMS-leverancier. Hierna kunnen we dit als een vaste klant blijven gebruiken ConnectionFactory.

Nu biedt Atomikos ons het laatste stukje van de puzzel om alles bij elkaar te brengen, een voorbeeld van UserTransaction:

UserTransaction userTransaction = nieuwe UserTransactionImp ();

Nu zijn we klaar om een ​​applicatie te maken met gedistribueerde transacties die zich uitstrekken over onze database en de berichtenwachtrij:

probeer {userTransaction.begin (); java.sql.Connection dbConnection = dataSource.getConnection (); PreparedStatement voorbereideStatement = dbConnection.prepareStatement (SQL_INSERT); voorbereideStatement.executeUpdate (); javax.jms.Connection mbConnection = connectionFactory.createConnection (); Sessiesessie = mbConnection.createSession (true, 0); Bestemmingsbestemming = session.createTopic ("TEST.FOO"); MessageProducer producer = session.createProducer (bestemming); producer.send (BERICHT); userTransaction.commit (); } catch (uitzondering e) {userTransaction.rollback (); }

Hier gebruiken we de methoden beginnen en plegen in de klas UserTransaction om de transactiegrens af te bakenen. Dit omvat het opslaan van een record in de database en het publiceren van een bericht in de berichtenwachtrij.

6. Transactieondersteuning in het voorjaar

Dat hebben we gezien Het afhandelen van transacties is nogal een ingewikkelde taak die veel standaardcodering omvat en configuraties. Bovendien heeft elke resource zijn eigen manier om lokale transacties af te handelen. In Java abstraheert JTA ons van deze variaties, maar brengt verder providerspecifieke details en de complexiteit van de applicatieserver.

Lente platform biedt ons een veel schonere manier om transacties af te handelen, zowel lokale als wereldwijde transacties in Java. Dit samen met de andere voordelen van Spring creëert een overtuigende reden om Spring te gebruiken om transacties af te handelen. Bovendien is het vrij eenvoudig om een ​​transactiebeheerder met Spring te configureren en om te schakelen, die door de server of stand-alone kan worden geleverd.

De lente biedt ons dit naadloze abstractie door een proxy voor de methoden te maken met transactiecode. De proxy beheert de transactiestatus namens de code met behulp van TransactionManager:

De centrale interface hier is PlatformTransactionManager die een aantal verschillende implementaties heeft. Het biedt abstracties over JDBC (DataSource), JMS, JPA, JTA en vele andere bronnen.

6.1. Configuraties

Laten we eens kijken hoe we kunnen configureren Spring om Atomikos te gebruiken als transactiebeheerder en transactionele ondersteuning te bieden voor JPA en JMS. We beginnen met het definiëren van een PlatformTransactionManager van het type JTA:

@Bean openbaar PlatformTransactionManager platformTransactionManager () gooit Throwable {retourneer nieuwe JtaTransactionManager (userTransaction (), transactionManager ()); }

Hier geven we voorbeelden van UserTransaction en TransactionManager naar JTATransactionManager. Deze instanties worden geleverd door een transactiebeheerbibliotheek zoals Atomikos:

@Bean openbare UserTransaction userTransaction () {retourneer nieuwe UserTransactionImp (); } @Bean (initMethod = "init", destroyMethod = "close") openbare TransactionManager transactionManager () {retourneer nieuwe UserTransactionManager (); }

De klassen UserTransactionImp en UserTransactionManager worden hier door Atomikos geleverd.

Verder moeten we de JmsTemplete welke de kernklasse die synchrone JMS-toegang toestaat in het voorjaar:

@Bean openbare JmsTemplate jmsTemplate () gooit Throwable {retourneer nieuwe JmsTemplate (connectionFactory ()); }

Hier, ConnectionFactory wordt geleverd door Atomikos waar het gedistribueerde transacties mogelijk maakt Verbinding verstrekt door:

@Bean (initMethod = "init", destroyMethod = "close") openbare ConnectionFactory connectionFactory () {ActiveMQXAConnectionFactory activeMQXAConnectionFactory = nieuwe ActiveMQXAConnectionFactory (); activeMQXAConnectionFactory.setBrokerURL ("tcp: // localhost: 61616"); AtomikosConnectionFactoryBean atomikosConnectionFactoryBean = nieuwe AtomikosConnectionFactoryBean (); atomikosConnectionFactoryBean.setUniqueResourceName ("xamq"); atomikosConnectionFactoryBean.setLocalTransactionMode (false); atomikosConnectionFactoryBean.setXaConnectionFactory (activeMQXAConnectionFactory); retourneer atomikosConnectionFactoryBean; }

Dus, zoals we kunnen zien, pakken we hier een JMS-providerspecifiek in XAConnectionFactory met AtomikosConnectionFactoryBean.

Vervolgens moeten we een AbstractEntityManagerFactoryBean dat is verantwoordelijk voor het creëren van JPA EntityManagerFactory boon in het voorjaar:

@Bean openbaar LocalContainerEntityManagerFactoryBean entityManager () gooit SQLException {LocalContainerEntityManagerFactoryBean entityManager = new LocalContainerEntityManagerFactoryBean (); entiteitManager.setDataSource (dataSource ()); Eigenschappen properties = nieuwe Eigenschappen (); properties.setProperty ("javax.persistence.transactionType", "jta"); entiteitManager.setJpaProperties (eigenschappen); terugkeer entityManager; }

Net als voorheen, de Databron die we in de LocalContainerEntityManagerFactoryBean hier wordt geleverd door Atomikos met gedistribueerde transacties ingeschakeld:

@Bean (initMethod = "init", destroyMethod = "close") openbare DataSource dataSource () gooit SQLException {MysqlXADataSource mysqlXaDataSource = nieuwe MysqlXADataSource (); mysqlXaDataSource.setUrl ("jdbc: mysql: //127.0.0.1: 3306 / test"); AtomikosDataSourceBean xaDataSource = nieuwe AtomikosDataSourceBean (); xaDataSource.setXaDataSource (mysqlXaDataSource); xaDataSource.setUniqueResourceName ("xads"); retourneer xaDataSource; }

Ook hier verpakken we de providerspecifieke XADataSource in AtomikosDataSourceBean.

6.2. Transactiebeheer

Nadat we alle configuraties in de laatste sectie hebben doorlopen, moeten we ons behoorlijk overweldigd voelen! We kunnen zelfs de voordelen van het gebruik van Spring in twijfel trekken. Maar onthoud dat al deze configuratie heeft stelde ons in staat om te abstraheren van de meeste providerspecifieke boilerplate en onze eigenlijke applicatiecode hoeft daar helemaal niet van op de hoogte te zijn.

Dus nu zijn we klaar om te onderzoeken hoe we transacties in het voorjaar kunnen gebruiken, waarbij we de database willen bijwerken en berichten willen publiceren. De lente biedt ons twee manieren om dit te bereiken met hun eigen voordelen om uit te kiezen. Laten we eens kijken hoe we ze kunnen gebruiken:

  • Declaratieve ondersteuning

De eenvoudigste manier om transacties in Spring te gebruiken, is met declaratieve ondersteuning. Hier hebben we een handige annotatie die bij de methode of zelfs in de klas kan worden toegepast. Dit maakt eenvoudig een wereldwijde transactie voor onze code mogelijk:

@PersistenceContext EntityManager entityManager; @Autowired JmsTemplate jmsTemplate; @Transactional (propagation = Propagation.REQUIRED) openbaar ongeldig proces (ENTITY, MESSAGE) {entityManager.persist (ENTITY); jmsTemplate.convertAndSend (BESTEMMING, BERICHT); }

De bovenstaande eenvoudige code is voldoende om een ​​opslagbewerking in de database en een publicatiebewerking in de berichtenwachtrij binnen een JTA-transactie mogelijk te maken.

  • Programmatische ondersteuning

Hoewel de declaratieve ondersteuning vrij elegant en eenvoudig is, biedt deze ons niet de voordeel van het nauwkeuriger beheersen van de transactiegrens. Dus als we een bepaalde behoefte hebben om dat te bereiken, biedt Spring programmatische ondersteuning om de transactiegrens af te bakenen:

@Autowired privé PlatformTransactionManager transactionManager; openbaar ongeldig proces (ENTITY, MESSAGE) {TransactionTemplate transactionTemplate = nieuwe TransactionTemplate (transactionManager); transactionTemplate.executeWithoutResult (status -> {entityManager.persist (ENTITY); jmsTemplate.convertAndSend (DESTINATION, MESSAGE);}); }

Dus, zoals we kunnen zien, moeten we een Transactietemplate met de beschikbare PlatformTransactionManager. Dan kunnen we de Transactie voltooid om een ​​aantal verklaringen binnen een globale transactie te verwerken.

7. Nabeschouwing

Zoals we hebben gezien, is het afhandelen van transacties, met name transacties die zich over meerdere bronnen uitstrekken, complex. Bovendien, transacties zijn inherent blokkerend, wat nadelig is voor latentie en doorvoer van een applicatie. Verder is het testen en onderhouden van code met gedistribueerde transacties niet eenvoudig, vooral als de transactie afhankelijk is van de onderliggende applicatieserver. Al met al is het dus het beste om transacties überhaupt te vermijden als we kunnen!

Maar dat is verre van werkelijkheid. Kortom, in real-world applicaties hebben we vaak een legitieme behoefte aan transacties. Hoewel het mogelijk om de applicatie-architectuur te heroverwegen zonder transacties, is het misschien niet altijd mogelijk. Daarom moeten we bepaalde best practices toepassen bij het werken met transacties in Java om onze applicaties te verbeteren:

  • Een van de fundamentele verschuivingen die we moeten doorvoeren, is die van gebruik zelfstandige transactiebeheerders in plaats van die van een applicatieserver. Dit alleen al kan onze applicatie enorm vereenvoudigen. Bovendien is het zeer geschikt voor cloud-native microservice-architectuur.
  • Verder, een abstractielaag als Spring kan ons helpen de directe impact van aanbieders in te dammen zoals JPA- of JTA-providers. Dit kan ons dus in staat stellen om tussen providers te schakelen zonder veel impact op onze bedrijfslogica. Bovendien neemt het de lagere verantwoordelijkheden voor het beheren van de transactiestatus van ons weg.
  • Ten slotte zouden we dat moeten zijn voorzichtig bij het kiezen van de transactiegrens in onze code. Omdat transacties blokkeren, is het altijd beter om de transactiegrens zo beperkt mogelijk te houden. Indien nodig zouden we programmatisch moeten verkiezen boven declaratieve controle voor transacties.

8. Conclusie

Samenvattend hebben we in deze tutorial transacties in de context van Java besproken. We hebben ondersteuning voor individuele resource-lokale transacties in Java voor verschillende resources doorlopen. We hebben ook de manieren doorgenomen om wereldwijde transacties in Java tot stand te brengen.

Verder hebben we verschillende manieren doorlopen om wereldwijde transacties in Java te beheren. We begrepen ook hoe Spring het voor ons gemakkelijker maakt om transacties in Java te gebruiken.

Ten slotte hebben we enkele best practices doorgenomen bij het werken met transacties in Java.