Het probleem:
Ik ben aan het spelen met EJB 3.0 en ik ben op een probleem gestuit waar ik echt niet uit kom. Het gaat om het afhandelen van berichten door een Message Driven Bean. Het is de bedoeling dat deze class, bij het ontvangen van een berichtje, een bedrag van één account naar de andere overschrijft. 2 bestaande Account objecten worden dus opgehaald, waarvan dan het balans (het bedrag) wordt aangepast.
Het is van groot belang dat dit middels een transactie gebeurt, anders is het mogelijk dat 2 berichten ervoor zorgen dat een Account 2 keer wordt uitgelezen en opgeslagen, en dat zo het balans niet meer klopt. Juist dit is het probleem, want ik krijg het met geen mogelijkheid in een transactie.
Als ik bijvoorbeeld 3 berichten verzend, dan worden ze alle 3 geaccepteerd door verschillende instanties van de Message Driven Bean class (concurrent dus). Dan worden de Account objecten opgehaald en gewijzigd, waarna de transacties één voor één aflopen doordat de functie beëindigd. Dit betekent dus meestal dat alleen het laatst gestuurde bericht effect heeft op het balans, omdat deze simpelweg de wijzigingen van de andere 2 overschrijft.
Wat heb ik geprobeerd:
Ik weet dat Message Driven Beans standaard door de container transacties krijgen toegewezen die standaard van het type REQUIRED zijn. Dit lijkt echter de problemen niet te voorkomen. Als ik zelf controle neem over de transacties (middels een EntityTransaction), dan gebeurt precies hetzelfde.
Ik heb ook verschillende manieren van het verzenden van een berichtje geprobeerd (zoals het voor ieder bericht opnieuw opzetten van een connection), maar dit had (zoals verwacht) geen effect.
Ik heb heel veel gezocht, maar ik word er echt niet wijs uit, helaas.
Waar ben ik naar op zoek?
Als ik er voor kan zorgen dat er maar echt 1 bericht tegelijk wordt verwerkt, dan ben ik al redelijk van het probleem af, maar dat kan later weer voor problemen zorgen. Wat ik eigenlijk dus wil, is dat de ingelezen objecten als het ware gelocked worden, totdat de wijziging is gedaan en het resultaat is opgeslagen in de database (de transactie is gecommit dus). Bij mijn weten is dit redelijk standaard bij een transactie, dus waarom dit dan nu niet werkt is me echt een raadsel. Hoe zorg ik er dus voor dat de 3 berichten zonder problemen verwerkt worden?
Code voorbeelden:
Sender:
Message Driven Bean:
Disclaimer: De 100% correctheid of veiligheid (behalve die transacties dan) van deze code is even geen issue, wat ik graag wil is dat ik het probleem snap, dat staat voorop. Dit is even een stuk versimpeld dus.
Ik ben aan het spelen met EJB 3.0 en ik ben op een probleem gestuit waar ik echt niet uit kom. Het gaat om het afhandelen van berichten door een Message Driven Bean. Het is de bedoeling dat deze class, bij het ontvangen van een berichtje, een bedrag van één account naar de andere overschrijft. 2 bestaande Account objecten worden dus opgehaald, waarvan dan het balans (het bedrag) wordt aangepast.
Het is van groot belang dat dit middels een transactie gebeurt, anders is het mogelijk dat 2 berichten ervoor zorgen dat een Account 2 keer wordt uitgelezen en opgeslagen, en dat zo het balans niet meer klopt. Juist dit is het probleem, want ik krijg het met geen mogelijkheid in een transactie.
Als ik bijvoorbeeld 3 berichten verzend, dan worden ze alle 3 geaccepteerd door verschillende instanties van de Message Driven Bean class (concurrent dus). Dan worden de Account objecten opgehaald en gewijzigd, waarna de transacties één voor één aflopen doordat de functie beëindigd. Dit betekent dus meestal dat alleen het laatst gestuurde bericht effect heeft op het balans, omdat deze simpelweg de wijzigingen van de andere 2 overschrijft.
Wat heb ik geprobeerd:
Ik weet dat Message Driven Beans standaard door de container transacties krijgen toegewezen die standaard van het type REQUIRED zijn. Dit lijkt echter de problemen niet te voorkomen. Als ik zelf controle neem over de transacties (middels een EntityTransaction), dan gebeurt precies hetzelfde.
Ik heb ook verschillende manieren van het verzenden van een berichtje geprobeerd (zoals het voor ieder bericht opnieuw opzetten van een connection), maar dit had (zoals verwacht) geen effect.
Ik heb heel veel gezocht, maar ik word er echt niet wijs uit, helaas.
Waar ben ik naar op zoek?
Als ik er voor kan zorgen dat er maar echt 1 bericht tegelijk wordt verwerkt, dan ben ik al redelijk van het probleem af, maar dat kan later weer voor problemen zorgen. Wat ik eigenlijk dus wil, is dat de ingelezen objecten als het ware gelocked worden, totdat de wijziging is gedaan en het resultaat is opgeslagen in de database (de transactie is gecommit dus). Bij mijn weten is dit redelijk standaard bij een transactie, dus waarom dit dan nu niet werkt is me echt een raadsel. Hoe zorg ik er dus voor dat de 3 berichten zonder problemen verwerkt worden?
Code voorbeelden:
Sender:
Java:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| InitialContext jndiContext = new InitialContext(); QueueConnectionFactory connectionFactory = (QueueConnectionFactory)jndiContext.lookup("java:/JmsXA"); Queue queue = (Queue)jndiContext.lookup("queue/AccountTransactionQueue"); QueueConnection connection = connectionFactory.createQueueConnection(); QueueSession session = connection.createQueueSession(true, 0); QueueSender sender = session.createSender(queue); for ( PendingTransaction pendingTransaction : pendingTransactions ) { MapMessage mapMessage = session.createMapMessage(); mapMessage.setInt("fromAccount", fromAccountNumber); mapMessage.setInt("toAccount", pendingTransaction.toAccountNumber); mapMessage.setDouble("amount", pendingTransaction.amount); sender.send(mapMessage); } sender.close(); session.close(); connection.close(); |
Message Driven Bean:
Java:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| @MessageDriven( activationConfig={ @ActivationConfigProperty( propertyName="destinationType", propertyValue="javax.jms.Queue" ), @ActivationConfigProperty( propertyName="destination", propertyValue="queue/AccountTransactionQueue" ) } ) public class AccountTransactionManager implements javax.jms.MessageListener { @PersistenceContext(unitName="AccountsDB") private EntityManager entityManager; public void onMessage(Message message) { MapMessage mapMessage = (MapMessage)message; int fromAccountNumber = mapMessage.getInt("fromAccount"); int toAccountNumber = mapMessage.getInt("toAccount"); double amount = mapMessage.getDouble("amount"); Account fromAccount = (Account)entityManager.find(Account.class, fromAccountNumber); Account toAccount = (Account)entityManager.find(Account.class, toAccountNumber); if ( fromAccount == null || toAccount == null ) { throw new Exception(); } fromAccount.setBalance(fromAccount.getBalance() - amount); toAccount.setBalance(toAccount.getBalance() + amount); } } |
Disclaimer: De 100% correctheid of veiligheid (behalve die transacties dan) van deze code is even geen issue, wat ik graag wil is dat ik het probleem snap, dat staat voorop. Dit is even een stuk versimpeld dus.