Senden von Ereignissen am Ende einer Transaktion

8

Ich habe eine Schnittstelle für ein Service-Objekt, das in etwa wie folgt aussieht (vereinfacht dargestellt):

%Vor%

ItemService eine einzelne Implementierung und ist als Spring Bean verkabelt. Es wird vom UI-Teil unseres Projekts und vom Code, der Ajax-Anfragen behandelt, verwendet, um Item -Objekte in unserem Datenspeicher zu erstellen und zu ändern.

Unter der Haube sendet jede Methode eine Reihe von Ereignissen, wenn sie aufgerufen wird. Die Ereignisse werden von anderen Modulen empfangen, um beispielsweise Lucene-Indizes auf dem neuesten Stand zu halten oder Nachrichten an Administratoren zu senden, damit sie wissen, dass sich etwas geändert hat. Jeder Methodenaufruf stellt im Frühjahr eine einzelne Transaktion dar (mit org.springframework.orm.hibernate3.HibernateTransactionManager und org.springframework.transaction.interceptor.TransactionProxyFactoryBean ).

Kürzlich bestand die Notwendigkeit, eine Reihe von Methodenaufrufen in einer einzigen Transaktion zusammenzustellen . Manchmal mit mehr als einem Service. Zum Beispiel möchten wir vielleicht etwas tun wie:

%Vor%

Wir haben dies getan, indem wir einen anderen Dienst erstellt haben, mit dem Sie Aufrufe von Diensten in einer einzigen Methode im übergeordneten Dienst zusammenstellen können. Lass es uns ComposingService nennen. ComposingService wird wie alle anderen auch von Spring verwaltet, und da Transaktionen reentrant sind, sollte dies alles funktionieren.

Es gibt jedoch ein Problem: Wenn einer dieser Vorgänge in der Transaktion fehlschlägt und die Transaktion zurückgesetzt wird, möchten wir keinerlei Ereignisse senden .

Wenn die Transaktion nach der Hälfte der Transaktion fehlschlägt, wird die Hälfte der Ereignisse von ItemService gesendet, bevor die Transaktion rückgängig gemacht wird. Das bedeutet, dass einige Module eine Reihe von Ereignissen für Dinge erhalten, die noch nicht geschehen sind.

Wir versuchen, einen Weg zu finden, dies zu beheben, aber wir konnten an nichts Elegantes denken. Das Beste, was wir bis jetzt erreicht haben, ist so etwas (und es ist hässlich):

%Vor%

In diesem geänderten ItemService werden die Ereignisse, die sofort gesendet werden, zur Liste der Ereignisse hinzugefügt, die als Argument übergeben werden. Die Liste wird von ComposingService verwaltet, und die Ereignisse werden von ComposingService gesendet, sobald alle Aufrufe von ItemService und andere Dienste erfolgreich beendet wurden.

Offensichtlich besteht das Problem darin, dass wir den Vertrag für ItemService auf hässliche Weise geändert haben. Das Aufrufen von Klassen, selbst wenn es sich um Dienste handelt, sollte sich nicht um die Verwaltung von Ereignissen kümmern müssen. Aber ich bin nicht in der Lage, darüber nachzudenken, daher diese Frage.

Das sieht nach der Art von Problem aus, das wahrscheinlich vorher gelöst wurde. Hat jemand ein Problem, das ähnlich aussieht, und wenn ja, wie haben Sie es gelöst?

    
Jon 12.08.2011, 11:19
quelle

3 Antworten

20

Zusammenfassung Ihrer Frage: Sie suchen nach einer transaktionssicheren Methode zum Senden von Nachrichten.

Option 1: JMS

Transaktionssichere Nachrichten sind genau das, wofür JMS ist. Im Frühjahr gibt es auch eine gute JMS-Integration. Weitere Informationen finden Sie im JMS-Kapitel in der Frühjahrsdokumentation .

Dadurch wird sichergestellt, dass die Nachrichten gesendet werden genau dann, wenn die Transaktion festgeschrieben ist. Es hilft auch, mit Fehlern in Listenern für diese Ereignisse umzugehen.

Ein Unterschied zu Ihrer aktuellen Einrichtung besteht darin, dass diese Ereignisse asynchron behandelt werden: Ihr Dienst wird zurückkehren, bevor diese Ereignisse behandelt wurden. (JMS wird sicherstellen, dass sie schließlich verarbeitet werden, es kann so konfiguriert werden, dass es mehrmals versucht wird und wie mit Fehlern umgegangen wird, ...). Je nach Ihren Bedürfnissen kann das gut oder schlecht sein.

Option 2: Transaktionssynchronisation

