Sagas in microservices
source link: https://sergiuoltean.com/2018/07/12/sagas-in-microservices/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Sagas in microservices
We all know that the shared data in microservices(if they are done right) is eventually consistent. This is due to the CAP theorem which states that availability is usually a better choice than consistency. In a previous post I wrote about having eventual consistency data using Eventuate.
Since distributed transactions(2PC) are heavy with a bad performance, it was needed for something else, more lighter. Best effort 1PC is a lighter alternative. For example there is a ChainedTransactionManager in spring, but this comes with its problems. As the doc says “The configured instances will start transactions in the order given and commit/rollback in reverse order, which means the PlatformTransactionManager
most likely to break the transaction should be the last in the list configured.” This means if the most outer transaction breaks first we haven’t achieved anything. It works under the assumption that “that errors causing a transaction rollback will usually happen before the transaction completion or during the commit of the most inner PlatformTransactionManager
.
Now let’s concentrate on a different solution and that is the saga pattern. Yes the proposal is from 1987. A saga consists from a sequence of transactions, each transaction being triggered by the completion of the previous one. There are also compensation transactions that returns the data to a eventually consistent state. The latter ones are triggered in something goes wrong. A service can have one or more sagas.
Let’s take the following example. In the above image we have 2 services: Bid Service and Payment Service. After your bid is the winner you must pay for the item you bid. So the Bid Service is initiating a transaction that spans across Payment Service. Within the boundaries of a services we have local transactions so ACID is present. The transaction(blue arrow) that spans the services is a distributed transaction so here we have BASE. With ACID we have atomicity and consistency so we are covered here. With BASE with have eventual consistency so here the saga comes into play.
Let’s put this in practice using Eventuate Sagas. This uses an orchestrator component which issues the saga.
@Service
public
class
BidService {
private
BidRepository bidRepository;
@Autowired
private
SagaManager bidPaymentSagaSagaManager;
@Autowired
public
BidService(BidRepository bidRepository) {
this
.bidRepository = bidRepository;
}
@Transactional
public
Bid payForBid(Bid bid) {
BidPaymentSagaData data =
new
BidPaymentSagaData(bid.getItemCode(), bid.getAmount());
bidRepository.save(bid);
bidPaymentSagaSagaManager.create(data, Bid.
class
, bid.getItemCode());
return
bid;
}
public
Optional find(String itemCode) {
return
Optional.ofNullable(bidRepository.findByItemCode(itemCode));
}
public
void
update(Bid bid) {
bidRepository.save(bid);
}
}
It works with commands. The Bid Service is initiating a payment command which will be handled by the Payment Service Command Handler.
public
class
PaymentCommandHandler {
@Autowired
private
PaymentService paymentService;
public
CommandHandlers commandHandlerDefinitions() {
return
SagaCommandHandlersBuilder
.fromChannel(
"paymentService"
)
.onMessage(InitiatePaymentCommand.
class
,
this
::pay)
.build();
}
private
Message pay(CommandMessage commandMessage) {
InitiatePaymentCommand cmd = commandMessage.getCommand();
Payment payment =
new
Payment(cmd.getItemCode(), cmd.getAmount());
try
{
paymentService.pay(payment);
return
withSuccess();
}
catch
(Exception e) {
payment.reject();
return
withFailure();
}
}
}
This will respond either with success or failure. According to the saga we must trigger the next participant or a compensating transaction. The compensating transactions are triggered in reverse order.
public
class
BidPaymentSaga
implements
SimpleSaga {
@Override
public
SagaDefinition getSagaDefinition() {
return
step().withCompensation(
this
::reject)
.step().invokeParticipant(
this
::initiatePayment)
.step().invokeParticipant(
this
::approvePayment)
.build();
}
public
CommandWithDestination initiatePayment(BidPaymentSagaData data) {
return
send(
new
InitiatePaymentCommand(data.getItemCode(), data.getAmount()))
.to(
"paymentService"
)
.build();
}
public
CommandWithDestination approvePayment(BidPaymentSagaData data) {
return
send(
new
ApprovePaymentCommand(data.getItemCode()))
.to(
"bidService"
)
.build();
}
public
CommandWithDestination reject(BidPaymentSagaData data) {
return
send(
new
RejectPaymentCommand(data.getItemCode()))
.to(
"bidService"
)
.build();
}
}
Bid Payment Service is responsible for handling the outcome. The saga will send the corresponding message and the Bid Payment Command Handler will act accordingly. If the payment fails the bid will be rejected, and this means that we need a retry mechanism. But this is not in the scope of this post.
public
class
BidCommandHandler {
@Autowired
private
BidService bidService;
public
CommandHandlers commandHandlerDefinitions() {
return
SagaCommandHandlersBuilder
.fromChannel(
"bidService"
)
.onMessage(RejectPaymentCommand.
class
,
this
::rejectBid)
.onMessage(ApprovePaymentCommand.
class
,
this
::bidPayed)
.build();
}
private
Message rejectBid(CommandMessage cmd) {
RejectPaymentCommand rejectPaymentCommand = cmd.getCommand();
Optional bid = bidService.find(rejectPaymentCommand.getItemCode());
if
(bid.isPresent()) {
bid.get().reject();
bidService.update(bid.get());
}
return
withFailure();
}
private
Message bidPayed(CommandMessage cmd) {
ApprovePaymentCommand approvePaymentCommand = cmd.getCommand();
bidService.find(approvePaymentCommand.getItemCode()).ifPresent(Bid::approve);
return
withSuccess();
}
}
This is a great way to ensure eventual consistency. Eventuate Sagas is based on two great products: Debezium for CDC (change data capture) which will tail the transaction log and Apache Kafka which is a well known message broker. The future is bright for these two.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK