Verteilte Transaktionen im Frühjahr mit und ohne XA

Während es in Spring üblich ist, die Java-Transaktions-API und das XA-Protokoll für verteilte Transaktionen zu verwenden, haben Sie andere Möglichkeiten. Die optimale Implementierung hängt von den Arten der Ressourcen ab, die Ihre Anwendung verwendet, und von den Kompromissen, die Sie zwischen Leistung, Sicherheit, Zuverlässigkeit und Datenintegrität eingehen möchten. In dieser JavaWorld-Funktion führt Sie David Syer von SpringSource durch sieben Muster für verteilte Transaktionen in Spring-Anwendungen, drei davon mit XA und vier ohne. Level: Mittelstufe

Die Unterstützung des Spring Framework für die Java Transaction API (JTA) ermöglicht es Anwendungen, verteilte Transaktionen und das XA-Protokoll zu verwenden, ohne in einem Java EE-Container ausgeführt zu werden. Trotz dieser Unterstützung ist XA jedoch teuer und kann unzuverlässig oder umständlich in der Verwaltung sein. Es kann daher eine willkommene Überraschung sein, dass eine bestimmte Klasse von Anwendungen die Verwendung von XA insgesamt vermeiden kann.

Um Ihnen das Verständnis der Überlegungen zu verschiedenen Ansätzen für verteilte Transaktionen zu erleichtern, werde ich sieben Transaktionsverarbeitungsmuster analysieren und Codebeispiele bereitstellen, um sie konkret zu machen. Ich werde die Muster in umgekehrter Reihenfolge der Sicherheit oder Zuverlässigkeit präsentieren, beginnend mit denen mit der höchsten Garantie für Datenintegrität und Atomizität unter den allgemeinsten Umständen. Wenn Sie sich in der Liste nach unten bewegen, gelten weitere Einschränkungen und Einschränkungen. Die Muster sind auch ungefähr in umgekehrter Reihenfolge der Laufzeitkosten (beginnend mit den teuersten). Die Muster sind alle architektonisch oder technisch, im Gegensatz zu Geschäftsmustern. Daher konzentriere ich mich nicht auf den Geschäftsanwendungsfall, sondern nur auf die minimale Menge an Code, damit jedes Muster funktioniert.

Beachten Sie, dass nur die ersten drei Muster XA betreffen und diese aus Leistungsgründen möglicherweise nicht verfügbar oder akzeptabel sind. Ich diskutiere die XA-Muster nicht so ausführlich wie die anderen, weil sie an anderer Stelle behandelt werden, obwohl ich eine einfache Demonstration des ersten vorstelle. Wenn Sie diesen Artikel lesen, erfahren Sie, was Sie mit verteilten Transaktionen tun können und was nicht und wie und wann Sie die Verwendung von XA vermeiden sollten - und wann nicht.

Verteilte Transaktionen und Atomizität

Eine verteilte Transaktion umfasst mehr als eine Transaktionsressource. Beispiele für Transaktionsressourcen sind die Konnektoren für die Kommunikation mit relationalen Datenbanken und Messaging-Middleware. Oft sind solche eine Ressource hat eine API , die in etwa so aussieht begin(), rollback(), commit(). In der Java-Welt wird eine Transaktionsressource normalerweise als Produkt einer von der zugrunde liegenden Plattform bereitgestellten Factory angezeigt: Bei einer Datenbank handelt es sich um eine Connection(von DataSource) oder Java Persistence API (JPA) EntityManager; für Java Message Service (JMS) ist es a Session.

In einem typischen Beispiel löst eine JMS-Nachricht eine Datenbankaktualisierung aus. In eine Zeitleiste unterteilt, sieht eine erfolgreiche Interaktion ungefähr so ​​aus:

  1. Starten Sie die Messaging-Transaktion
  2. Erhalte Nachricht
  3. Starten Sie die Datenbanktransaktion
  4. Datenbank auf den neusten Stand bringen
  5. Datenbanktransaktion festschreiben
  6. Messaging-Transaktion festschreiben

