I explained how the Oracle Transaction Manager for Microservices (MicroTx) can help applications adopt distributed transactions using the XA protocol to guarantee data consistency across polyglot microservices. In this post I’ll show how one can use Sagas with MicroTx to achieve eventual data consistency across polyglot microservices. MicroTx supports Sagas in the form of the Eclipse MicroProfile Long Running Actions (LRA) protocol. This protocol is designed for applications that need to keep a distributed transaction open for a long period of time without locking all the resources involved as XA does.
The basic transaction flow as shown below is:
1. Transaction initiator calls a transaction coordinator to start the LRA
2. Transaction initiator then calls participant 1
3. Participant 1 calls the transaction coordinator to join the LRA
4. Transaction initiator then calls participant 2
5. Participant 2 calls the transaction coordinator to join the LRA
6. Initiator calls the transaction coordinator to Complete or Compensate the LRA
7. The transaction coordinator then calls Complete or Compensate on each of the participants
While this all seems trivial and obvious, it's actually relatively difficult to handle all the possible failure scenarios. This is where MicroTx comes into the picture.
Completing and Compensating an LRA
Once the initiator has decided to complete or compensate the LRA, the coordinator will call back to each of the participants to complete or compensate. What it means to complete or compensate the LRA is totally up to the application. The application must have the necessary business logic to perform these operations. For example, a participant might use the complete callback to finish a pending reservation by removing it from being pending to being complete, thus completing the participants participation in the LRA. On the other hand, it might use the compensate callback to delete the pending reservation and place the reserved item back in inventory.
A Java Implementation
Using LRA in a Java application is extremely simple. Aside from importing the LRA annotations, all that is necessary is to annotate the methods to indicate their role in the LRA. In the code shown below, the first annotation:
@LRA(value = LRA.Type.MANDATORY, end = false)
indicates that when this method is called, an LRA must already have been started. The end = false indicates that the LRA is to remain open after this method returns. The assumption here is that another participant started the LRA and then called this function, which is why end is set to false.
The next annotation:
@Complete
indicates that this method should be called when the LRA completes.
The last annotation:
@Compensate
indicates that this method should be called when the LRA requires compensation.
There are several other annotations that can be used to do such things as getting notified when the LRA closes (completes or compensates), or to indicate that a participant no longer needs to be involved in the LRA. Here is an example of what's required from an LRA participant. The changes required are highlighted in red assuming your application already has the required business logic in place.
import org.eclipse.microprofile.lra.annotation.ws.rs.LRA;
@Path("/hotelService/api")
@ApplicationScoped
public class HotelResource {
@POST
@Path("/hotel")
@Produces(MediaType.APPLICATION_JSON)
@LRA(value = LRA.Type.MANDATORY, end = false)
public Response bookRoom(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) String lraId, @QueryParam("hotelName") @DefaultValue("Default") String hotelName) {
Booking booking = hotelService.book(lraId, hotelName); //Business Logic that executes within an LRA
return Response.ok(booking).build();
}
@PUT
@Path("/complete")
@Produces(MediaType.TEXT_PLAIN)
@Complete
public Response completeWork(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) String lraId) throws NotFoundException {
// Business logic to complete the work related to this LRA
booking.setStatus(Booking.BookingStatus.CONFIRMED);
return Response.ok(ParticipantStatus.Completed.name()).build();
}
@PUT
@Path("/compensate")
@Produces(MediaType.TEXT_PLAIN)
@Compensate
public Response compensateWork(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) String lraId) throws NotFoundException {
// Business logic to compensate the work related to this LRA
booking.setStatus(Booking.BookingStatus.CANCELLED);
return Response.ok(ParticipantStatus.Compensated.name()).build();
}
}
Besides Java, MicroTx also supports LRA transaction in TypeScript using Node.js. A sample TypeScript application using LRA is included in the MicroTx distribution.
Reasons to Use LRA Protocol
There are several reasons one would want to use LRA based distributed transactions. From a 12-factor application perspective, LRA transactions provide less coupling compared to XA as there are no locks held across the microservices involved. LRA transaction also allow applications to take specific actions to complete or compensate a transaction, whereas in XA there is no such mechanism to provide application specific commitment or rollback logic.
Completion by Participant Rather Than Initiator
One thing that is unique about LRA transactions is that the microservice that initiates the LRA doesn’t have to be the same microservice that completes the LRA. This contrasts with the XA protocol where the initiator of the transaction is the only one that can commit or rollback the transaction. Depending upon the XA transaction coordinator, participants may be able to mark the transaction as having to rollback, but the transaction initiator is still the only one that can commit or rollback the transaction.
In some use cases, it may be valuable for a participant microservice in an LRA transaction to make the request to complete or compensate the LRA. One reason may be that a participant may know that the LRA needs to be compensated, so instead of waiting for the initiator to start the compensation phase, a participant microservice can also start the compensation action.
Participants Aware of Transaction Status and Outcome
Another difference between LRA transactions and XA transactions is that the participants in an LRA transaction can ask the transaction coordinator for the status of the transaction, as well as getting called back to learn about the outcome of the transaction.
Avoiding Lock Contention
However, the most common reason to use LRA is to try and avoid locks that are held for the entire duration of an XA distributed transaction. Lock contention can limit the throughput and increase the latency of applications using XA if attention isn’t paid to avoiding hotspots in the involved XA resources. This potentially allows more concurrency than XA may be able to provide. However, this added concurrency is achieved by using local transactions in each participant and dropping isolation as a requirement. As these local transactions commit before the distributed transaction is complete, it is up to the application logic to determine how to compensate an already committed local transaction if necessary.
Reasons to Avoid Using LRA Protocol
Just as there are reasons for using LRA transactions, there are also reasons to not using LRA transactions. The LRA protocol provides fewer guarantees than other distributed transaction protocols. While LRA provides loose coupling between the services, it does so at the expense of isolation.
Why Giving Up Isolation Matters
One issue to consider when selecting the LRA protocol is that LRA gives up the Isolation property from other transaction protocols such as XA that provide the full ACID properties:
◉ Atomicity – All updates occur or none of them occur
◉ Consistency – The system moves from one consistent state to another consistent state
◉ Isolation – The intermediate results of a transaction cannot be seen by other transactions
◉ Durability – Once the transaction is outcome has been decided, the results are durable
While giving up isolation may not seem too problematic, let's look at an example of how this could impact an application. We have two LRA transactions, LRA1 and LRA2.
1. LRA1 adds $1,000 to an account with a zero balance, so the balance is now $1,000
2. Before LRA1 completes, LRA2 removes $700 from the account
3. LRA2 completes, so the balance is now $300
4. LRA1 needs to compensate for some reason so the compensating action would be to remove $1,000 from the account. But there is only $300 in the account, so compensation in this case is impossible. The user that initiated LRA1 would potentially be out $700.
This is all a result of a dirty read LRA2 made due to the lack of isolation. Had isolation been provided, LRA2 would never have seen the $1,000 balance until LRA1 completed.
Source: oracle.com
0 comments:
Post a Comment