Alternativ, wenn JMS zu schwergewichtig für Ihren Fall ist, könnten Sie Transaktionssynchronisierung verwenden: Wenn Sie ein Ereignis senden, verwenden Sie Spring's TransactionSynchronizationManager.registerSynchronization und senden Sie die Nachricht direkt die afterCommit() Ihrer TransactionSynchronization . Sie können entweder eine neue Synchronisation pro zu sendendem Ereignis hinzufügen oder eine Synchronisation hinzufügen und verfolgen, welche Ereignisse gesendet werden, indem Sie ein Objekt mit dieser Liste an die Transaktion binden, indem Sie TransactionSynchronizationManager.bindResource verwenden.

Ich rate Ihnen davon ab, Ihr eigenes ThreadLocal dafür zu verwenden, weil das in manchen Fällen schief gehen würde; Zum Beispiel, wenn Sie innerhalb Ihrer Transaktion eine neue Transaktion starten ( RequiresNew ).

Unterschiede zu Ihrem aktuellen Setup:

  • Wenn in diesem Fall eine Ausnahme bei der Behandlung der Ereignisse ausgelöst wird, löst Ihr Dienst die Ausnahme aus, aber die Änderungen wurden bereits in der Datenbank festgeschrieben.
  • Wenn einer Ihrer Listener ebenfalls in die Datenbank schreibt, muss er dies in einer neuen Transaktion tun.

Alternativ können Sie beforeCommit anstelle von afterCommit verwenden, aber dann werden Ihre Ereignisse verarbeitet (E-Mails gesendet, ...), selbst wenn die eigentliche Übertragung an die Datenbank später fehlschlägt.

Dies ist weniger robust ( weniger transaktional ) als die Verwendung von JMS, aber leichter und einfacher einzurichten und normalerweise gut genug .

    
Wouter Coekaerts 12.08.2011, 11:28
quelle
1

Im Gegensatz zu Wouter Coekaerts verstehe ich, dass Sie nach einer Möglichkeit fragen, Benachrichtigungen zu senden, die nur dann an den Empfänger gesendet werden, wenn die Transaktion, in der sie erstellt wurden, erfolgreich war. - Sie suchen also nach etwas, das dem Transaktions-CDI- Ereignismechanismus.

Meine Idee, es zu lösen, ist so:

  • senden Sie die Ereignisse und "speichern" Sie sie in einer Liste in Ihrem Ereignisbehandlungsmechanismus, aber leiten Sie die Ereignisse nicht an die Empfänger weiter. (Ich denke, das ist einfach zu implementieren)
  • leitet die Ereignisse von der Liste zu den Recviers weiter, wenn die Transaktion erfolgreich war
  • Löschen Sie die Ereignisse aus der Liste, wenn die Transaktion ein Rollback
  • aufweist

Um die Ereignisse weiterzuleiten oder zu löschen, würde ich zuerst den Feder-Transaktionsmechanismus betrachten. Wenn es nicht möglich ist, sie zu erweitern, könnten Sie einen AOP-Aspekt schreiben, der die Ereignisse weiterleitet, wenn eine mit @Transactional annotierte Methode ohne (Runtime) Exception ausgeht. Wenn die Methode @Transactional annotierte Methode jedoch mit einer Ausnahme (Runtime) beendet wurde, löschen Sie die Ereignisse aus der Liste.

    
Ralph 12.08.2011 11:41
quelle
1

Wie @Wouter eine Alternative erwähnt, ist die Verwendung von asynchronen Messaging-Techniken. Ich kann mir zwei andere Ansätze vorstellen:

  1. Die Ereignisse, die von ItemService ausgelöst wurden, werden nach dem Festschreiben in der Datenbanktabelle gespeichert (so dass sie beim Rollback nicht verfügbar sind). Ein Hintergrundjob (asynchron) untersucht die Ereignisse und ruft die entsprechenden Dienste auf. Man könnte es "Selfmade JMS" nennen. Mögliche Alternative, wenn die Messaging-Infrastruktur nicht verfügbar ist.
  2. Verwenden Sie eine ThreadLocal , um alle Ereignisse und danach temporär in eine Warteschlange zu stellen die zusammengesetzten Transaktionen, feuern die Ereignisse ab. Dies ist nicht beständig und kann fehlschlagen, z.B. nach dem Festschreiben und bevor alle Ereignisse aus der Warteschlange genommen wurden. Ich bin nicht der Fan von ThreadLocal , aber es ist besser, als die Business-Interfaces (ItemService) mit nicht verwandten Parametern zu vermischen. Sehen Sie dies für die Frühjahrsaktion Implementierung .

Ich schätze, eine endgültige Antwort ist nicht möglich, da sie wirklich von den Details Ihres Systems abhängt, insbesondere von allen externen Diensten, die durch die Ereignisse ausgelöst werden.

    
home 12.08.2011 11:42
quelle