Wenn beim Update ein Datenbankfehler wie eine Einschränkungsverletzung aufgetreten ist, sieht die gewünschte Reihenfolge folgendermaßen aus:

  1. Starten Sie die Messaging-Transaktion
  2. Erhalte Nachricht
  3. Starten Sie die Datenbanktransaktion
  4. Datenbank aktualisieren, fehlschlagen!
  5. Datenbanktransaktion zurücksetzen
  6. Rollback der Messaging-Transaktion

In diesem Fall kehrt die Nachricht nach dem letzten Rollback zur Middleware zurück und kehrt zu einem bestimmten Zeitpunkt zurück, um in einer anderen Transaktion empfangen zu werden. Dies ist normalerweise eine gute Sache, da Sie sonst möglicherweise nicht wissen, dass ein Fehler aufgetreten ist. (Mechanismen zur Behandlung automatischer Wiederholungs- und Bearbeitungsausnahmen fallen nicht in den Geltungsbereich dieses Artikels.)

Das wichtige Merkmal beider Zeitleisten ist, dass sie atomar sind und eine einzige logische Transaktion bilden, die entweder vollständig erfolgreich ist oder vollständig fehlschlägt.

Aber was garantiert, dass die Timeline wie eine dieser Sequenzen aussieht? Es muss eine gewisse Synchronisation zwischen den Transaktionsressourcen erfolgen, damit beide eine Festschreibung durchführen und umgekehrt. Andernfalls ist die gesamte Transaktion nicht atomar. Die Transaktion wird verteilt, da mehrere Ressourcen beteiligt sind, und ohne Synchronisierung ist sie nicht atomar. Die technischen und konzeptionellen Schwierigkeiten bei verteilten Transaktionen hängen alle mit der Synchronisation der Ressourcen (oder deren Fehlen) zusammen.

Die ersten drei unten diskutierten Muster basieren auf dem XA-Protokoll. Da diese Muster weit verbreitet sind, werde ich hier nicht näher darauf eingehen. Personen, die mit XA-Mustern vertraut sind, möchten möglicherweise mit dem Muster für gemeinsam genutzte Transaktionsressourcen fortfahren.

Volle XA mit 2PC

Wenn Sie nahezu kugelsichere Garantien benötigen, dass die Transaktionen Ihrer Anwendung nach einem Ausfall, einschließlich eines Serverabsturzes, wiederhergestellt werden, ist Full XA Ihre einzige Wahl. Die gemeinsam genutzte Ressource, die in diesem Fall zum Synchronisieren der Transaktion verwendet wird, ist ein spezieller Transaktionsmanager, der Informationen über den Prozess mithilfe des XA-Protokolls koordiniert. In Java wird das Protokoll aus Entwicklersicht über einen JTA verfügbar gemacht UserTransaction.

Als Systemschnittstelle ist XA eine Technologie, die die meisten Entwickler nie sehen. Sie müssen wissen, dass es da ist, was es ermöglicht, was es kostet und welche Auswirkungen es auf die Verwendung von Transaktionsressourcen hat. Die Kosten stammen aus dem 2PC-Protokoll (Two-Phase Commit), mit dem der Transaktionsmanager sicherstellt, dass sich alle Ressourcen vor dem Ende einer Transaktion auf das Ergebnis einer Transaktion einigen.

Wenn die Anwendung Spring-fähig ist, verwendet sie die JtaTransactionManagerdeklarative Transaktionsverwaltung von Spring und Spring, um die Details der zugrunde liegenden Synchronisation auszublenden. Der Unterschied für den Entwickler zwischen der Verwendung von XA und der Nichtverwendung von XA besteht in der Konfiguration der Factory-Ressourcen: der DataSourceInstanzen und des Transaktionsmanagers für die Anwendung. Dieser Artikel enthält eine Beispielanwendung (das atomikos-dbProjekt), die diese Konfiguration veranschaulicht. Die DataSourceInstanzen und der Transaktionsmanager sind die einzigen XA- oder JTA-spezifischen Elemente der Anwendung.

Führen Sie die Komponententests unter aus, um zu sehen, wie das Beispiel funktioniert com.springsource.open.db. Eine einfache MulipleDataSourceTestsKlasse fügt nur Daten in zwei Datenquellen ein und verwendet dann die Funktionen zur Unterstützung der Spring-Integration, um die Transaktion zurückzusetzen, wie in Listing 1 gezeigt:

Listing 1. Transaktions-Rollback

@Transactional @Test public void testInsertIntoTwoDataSources() throws Exception { int count = getJdbcTemplate().update( "INSERT into T_FOOS (id,name,foo_date) values (?,?,null)", 0, "foo"); assertEquals(1, count); count = getOtherJdbcTemplate() .update( "INSERT into T_AUDITS (id,operation,name,audit_date) values (?,?,?,?)", 0, "INSERT", "foo", new Date()); assertEquals(1, count); // Changes will roll back after this method exits }

Dann MulipleDataSourceTestsverifiziert , dass die beiden Operationen sowohl zurückgerollt wurden, wie in Listing 2 gezeigt:

Listing 2. Überprüfen des Rollbacks

@AfterTransaction public void checkPostConditions() { int count = getJdbcTemplate().queryForInt("select count(*) from T_FOOS"); // This change was rolled back by the test framework assertEquals(0, count); count = getOtherJdbcTemplate().queryForInt("select count(*) from T_AUDITS"); // This rolled back as well because of the XA assertEquals(0, count); }

Weitere Informationen zur Funktionsweise der Spring-Transaktionsverwaltung und zur allgemeinen Konfiguration finden Sie im Spring-Referenzhandbuch.

XA mit 1PC-Optimierung

Dieses Muster ist eine Optimierung, die viele Transaktionsmanager verwenden, um den Overhead von 2PC zu vermeiden, wenn die Transaktion eine einzelne Ressource enthält. Sie würden erwarten, dass Ihr Anwendungsserver dies herausfinden kann.

XA und das letzte Ressourcengambit

Ein weiteres Merkmal vieler XA-Transaktionsmanager ist, dass sie immer noch dieselben Wiederherstellungsgarantien bieten können, wenn alle bis auf eine Ressource XA-fähig sind, wie sie es können, wenn sie alle sind. Sie tun dies, indem sie die Ressourcen bestellen und die Nicht-XA-Ressource als Stimmabgabe verwenden. Wenn das Festschreiben fehlschlägt, können alle anderen Ressourcen zurückgesetzt werden. Es ist nahezu 100 Prozent kugelsicher - aber nicht ganz so. Und wenn es fehlschlägt, schlägt es fehl, ohne große Spuren zu hinterlassen, es sei denn, es werden zusätzliche Schritte unternommen (wie dies bei einigen Top-End-Implementierungen der Fall ist).

Muster für gemeinsam genutzte Transaktionsressourcen

Ein gutes Muster für die Verringerung der Komplexität und die Erhöhung des Durchsatzes in einigen Systemen besteht darin, die Notwendigkeit von XA insgesamt zu beseitigen, indem sichergestellt wird, dass alle Transaktionsressourcen im System tatsächlich von derselben Ressource unterstützt werden. Dies ist eindeutig nicht in allen Anwendungsfällen der Verarbeitung möglich, aber es ist genauso solide wie XA und normalerweise viel schneller. Das Muster für gemeinsam genutzte Transaktionsressourcen ist kugelsicher, jedoch spezifisch für bestimmte Plattformen und Verarbeitungsszenarien.

Ein einfaches und bekanntes (zu vielen) Beispiel für dieses Muster ist die gemeinsame Nutzung einer Datenbank Connectionzwischen einer Komponente, die ORM (Object Relational Mapping) verwendet, und einer Komponente, die JDBC verwendet. In diesem Fall verwenden Sie die Spring-Transaktionsmanager, die die ORM-Tools wie Hibernate, EclipseLink und die Java Persistence API (JPA) unterstützen. Dieselbe Transaktion kann sicher für ORM- und JDBC-Komponenten verwendet werden, normalerweise von oben durch eine Ausführung auf Service-Level-Methode, bei der die Transaktion gesteuert wird.

Eine weitere effektive Verwendung dieses Musters ist der Fall der nachrichtengesteuerten Aktualisierung einer einzelnen Datenbank (wie im einfachen Beispiel in der Einführung dieses Artikels). Messaging-Middleware-Systeme müssen ihre Daten irgendwo speichern, häufig in einer relationalen Datenbank. Um dieses Muster zu implementieren, muss das Messagingsystem lediglich auf dieselbe Datenbank verweisen, in die die Geschäftsdaten eingegeben werden. Dieses Muster basiert darauf, dass der Anbieter der Messaging-Middleware die Details seiner Speicherstrategie offenlegt, sodass er so konfiguriert werden kann, dass er auf dieselbe Datenbank verweist und sich an dieselbe Transaktion anschließt.

Nicht alle Anbieter machen dies einfach. Eine Alternative, die für fast jede Datenbank funktioniert, besteht darin, Apache ActiveMQ für Messaging zu verwenden und eine Speicherstrategie in den Nachrichtenbroker einzufügen. Dies ist ziemlich einfach zu konfigurieren, sobald Sie den Trick kennen. Dies wird im Beispielprojekt dieses Artikels demonstriert shared-jms-db. Der Anwendungscode (in diesem Fall Komponententests) muss nicht wissen, dass dieses Muster verwendet wird, da in der Spring-Konfiguration alles deklarativ aktiviert ist.

Ein Unit-Test in der aufgerufenen Stichprobe bestätigt, SynchronousMessageTriggerAndRollbackTestsdass alles mit synchronem Nachrichtenempfang funktioniert. Die testReceiveMessageUpdateDatabaseMethode empfängt zwei Nachrichten und verwendet sie, um zwei Datensätze in die Datenbank einzufügen. Wenn diese Methode beendet wird, setzt das Testframework die Transaktion zurück, sodass Sie überprüfen können, ob sowohl die Nachrichten als auch die Datenbankaktualisierungen zurückgesetzt werden, wie in Listing 3 gezeigt:

Listing 3. Überprüfen des Rollbacks von Nachrichten und Datenbankaktualisierungen

@AfterTransaction public void checkPostConditions() { assertEquals(0, SimpleJdbcTestUtils.countRowsInTable(jdbcTemplate, "T_FOOS")); List list = getMessages(); assertEquals(2, list.size()); }

Die wichtigsten Merkmale der Konfiguration sind die ActiveMQ-Persistenzstrategie, bei der das Messagingsystem mit DataSourceden Geschäftsdaten verknüpft wird , und das Flag auf dem Spring JmsTemplate, mit dem die Nachrichten empfangen werden. Listing 4 zeigt, wie Sie die ActiveMQ-Persistenzstrategie konfigurieren:

Listing 4. Konfigurieren der ActiveMQ-Persistenz

    ...             

Listing 5 zeigt die Flagge auf der Feder JmsTemplate, die zum Empfangen der Nachrichten verwendet wird:

Listing 5. Einrichten der JmsTemplatefür den Transaktionsgebrauch

 ...   

Ohne sessionTransacted=truewerden die API-Aufrufe der JMS-Sitzungstransaktion niemals ausgeführt, und der Nachrichtenempfang kann nicht zurückgesetzt werden. Die wichtigsten Bestandteile hierbei sind der eingebettete Broker mit einem speziellen async=falseParameter und ein Wrapper, der DataSourcezusammen sicherstellt, dass ActiveMQ denselben Transaktions-JDBC Connectionwie Spring verwendet.